diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..24156ca --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,190 @@ +version: 2.1 + +executors: + python37-executor: + docker: + - image: circleci/python:3.7 + +commands: + install-test-suite: + description: Install testing packages + steps: + - restore_cache: + keys: + - cache-test-{{ checksum "requirements-test.txt" }} + + - run: + name: Installing testing packages + command: | + python -m venv venv + . venv/bin/activate + pip install -r requirements-test.txt + + - save_cache: + paths: + - ./venv + key: cache-test-{{ checksum "requirements-test.txt" }} + + install-sam: + description: Install AWS SAM + steps: + - restore_cache: + keys: + - cache-sam-{{ checksum "requirements-sam-cli.txt" }} + + - run: + name: Installing AWS SAM CLI + command: | + python -m venv venv + . venv/bin/activate + pip install -r requirements-sam-cli.txt + + - save_cache: + paths: + - ./venv + key: cache-sam-{{ checksum "requirements-sam-cli.txt" }} + +jobs: + unit-tests: + executor: python37-executor + + steps: + - checkout + + - install-test-suite + + - run: + name: Running unit tests + command: | + . venv/bin/activate + pytest -s -vv --cov=src/rules/ + + package: + executor: python37-executor + + steps: + - checkout + + - install-sam + + - run: + name: Copying Deep Security dependencies + command: | + for i in rules/*; do + cp -r src requirements.txt $i + done + + - run: + name: Building from source + command: | + . venv/bin/activate + sam build -t deep-security.yml + + - run: + name: Packaging Lambda functions + command: | + . venv/bin/activate + mkdir -p circleci-ws + sam package \ + --template-file .aws-sam/build/template.yaml \ + --s3-bucket "$LAMBDA_BUCKET" \ + --s3-prefix "$LAMBDA_PREFIX" \ + --output-template-file circleci-ws/packaged.yml + + - setup_remote_docker: + docker_layer_caching: true + + - run: + name: Scan cfn template for vulnerabilities (cfn_nag) + command: | + docker create -v /templates --name cfn_nag stelligent/cfn_nag /bin/true + docker cp $PWD/circleci-ws/packaged.yml cfn_nag:/templates + docker run --volumes-from cfn_nag stelligent/cfn_nag /templates/packaged.yml + + - persist_to_workspace: + root: circleci-ws + paths: + - packaged.yml + + deploy: + executor: python37-executor + + steps: + - checkout + + - attach_workspace: + at: circleci-ws + + - install-sam + + - run: + name: Deploying Lambda functions and Config rules + command: | + . venv/bin/activate + sam deploy \ + --stack-name "$STACK_NAME" \ + --template-file circleci-ws/packaged.yml \ + --capabilities CAPABILITY_IAM \ + --parameter-overrides \ + ConfigBucket="$CONFIG_BUCKET" \ + ConfigPrefix="$CONFIG_PREFIX" \ + DSUsernameKey="$DS_USERNAME_PARAM_STORE_KEY" \ + DSPasswordKey="$DS_PASSWORD_PARAM_STORE_KEY" \ + DSHostname="$DS_HOSTNAME" \ + DSPort="$DS_PORT" \ + DSTenant="$DS_TENANT" \ + DSIgnoreSslValidation="$DS_IGNORE_SSL_VALIDATION" \ + DSPolicy="$DS_POLICY" \ + DSControl="$DS_CONTROL" + + publish: + executor: python37-executor + + steps: + - checkout + + - attach_workspace: + at: circleci-ws + + - install-sam + + - run: + name: Publish to AWS Serverless Application Repository + command: | + . venv/bin/activate + sam publish --template circleci-ws/packaged.yml + +workflows: + version: 2 + + wf-deploy: + jobs: + - unit-tests + - package: + requires: + - unit-tests + filters: + branches: + only: master + - deploy: + requires: + - package + filters: + branches: + only: master + + wf-publish: + jobs: + - unit-tests + - package: + requires: + - unit-tests + filters: + branches: + only: master + - publish: + requires: + - package + filters: + branches: + only: master diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b1e0365..0000000 --- a/.gitignore +++ /dev/null @@ -1,63 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover - -# Translations -*.mo -*.pot - -# Django stuff: -*.log - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Mac -.DS_Store - -# Deployment binaries -deploy diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..8dada3e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 53beb6a..3754a9e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,368 @@ -# Project Retired -When AWS Config rules launched, this project was created in order to provide the custom code required to trigger various actions in Trend Micro Deep Security when various conditions were met within AWS Config. +# AWS Config Rules for Deep Security -Over time, the Deep Security API has evolved and more AWS Config features have been added, making this workflow easier using the APIs. Therefore this code has been retired and moved to a separate 'retired' branch. +A set of AWS Config Rules to help ensure that your AWS deployments are leveraging the protection of Deep Security. These rules help centralize your compliance information in one place, AWS Config. -To create the workflow using the newest APIs, please refer to [Integrate Deep Security with AWS Services](https://automation.deepsecurity.trendmicro.com/article/12_1/integrate-deep-security-with-aws-services?platform=dsaas) in the Deep Security Automation Center. \ No newline at end of file + +## Table of Contents + +* [Architecture](#architecture) +* [Setup](#setup) +* [Support](#support) +* [Contribute](#contribute) + +## Setup + +## Create a User in Deep Security + +During execution, the AWS Lambda functions will query the Deep Security API. To do this, they require a Deep Security login with permissions. + +You should set up a dedicated use account for API access. To configure the account with the minimum privileges (which reduces the risk if the credentials are exposed) required by this integration, follow the steps below. + +1. In Deep Security, go to *Administration* > *User Manager* > *Roles*. +1. Click **New**. Create a new role with a unique, meaningful name. +1. Under **Access Type**, select **Allow Access to web services API**. +1. Under **Access Type**, deselect **Allow Access to Deep Security Manager User Interface**. +1. On the **Computer Rights** tab, select either **All Computers** or **Selected Computers**, ensuring that only the greyed-out **View** right (under **Allow Users to**) is selected. +1. On the **Policy Rights** tab, select **Selected Policies**. Verify that no policies are selected. (The role does not grant rights for any policies.) +1. On the **User Rights** tab, select **Change own password and contact information only**. +1. On the **Other Rights** tab, verify that the default options remain, with only **View-Only** and **Hide** permissions. +1. Go to **Administration** > **User Manager** > **Users**. +1. Click **New**. Create a new user with a unique, meaningful name. +1. Select the role that you created in the previous section. + + +### Manage secrets + +Deep Security Config rules utilize AWS SSM Parameter Store and KMS to securely manage credentials. Before deploying +Deep Security Lambda functions and Config rules, you need to create entries in Parameter Store for above created +user's username and password. Be sure to select `SecureString` as parameter type and use appropriate KMS 'Customer managed key (CMK)' +from your account. These 2 Parameter Store keys will be added as environment variables for deployment process +described below. + + +### Deploy AWS Lambda and Config rules + +This project is designed be deployed via several Bash scripts, but certain configuration needs to be in place fist. + +Environment variables file - `deploy.config` + +> - `STACK_NAME`: CloudFormation stack name for all lambda and Config rule resources +> - `LAMBDA_BUCKET`: S3 bucket name where Lambda source code is uploaded +> - `LAMBDA_PREFIX`: S3 object prefix within `LAMBDA_BUCKET` +> - `CONFIG_BUCKET`: S3 bucket name where AWS Config to store history and files +> - `CONFIG_PREFIX`: S3 object prefix within `CONFIG_BUCKET` +> - `DS_HOSTNAME`: Deep Security Manager host name +> - `DS_PORT`: (optional) Deep Security Manager host port (default: 443) +> - `DS_TENANT`: (optional) Deep Security tenant name (default: '') +> - `DS_IGNORE_SSL_VALIDATION`: (optional) Whether to validate SSL connection to Deep Security Manager (default: false) +> - `DS_USERNAME_PARAM_STORE_KEY`: SSM Parameter Store key to retrieve Deep Security username +> - `DS_PASSWORD_PARAM_STORE_KEY`: SSM Parameter Store key to retrieve Deep Security password +> - `DS_POLICY`: Policy name to check used by `DoesInstanceHavePolicy` Lambda +> - `DS_CONTROL`: Control name to check used by `IsInstanceProtectedBy` Lambda (Allowed values are +> [ anti_malware, web_reputation, firewall, intrusion_prevention, integrity_monitoring, log_inspection ]) + +Dependencies + +- Python 3.7 +- AWS SAM CLI command line tools ([instructions](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)) +- AWS credentials correctly configured. ([instructions](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html)) + +To deploy + +`./deploy.sh` + +To run unit tests + +`pytest -s -vv` + +To publish to AWS Serverless Application Repository + +`./publish.sh` + + +## [circleci](https://circleci.com/) Configuration + +This project is managed by a CircleCI CI loop. It does require some configuration be set up to do so, though. + +- Create an account with `circleci` if you don't have one. +- In `circleci`, add this project (or your copy). +- In `project settings` of added project, `environment variables` section, add the following variables: +> - `STACK_NAME`: CloudFormation stack name for all lambda and Config rule resources +> - `LAMBDA_BUCKET`: S3 bucket name where Lambda source code is uploaded +> - `LAMBDA_PREFIX`: S3 object prefix within `LAMBDA_BUCKET` +> - `CONFIG_BUCKET`: S3 bucket name where AWS Config to store history and files +> - `CONFIG_PREFIX`: S3 object prefix within `CONFIG_BUCKET` +> - `DS_HOSTNAME`: Deep Security Manager host name +> - `DS_PORT`: (optional) Deep Security Manager host port (default: 443) +> - `DS_TENANT`: (optional) Deep Security tenant name (default: '') +> - `DS_IGNORE_SSL_VALIDATION`: (optional) Whether to validate SSL connection to Deep Security Manager (default: false) +> - `DS_USERNAME_PARAM_STORE_KEY`: SSM Parameter Store key to retrieve Deep Security username +> - `DS_PASSWORD_PARAM_STORE_KEY`: SSM Parameter Store key to retrieve Deep Security password +> - `DS_POLICY`: Policy name to check used by `DoesInstanceHavePolicy` Lambda +> - `DS_CONTROL`: Control name to check used by `IsInstanceProtectedBy` Lambda (Allowed values are +> [ anti_malware, web_reputation, firewall, intrusion_prevention, integrity_monitoring, log_inspection ]) +> - `AWS_ACCESS_KEY_ID`: Your AWS access key ID +> - `AWS_SECRET_ACCESS_KEY`: Your AWS secret access key +> - `AWS_SESSION_TOKEN`: (optional) Session token if you need one to access AWS +> - `AWS_DEFAULT_REGION`: AWS region to deploy into +- Done. Now when you push into your GitHub repository, `circleci` deployment will be triggered automatically. +- **Note:** Configuration settings for `circleci` are located at `.circleci/config.yml`. + +### Rules + +#### IsInstanceProtectedByAntiMalware + +Checks to see if the current instance is protected by Deep Security Anti-Malware controls. Anti-malware must be "on" and in "real-time" mode for the rule to be considered compliant. + +Lambda handler: **dsIsInstanceProtectedByAntiMalware.aws_config_rule_handler** + +##### Rule Parameters: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rule ParameterExpected Value TypeDescription
dsUsernameKeystringSSM Parameter Store key to retrive username of the Deep Security account to use for querying anti-malware status
dsPasswordKeystringSSM Parameter Store key to retrive password for the Deep Security account to use for querying anti-malware status.
dsPasswordEncryptionContextstring or URIThe encryption context used to encrypt the dsPassword. If this parameter is given, the rule will include the encryption context information when decrypting the dsPassword value. Requires dsPasswordKey to be useful. See [Protecting Your Deep Security Manager API Password](#protecting-your-deep-security-manager-api-password) below for more details. +
dsTenantstringOptional as long as dsHostname is specified. Indicates which tenant to sign in to within Deep Security
dsHostnamestringOptional as long as dsTenant is specified. Defaults to Deep Security as a Service. Indicates which Deep Security manager the rule should sign in to
dsPortintOptional. Defaults to 443. Indicates the port to connect to the Deep Security manager on
dsIgnoreSslValidationboolean (true or false)Optional. Use only when connecting to a Deep Security manager that is using a self-signed SSL certificate
+ +During execution, this rule sign in to the Deep Security API. You should setup a dedicated API access account to do this. Deep Security contains a robust role-based access control (RBAC) framework which you can use to ensure that this set of credentials has the least amount of privileges to success. + +This rule requires view access to one or more computers within Deep Security. + +#### IsInstanceProtectedBy + +Checks to see if the current instance is protected by any of Deep Security's controls. Controls must be "on" and set to their strongest setting (a/k/a "real-time" or "prevention") in order for the rule to be considered compliant. + +This is the generic version of *IsInstanceProtectedByAntiMalware*. + +Lambda handler: **dsIsInstanceProtectedBy.aws_config_rule_handler** + +##### Rule Parameters: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rule ParameterExpected Value TypeDescription
dsUsernameKeystringSSM Parameter Store key to retrive username of the Deep Security account to use for querying anti-malware status
dsPasswordKeystringSSM Parameter Store key to retrive password for the Deep Security account to use for querying anti-malware status.
dsPasswordEncryptionContextstring or URIThe encryption context used to encrypt the dsPassword. If this parameter is given, the rule will include the encryption context information when decrypting the dsPassword value. Requires dsPasswordKey to be useful. See [Protecting Your Deep Security Manager API Password](#protecting-your-deep-security-manager-api-password) below for more details. +
dsTenantstringOptional as long as dsHostname is specified. Indicates which tenant to sign in to within Deep Security
dsHostnamestringOptional as long as dsTenant is specified. Defaults to Deep Security as a Service. Indicates which Deep Security manager the rule should sign in to
dsPortintOptional. Defaults to 443. Indicates the port to connect to the Deep Security manager on
dsIgnoreSslValidationboolean (true or false)Optional. Use only when connecting to a Deep Security manager that is using a self-signed SSL certificate
dsControlstringThe name of the control to verify. Must be one of [ anti_malware, web_reputation, firewall, intrusion_prevention, integrity_monitoring, log_inspection ]
+ +During execution, this rule signs in to the Deep Security API. You should setup a dedicated API access account to do this. Deep Security contains a robust role-based access control (RBAC) framework which you can use to ensure that this set of credentials has the least amount of privileges to success. + +This rule requires view access to one or more computers within Deep Security. + +#### DoesInstanceHavePolicy + +Checks to see if the current instance is protected by a specific Deep Security policy. + +Lambda handler: **dsDoesInstanceHavePolicy.aws_config_rule_handler** + +##### Rule Parameters: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rule ParameterExpected Value TypeDescription
dsUsernameKeystringSSM Parameter Store key to retrive username of the Deep Security account to use for querying anti-malware status
dsPasswordKeystringSSM Parameter Store key to retrive password for the Deep Security account to use for querying anti-malware status.
dsPasswordEncryptionContextstring or URIThe encryption context used to encrypt the dsPassword. If this parameter is given, the rule will include the encryption context information when decrypting the dsPassword value. Requires dsPasswordKey to be useful. See [Protecting Your Deep Security Manager API Password](#protecting-your-deep-security-manager-api-password) below for more details. +
dsTenantstringOptional as long as dsHostname is specified. Indicates which tenant to sign in to within Deep Security
dsHostnamestringOptional as long as dsTenant is specified. Defaults to Deep Security as a Service. Indicates which Deep Security manager the rule should sign in to
dsPortintOptional. Defaults to 443. Indicates the port to connect to the Deep Security manager on
dsIgnoreSslValidationboolean (true or false)Optional. Use only when connecting to a Deep Security manager that is using a self-signed SSL certificate
dsPolicystringThe name of the policy to verify
+ +During execution, this rule signs in to the Deep Security API. You should setup a dedicated API access account to do this. Deep Security contains a robust role-based access control (RBAC) framework which you can use to ensure that this set of credentials has the least amount of privileges to success. + +This rule requires view access to one or more computers within Deep Security. + +#### IsInstanceClear + +Checks to see if the current instance is has any warnings, alerts, or errors in Deep Security. An instance is compliant if it does **not** have any warnings, alerts, or errors (a/k/a compliant, which means everything is working as expected with no active security alerts). + +Lambda handler: **dsIsInstanceClear.aws_config_rule_handler** + +##### Rule Parameters: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rule ParameterExpected Value TypeDescription
dsUsernameKeystringSSM Parameter Store key to retrive username of the Deep Security account to use for querying anti-malware status
dsPasswordKeystringSSM Parameter Store key to retrive password for the Deep Security account to use for querying anti-malware status.
dsPasswordEncryptionContextstring or URIThe encryption context used to encrypt the dsPassword. If this parameter is given, the rule will include the encryption context information when decrypting the dsPassword value. Requires dsPasswordKey to be useful. See [Protecting Your Deep Security Manager API Password](#protecting-your-deep-security-manager-api-password) below for more details. +
dsTenantstringOptional as long as dsHostname is specified. Indicates which tenant to sign in to within Deep Security
dsHostnamestringOptional as long as dsTenant is specified. Defaults to Deep Security as a Service. Indicates which Deep Security manager the rule should sign in to
dsPortintOptional. Defaults to 443. Indicates the port to connect to the Deep Security manager on
dsIgnoreSslValidationboolean (true or false)Optional. Use only when connecting to a Deep Security manager that is using a self-signed SSL certificate
+ +During execution, this rule signs in to the Deep Security API. You should setup a dedicated API access account to do this. Deep Security contains a robust role-based access control (RBAC) framework which you can use to ensure that this set of credentials has the least amount of privileges to success. + +This rule requires view access to one or more computers within Deep Security. + +## Support + +This is an Open Source community project. Project contributors may be able to help, +depending on their time and availability. Please be specific about what you're +trying to do, your system, and steps to reproduce the problem. + +For bug reports or feature requests, please +[open an issue](../issues). +You are welcome to [contribute](#contribute). + +Official support from Trend Micro is not available. Individual contributors may be +Trend Micro employees, but are not official support. + +## Contribute + +We accept contributions from the community. To submit changes: + +1. Fork this repository. +1. Create a new feature branch. +1. Make your changes. +1. Submit a pull request with an explanation of your changes or additions. + +We will review and work with you to release the code. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..236742e --- /dev/null +++ b/build.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -e + +echo "" +echo "Running unit tests..." + +pytest -s -vv + +echo "" +echo "Copying dependencies..." + +for i in rules/*; do + cp -r src requirements.txt $i +done + +echo "" +echo "Building from source..." + +if [[ "`uname`" == "Linux" ]]; then + sam build -t deep-security.yml +else + sam build -t deep-security.yml -u +fi + +echo "" diff --git a/clean.sh b/clean.sh new file mode 100755 index 0000000..61accdd --- /dev/null +++ b/clean.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -e + +echo "" +echo "Cleaning up..." + +for i in rules/*; do + rm -rf $i/src $i/tests $i/requirements.txt +done + +rm -rf .aws-sam + +echo "" diff --git a/deep-security.yml b/deep-security.yml new file mode 100644 index 0000000..93c1676 --- /dev/null +++ b/deep-security.yml @@ -0,0 +1,278 @@ +Transform: AWS::Serverless-2016-10-31 +Description: AWS CloudFormation template to create custom AWS Config rules that interact + with the Trend Micro Deep Security Manager. You will be billed for the AWS resources + used if you create a stack from this template. +Parameters: + ConfigBucket: + Description: Name of the S3 bucket for AWS Config to store history and files + Type: String + MinLength: 1 + MaxLength: 255 + ConfigPrefix: + Description: Object prefix inside config bucket + Type: String + MinLength: 1 + MaxLength: 255 + DSUsernameKey: + Description: Parameter Store key name for Deep Security Manager username + Type: String + DSPasswordKey: + Description: Parameter Store key name for Deep Security Manager password + Type: String + DSHostname: + Description: Deep Security Manager hostname + Type: String + DSPort: + Description: Deep Security Manager port + Type: Number + Default: 443 + DSTenant: + Description: Deep Security tenant name + Type: String + Default: '' + DSIgnoreSslValidation: + Description: Whether to ignore SSL validation on connection + Type: String + Default: false + DSPolicy: + Description: Deep Security policy to check against + Type: String + DSControl: + Description: Deep Security protection name to check against + Type: String + AllowedValues: [ anti_malware, web_reputation, firewall, intrusion_prevention, integrity_monitoring, log_inspection ] +Metadata: + AWS::ServerlessRepo::Application: + Name: Deep-Security-Config-Rules + Description: A set of AWS Config Rules to help ensure that your AWS deployments are leveraging the protection of Deep Security. + These rules help centralize your compliance information in one place, AWS Config. + Author: Trend Micro + SpdxLicenseId: Apache-2.0 + LicenseUrl: LICENSE + ReadmeUrl: README.md + Labels: ['trendmicro', 'deepsecurity', 'security', 'config'] + HomePageUrl: https://github.com/deep-security/aws-config + SemanticVersion: 0.0.1 + SourceCodeUrl: https://github.com/deep-security/aws-config +Resources: + dsDoesInstanceHavePolicyLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: rules/ds-DoesInstanceHavePolicy + Description: Custom AWS Config rule that checks with the Trend Micro Deep Security + Manager to see if the named policy is in effect. See https://github.com/deep-security/aws-config + for more details. + Handler: dsDoesInstanceHavePolicy.aws_config_rule_handler + Role: !GetAtt dsConfigRuleRole.Arn + Runtime: python3.7 + Timeout: 60 + dsDoesInstanceHavePolicyLambdaPermission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !GetAtt dsDoesInstanceHavePolicyLambda.Arn + Action: lambda:InvokeFunction + Principal: config.amazonaws.com + dsIsInstanceClearLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: rules/ds-IsInstanceClear + Description: Custom AWS Config rule that checks with the Trend Micro Deep Security + Manager to see if the instance is clear of any alerts, warnings, or errors. + See https://github.com/deep-security/aws-config for more details. + Handler: dsIsInstanceClear.aws_config_rule_handler + Role: !GetAtt dsConfigRuleRole.Arn + Runtime: python3.7 + Timeout: 60 + dsIsInstanceClearLambdaPermission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !GetAtt dsIsInstanceClearLambda.Arn + Action: lambda:InvokeFunction + Principal: config.amazonaws.com + dsIsInstanceProtectedByLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: rules/ds-IsInstanceProtectedBy + Description: Custom AWS Config rule that checks with the Trend Micro Deep Security + Manager to see if the instance is protected using the specified security control. + See https://github.com/deep-security/aws-config for more details. + Handler: dsIsInstanceProtectedBy.aws_config_rule_handler + Role: !GetAtt dsConfigRuleRole.Arn + Runtime: python3.7 + Timeout: 60 + dsIsInstanceProtectedByLambdaPermission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !GetAtt dsIsInstanceProtectedByLambda.Arn + Action: lambda:InvokeFunction + Principal: config.amazonaws.com + dsIsInstanceProtectedByAntiMalwareLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: rules/ds-IsInstanceProtectedByAntiMalware + Description: Custom AWS Config rule that checks with the Trend Micro Deep Security + Manager to see if the instance is protected using the anti-malware security + control. See https://github.com/deep-security/aws-config for more details. + Handler: dsIsInstanceProtectedByAntiMalware.aws_config_rule_handler + Role: !GetAtt dsConfigRuleRole.Arn + Runtime: python3.7 + Timeout: 60 + dsIsInstanceProtectedByAntiMalwareLambdaPermission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !GetAtt dsIsInstanceProtectedByAntiMalwareLambda.Arn + Action: lambda:InvokeFunction + Principal: config.amazonaws.com + dsConfigRuleRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: dsConfigRulePolicy + PolicyDocument: + Statement: + - Action: + - ssm:GetParameter + Effect: Allow + Resource: + - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/* + - Action: + - kms:Decrypt + Effect: Allow + Resource: '*' + - Action: + - s3:GetObject + Effect: Allow + Resource: !Sub arn:aws:s3:::${ConfigBucket}/${ConfigPrefix}/* + - Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + - logs:DescribeLogStreams + Effect: Allow + Resource: '*' + - Action: + - config:PutEvaluations + Effect: Allow + Resource: '*' + dsDoesInstanceHavePolicyRule: + Type: AWS::Config::ConfigRule + Properties: + Description: This rule checks with the Trend Micro Deep Security + Manager to see if the named policy is in effect. + Scope: + ComplianceResourceTypes: + - AWS::EC2::Instance + Source: + Owner: CUSTOM_LAMBDA + SourceIdentifier: !GetAtt dsDoesInstanceHavePolicyLambda.Arn + SourceDetails: + - EventSource: aws.config + MessageType: ConfigurationItemChangeNotification + - EventSource: aws.config + MessageType: OversizedConfigurationItemChangeNotification + InputParameters: + dsUsernameKey: !Ref DSUsernameKey + dsPasswordKey: !Ref DSPasswordKey + dsHostname: !Ref DSHostname + dsPort: !Ref DSPort + dsTenant: !Ref DSTenant + dsIgnoreSslValidation: !Ref DSIgnoreSslValidation + dsPolicy: !Ref DSPolicy + DependsOn: + - dsDoesInstanceHavePolicyLambdaPermission + dsIsInstanceClearRule: + Type: AWS::Config::ConfigRule + Properties: + Description: This rule checks with the Trend Micro Deep Security Manager to see + if the instance is clear of any alerts, warnings, or errors. + Scope: + ComplianceResourceTypes: + - AWS::EC2::Instance + Source: + Owner: CUSTOM_LAMBDA + SourceIdentifier: !GetAtt dsIsInstanceClearLambda.Arn + SourceDetails: + - EventSource: aws.config + MessageType: ConfigurationItemChangeNotification + - EventSource: aws.config + MessageType: OversizedConfigurationItemChangeNotification + InputParameters: + dsUsernameKey: !Ref DSUsernameKey + dsPasswordKey: !Ref DSPasswordKey + dsHostname: !Ref DSHostname + dsPort: !Ref DSPort + dsTenant: !Ref DSTenant + dsIgnoreSslValidation: !Ref DSIgnoreSslValidation + DependsOn: + - dsIsInstanceClearLambdaPermission + dsIsInstanceProtectedByRule: + Type: AWS::Config::ConfigRule + Properties: + Description: This rule checks with the Trend Micro Deep Security + Manager to see if the instance is protected using the specified security control. + Scope: + ComplianceResourceTypes: + - AWS::EC2::Instance + Source: + Owner: CUSTOM_LAMBDA + SourceIdentifier: !GetAtt dsIsInstanceProtectedByLambda.Arn + SourceDetails: + - EventSource: aws.config + MessageType: ConfigurationItemChangeNotification + - EventSource: aws.config + MessageType: OversizedConfigurationItemChangeNotification + InputParameters: + dsUsernameKey: !Ref DSUsernameKey + dsPasswordKey: !Ref DSPasswordKey + dsHostname: !Ref DSHostname + dsPort: !Ref DSPort + dsTenant: !Ref DSTenant + dsIgnoreSslValidation: !Ref DSIgnoreSslValidation + dsControl: !Ref DSControl + DependsOn: + - dsIsInstanceProtectedByLambdaPermission + dsIsInstanceProtectedByAntiMalwareRule: + Type: AWS::Config::ConfigRule + Properties: + Description: This rule checks with the Trend Micro Deep Security + Manager to see if the instance is protected using the anti-malware security + control. + Scope: + ComplianceResourceTypes: + - AWS::EC2::Instance + Source: + Owner: CUSTOM_LAMBDA + SourceIdentifier: !GetAtt dsIsInstanceProtectedByAntiMalwareLambda.Arn + SourceDetails: + - EventSource: aws.config + MessageType: ConfigurationItemChangeNotification + - EventSource: aws.config + MessageType: OversizedConfigurationItemChangeNotification + InputParameters: + dsUsernameKey: !Ref DSUsernameKey + dsPasswordKey: !Ref DSPasswordKey + dsHostname: !Ref DSHostname + dsPort: !Ref DSPort + dsTenant: !Ref DSTenant + dsIgnoreSslValidation: !Ref DSIgnoreSslValidation + DependsOn: + - dsIsInstanceProtectedByAntiMalwareLambdaPermission +Outputs: + dsDoesInstanceHavePolicyLambda: + Description: ARN for the dsDoesInstanceHavePolicy lambda + Value: !GetAtt dsDoesInstanceHavePolicyLambda.Arn + dsIsInstanceClearLambda: + Description: ARN for the dsIsInstanceClearLambda lambda + Value: !GetAtt dsIsInstanceClearLambda.Arn + dsIsInstanceProtectedByLambda: + Description: ARN for the dsIsInstanceProtectedByLambda lambda + Value: !GetAtt dsIsInstanceProtectedByLambda.Arn + dsIsInstanceProtectedByAntiMalwareLambda: + Description: ARN for the dsIsInstanceProtectedByAntiMalwareLambda lambda + Value: !GetAtt dsIsInstanceProtectedByAntiMalwareLambda.Arn diff --git a/deploy.config b/deploy.config new file mode 100644 index 0000000..71afb00 --- /dev/null +++ b/deploy.config @@ -0,0 +1,35 @@ + +# CFN stack name targeted for deployment +STACK_NAME=my-stack + +# S3 Bucket and object prefix to upload Lambda source code to +LAMBDA_BUCKET=my-lambda-bucket +LAMBDA_PREFIX=my-lambda-object-prefix + +# S3 Bucket and object prefix where AWS Config stores artifacts +CONFIG_BUCKET=my-config-bucket +CONFIG_PREFIX=my-config-object-prefix + +# Deep Security connection properties +# Note: username and password will be store in AWS Secret Manager +DS_HOSTNAME=0.0.0.0 +DS_PORT=443 +DS_TENANT= +DS_IGNORE_SSL_VALIDATION=true + +# Deep Security Config rules use SSM Parameter Store and KMS to +# retrieve credentials securely +# Below are Parameter Store keys for username and password +DS_USERNAME_PARAM_STORE_KEY=my-ds-username-key +DS_PASSWORD_PARAM_STORE_KEY=my-ds-password-key + + +# Other Deep Security parameters + +# Policy to check for DoesInstanceHavePolicy rule +DS_POLICY='my webserver policy' + +# Control to check for IsInstanceProtectedBy rule +# Valid values are [ anti_malware, web_reputation, firewall, +# intrusion_prevention, integrity_monitoring, log_inspection ] +DS_CONTROL=firewall diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..67bb3b0 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -e + +source deploy.config + +if [[ -z "$STACK_NAME" || \ + -z "$CONFIG_BUCKET" || \ + -z "$CONFIG_PREFIX" || \ + -z "$DS_USERNAME_PARAM_STORE_KEY" || \ + -z "$DS_PASSWORD_PARAM_STORE_KEY" || \ + -z "$DS_HOSTNAME" || \ + -z "$DS_POLICY" || \ + -z "$DS_CONTROL" ]] +then + echo "" >&2 + echo "Required parameters missing in 'deploy.config':" >&2 + echo " - STACK_NAME" >&2 + echo " - CONFIG_BUCKET" >&2 + echo " - CONFIG_PREFIX" >&2 + echo " - DS_USERNAME_PARAM_STORE_KEY" >&2 + echo " - DS_PASSWORD_PARAM_STORE_KEY" >&2 + echo " - DS_HOSTNAME" >&2 + echo " - DS_POLICY" >&2 + echo " - DS_CONTROL" >&2 + echo "" >&2 + exit 1 +fi + +./package.sh + +echo "" +echo "Deploying Lambda functions and Config rules..." + +sam deploy \ + --stack-name "$STACK_NAME" \ + --template-file packaged.yml \ + --capabilities CAPABILITY_IAM \ + --parameter-overrides \ + ConfigBucket="$CONFIG_BUCKET" \ + ConfigPrefix="$CONFIG_PREFIX" \ + DSUsernameKey="$DS_USERNAME_PARAM_STORE_KEY" \ + DSPasswordKey="$DS_PASSWORD_PARAM_STORE_KEY" \ + DSHostname="$DS_HOSTNAME" \ + DSPort="$DS_PORT" \ + DSTenant="$DS_TENANT" \ + DSIgnoreSslValidation="$DS_IGNORE_SSL_VALIDATION" \ + DSPolicy="$DS_POLICY" \ + DSControl="$DS_CONTROL" + +./clean.sh + +echo "" diff --git a/package.sh b/package.sh new file mode 100755 index 0000000..653d8a9 --- /dev/null +++ b/package.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -e + +source deploy.config + +if [[ -z "$LAMBDA_BUCKET" || \ + -z "$LAMBDA_PREFIX" ]] +then + echo "" >&2 + echo "Required parameters missing in 'deploy.config':" >&2 + echo " - LAMBDA_BUCKET" >&2 + echo " - LAMBDA_PREFIX" >&2 + echo "" >&2 + exit 1 +fi + +./build.sh + +echo "" +echo "Packaging Lambda functions..." + +sam package \ + --template-file .aws-sam/build/template.yaml \ + --s3-bucket "$LAMBDA_BUCKET" \ + --s3-prefix "$LAMBDA_PREFIX" \ + --output-template-file packaged.yml + +echo "" diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..26209cf --- /dev/null +++ b/publish.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -e + +./package.sh + +echo "" +echo "Publishing to AWS Serverless Application Repository..." + +sam publish --template packaged.yml + +./clean.sh + +echo "" diff --git a/requirements-sam-cli.txt b/requirements-sam-cli.txt new file mode 100644 index 0000000..46a2b29 --- /dev/null +++ b/requirements-sam-cli.txt @@ -0,0 +1,2 @@ +awscli==1.16.209 +aws-sam-cli==0.19.0 diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..ac5baf0 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,5 @@ +pytest==5.0.1 +pytest-cov==2.7.1 +boto3==1.9.199 +xmltodict==0.10.1 +jsonpickle==1.2 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2d65c8c --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +xmltodict==0.10.1 diff --git a/rules/ds-DoesInstanceHavePolicy/README.md b/rules/ds-DoesInstanceHavePolicy/README.md new file mode 100755 index 0000000..cfe05a8 --- /dev/null +++ b/rules/ds-DoesInstanceHavePolicy/README.md @@ -0,0 +1,9 @@ +# ds-DoesInstanceHavePolicy + +## Dependencies + +This is dependent on the `deepsecurity` code stored in the `src` directory. + +## Deployment + +Run the `deploy.sh` script in this project's root directory. diff --git a/rules/ds-DoesInstanceHavePolicy/dsDoesInstanceHavePolicy.py b/rules/ds-DoesInstanceHavePolicy/dsDoesInstanceHavePolicy.py new file mode 100755 index 0000000..55aa5b4 --- /dev/null +++ b/rules/ds-DoesInstanceHavePolicy/dsDoesInstanceHavePolicy.py @@ -0,0 +1,28 @@ +# ********************************************************************* +# Deep Security - Does Instance Have Policy ______? +# ********************************************************************* + +# Standard library +import logging +logging.getLogger().setLevel(logging.INFO) + +from src.rules.rule_does_instance_have_policy import RuleDoesInstanceHavePolicy + + +def aws_config_rule_handler(event, context): + try: + rule = RuleDoesInstanceHavePolicy(event) + response = rule.execute() + result = { + 'annotation': rule.compliance_msg, + 'response': response, + 'result': 'success' + } + except Exception as ex: + logging.error('Exception thrown: {}'.format(ex)) + result = { + 'response': ex, + 'result': 'failure' + } + + return result diff --git a/rules/ds-IsInstanceClear/README.md b/rules/ds-IsInstanceClear/README.md new file mode 100755 index 0000000..7f6032d --- /dev/null +++ b/rules/ds-IsInstanceClear/README.md @@ -0,0 +1,9 @@ +# ds-IsInstanceClear + +## Dependencies + +This is dependent on the `deepsecurity` code stored in the `src` directory. + +## Deployment + +Run the `deploy.sh` script in this project's root directory. diff --git a/rules/ds-IsInstanceClear/dsIsInstanceClear.py b/rules/ds-IsInstanceClear/dsIsInstanceClear.py new file mode 100755 index 0000000..26689be --- /dev/null +++ b/rules/ds-IsInstanceClear/dsIsInstanceClear.py @@ -0,0 +1,28 @@ +# ********************************************************************* +# Deep Security - Is Instance Clear? +# ********************************************************************* + +# Standard library +import logging +logging.getLogger().setLevel(logging.INFO) + +from src.rules.rule_is_instance_clear import RuleIsInstanceClear + + +def aws_config_rule_handler(event, context): + try: + rule = RuleIsInstanceClear(event) + response = rule.execute() + result = { + 'annotation': rule.compliance_msg, + 'response': response, + 'result': 'success' + } + except Exception as ex: + logging.error('Exception thrown: {}'.format(ex)) + result = { + 'response': ex, + 'result': 'failure' + } + + return result diff --git a/rules/ds-IsInstanceProtectedBy/README.md b/rules/ds-IsInstanceProtectedBy/README.md new file mode 100755 index 0000000..eebfd63 --- /dev/null +++ b/rules/ds-IsInstanceProtectedBy/README.md @@ -0,0 +1,9 @@ +# ds-IsInstanceProtectedBy + +## Dependencies + +This is dependent on the `deepsecurity` code stored in the `src` directory. + +## Deployment + +Run the `deploy.sh` script in this project's root directory. diff --git a/rules/ds-IsInstanceProtectedBy/dsIsInstanceProtectedBy.py b/rules/ds-IsInstanceProtectedBy/dsIsInstanceProtectedBy.py new file mode 100755 index 0000000..570b44d --- /dev/null +++ b/rules/ds-IsInstanceProtectedBy/dsIsInstanceProtectedBy.py @@ -0,0 +1,28 @@ +# ********************************************************************* +# Deep Security - Is Instance Protected By _______? +# ********************************************************************* + +# Standard library +import logging +logging.getLogger().setLevel(logging.INFO) + +from src.rules.rule_is_instance_protected_by import RuleIsInstanceProtectedBy + + +def aws_config_rule_handler(event, context): + try: + rule = RuleIsInstanceProtectedBy(event) + response = rule.execute() + result = { + 'annotation': rule.compliance_msg, + 'response': response, + 'result': 'success' + } + except Exception as ex: + logging.error('Exception thrown: {}'.format(ex)) + result = { + 'response': ex, + 'result': 'failure' + } + + return result diff --git a/rules/ds-IsInstanceProtectedByAntiMalware/README.md b/rules/ds-IsInstanceProtectedByAntiMalware/README.md new file mode 100755 index 0000000..ad29dec --- /dev/null +++ b/rules/ds-IsInstanceProtectedByAntiMalware/README.md @@ -0,0 +1,9 @@ +# ds-IsInstanceProtectedByAntiMalware + +## Dependencies + +This is dependent on the `deepsecurity` code stored in the `src` directory. + +## Deployment + +Run the `deploy.sh` script in this project's root directory. diff --git a/rules/ds-IsInstanceProtectedByAntiMalware/dsIsInstanceProtectedByAntiMalware.py b/rules/ds-IsInstanceProtectedByAntiMalware/dsIsInstanceProtectedByAntiMalware.py new file mode 100755 index 0000000..584e7d5 --- /dev/null +++ b/rules/ds-IsInstanceProtectedByAntiMalware/dsIsInstanceProtectedByAntiMalware.py @@ -0,0 +1,28 @@ +# ********************************************************************* +# Deep Security - Is Instance Protected By AntiMalware? +# ********************************************************************* + +# Standard library +import logging +logging.getLogger().setLevel(logging.INFO) + +from src.rules.rule_is_instance_protected_by_anti_malware import RuleIsInstanceProtectedByAntiMalware + + +def aws_config_rule_handler(event, context): + try: + rule = RuleIsInstanceProtectedByAntiMalware(event) + response = rule.execute() + result = { + 'annotation': rule.compliance_msg, + 'response': response, + 'result': 'success' + } + except Exception as ex: + logging.error('Exception thrown: {}'.format(ex)) + result = { + 'response': ex, + 'result': 'failure' + } + + return result diff --git a/src/README.md b/src/README.md new file mode 100755 index 0000000..705f8db --- /dev/null +++ b/src/README.md @@ -0,0 +1,5 @@ +# Source Folder + +This folder contains the python modules required by various rules to execute. For an AWS Lambda deployment, it's far easier to embed requirements within the function vs. installing during execution. This stabilizes the code base and ensures that execution is consistent. + +Each function requires its own copy of its pre-requisites. diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/deepsecurity/__init__.py b/src/deepsecurity/__init__.py new file mode 100755 index 0000000..d198ca6 --- /dev/null +++ b/src/deepsecurity/__init__.py @@ -0,0 +1,11 @@ +# make sure we add the current path structure to sys.path +# this is required to import local dependencies +import sys +import os +current_path = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(current_path) + +# import project files as required +import dsm +import translation +translation.Terms.read_terms_file() \ No newline at end of file diff --git a/src/deepsecurity/computers.py b/src/deepsecurity/computers.py new file mode 100755 index 0000000..043c2be --- /dev/null +++ b/src/deepsecurity/computers.py @@ -0,0 +1,303 @@ +# standard library +import datetime + +# 3rd party libraries + +# project libraries +import core + +class Computers(core.CoreDict): + def __init__(self, manager=None): + core.CoreDict.__init__(self) + self.manager = manager + self.log = self.manager.log if self.manager else None + + def get(self, detail_level='HIGH', computer_id=None, computer_group_id=None, policy_id=None, computer_name=None, external_id=None, external_group_id=None): + """ + Get all or a filtered set of computers from Deep Security + + Can filter by: + computer_id + computer_group_id + policy_id + computer_name (specific or starts with using name*) + external_id + external_group_id + + If multiple filters are requested, only one is applied. The priority is; + external_id + external_group_id + computer_name + computer_id + computer_group_id + policy_id + + detail_level can be set to one of ['HIGH', 'MEDIUM', 'LOW'] + """ + # make sure we have a valid detail level + detail_level = detail_level.upper() + if not detail_level in ['HIGH', 'MEDIUM', 'LOW']: detail_level = 'HIGH' + + call = None + if external_id and external_group_id: + call = self.manager._get_request_format(call='hostDetailRetrieveByExternal') + if external_id: + call['data'] = { + 'externalFilter': { + 'hostExternalID': external_id, + 'hostGroupExternalID': None, + }, + 'hostDetailLevel': detail_level + } + elif external_group_id: + call['data'] = { + 'externalFilter': { + 'hostExternalID': None, + 'hostGroupExternalID': external_group_id, + }, + 'hostDetailLevel': detail_level + } + elif computer_name: + if computer_name.endswith('*'): + call = self.manager._get_request_format(call='hostDetailRetrieveByNameStartsWith') + call['data'] = { + 'hostname': computer_name, + 'hostDetailLevel': detail_level + } + else: + call = self.manager._get_request_format(call='hostDetailRetrieveByName') + call['data'] = { + 'startsWithHostname': computer_name.rstrip('*'), + 'hostDetailLevel': detail_level + } + else: + # get with no arguments = hostRetrieve() with ALL_HOSTS + call = self.manager._get_request_format(call='hostDetailRetrieve') + if computer_id: + call['data'] = { + 'hostFilter': { + 'hostGroupID': None, + 'hostID': computer_id, + 'securityProfileID': None, + 'type': 'ALL_HOSTS', + }, + 'hostDetailLevel': detail_level + } + elif computer_group_id: + call['data'] = { + 'hostFilter': { + 'hostGroupID': computer_group_id, + 'hostID': None, + 'securityProfileID': None, + 'type': 'ALL_HOSTS', + }, + 'hostDetailLevel': detail_level + } + elif policy_id: + call['data'] = { + 'hostFilter': { + 'hostGroupID': None, + 'hostID': None, + 'securityProfileID': policy_id, + 'type': 'ALL_HOSTS', + }, + 'hostDetailLevel': detail_level + } + else: + call['data'] = { + 'hostFilter': { + 'hostGroupID': None, + 'hostID': None, + 'securityProfileID': None, + 'type': 'ALL_HOSTS', + }, + 'hostDetailLevel': detail_level + } + + response = self.manager._request(call) + + if response and response['status'] == 200: + if not type(response['data']) == type([]): response['data'] = [response['data']] + for computer in response['data']: + computer_obj = None + try: + computer_obj = Computer(self.manager, computer, self.log) + except Exception as err: + self.log("Could not create Computer from API response") + if computer_obj: + self[computer_obj.id] = computer_obj + self.log("Added Computer {}".format(computer_obj.id), level='debug') + + try: + # add this computer to any appropriate groups on the Manager() + if 'computer_group_id' in dir(computer_obj) and computer_obj.computer_group_id: + if self.manager.computer_groups and computer_obj.computer_group_id in self.manager.computer_groups: + self.manager.computer_groups[computer_obj.computer_group_id].computers[computer_obj.id] = computer_obj + self.log("Added Computer {} to ComputerGroup {}".format(computer_obj.id, computer_obj.computer_group_id), level='debug') + except Exception as hostGroupid_err: + self.log("Could not add Computer {} to ComputerGroup".format(computer_obj.id), err=hostGroupid_err) + + try: + # add this computer to any appropriate policies on the Manager() + if 'policy_id' in dir(computer_obj) and computer_obj.policy_id: + if self.manager.policies and computer_obj.policy_id in self.manager.policies: + self.manager.policies[computer_obj.policy_id].computers[computer_obj.id] = computer_obj + self.log("Added Computer {} to Policy {}".format(computer_obj.id, computer_obj.policy_id), level='debug') + except Exception as securityProfileid_err: + self.log("Could not add Computer {} to Policy".format(computer_obj.id), err=securityProfileid_err) + + return len(self) + +class ComputerGroups(core.CoreDict): + def __init__(self, manager=None): + core.CoreDict.__init__(self) + self.manager = manager + self._exempt_from_find.append('computers') + self.computers = core.CoreDict() + self.log = self.manager.log if self.manager else None + + def get(self, name=None, group_id=None): + """ + Get all or a filtered set of computer groups from Deep Security + + If a name or group_id is specified, will only retrieve the + computer groups matching the name or group_id. + + If both are specified, name takes priority + """ + call = None + if name or group_id: + # filtered call + if name: + call = self.manager._get_request_format(call='hostGroupRetrieveByName') + call['data'] = { + 'Name': name + } + elif group_id: + call = self.manager._get_request_format(call='hostGroupRetrieve') + call['data'] = { + 'id': '{}'.format(group_id) + } + else: + call = self.manager._get_request_format(call='hostGroupRetrieveAll') + + response = self.manager._request(call) + if response and response['status'] == 200: + self.clear() # empty the current groups + if not type(response['data']) == type([]): response['data'] = [response['data']] + for group in response['data']: + computer_group_obj = ComputerGroup(self.manager, group, self.log) + if computer_group_obj: + self[computer_group_obj.id] = computer_group_obj + self.log("Added ComputerGroup {}".format(computer_group_obj.id), level='debug') + + return len(self) + +class Computer(core.CoreObject): + def __init__(self, manager=None, api_response=None, log_func=None): + self.manager = manager + self.recommended_rules = None + if api_response: self._set_properties(api_response, log_func) + + if not ('id') in dir(self): raise Exception("Could not create Computer from response") + + def send_events(self): + """ + Send the latest set of events to this computer's Manager + """ + return self.manager.request_events_from_computer(self.id) + + def clear_alerts_and_warnings(self): + """ + Clear any alerts or warnings for the computer + """ + return self.manager.clear_alerts_and_warnings_from_computers(self.id) + + def scan_for_malware(self): + """ + Request a malware scan be run on the computer + """ + return self.manager.scan_computers_for_malware(self.id) + + def scan_for_integrity(self): + """ + Request an integrity scan be run on the computer + """ + return self.manager.scan_computers_for_integrity(self.id) + + def scan_for_recommendations(self): + """ + Request a recommendation scan be run on the computer + """ + return self.manager.scan_computers_for_recommendations(self.id) + + def assign_policy(self, policy_id): + """ + Assign the specified policy to the computer + """ + return self.manager.assign_policy_to_computers(policy_id, self.id) + + def get_recommended_rules(self): + """ + Recommend a set of rules to apply to the computer + """ + self.recommended_rules = self.manager.get_rule_recommendations_for_computer(self.id) + return self.recommended_rules['total_recommedations'] + +class ComputerGroup(core.CoreObject): + def __init__(self, manager=None, api_response=None, log_func=None): + self.manager = manager + if api_response: self._set_properties(api_response, log_func) + self.computers = core.CoreDict() + + def send_events(self): + """ + Send the latest set of events for all computers in this group + """ + results = {} + for computer_id, computer in self.computers.items(): + if 'send_events' in dir(computer): + results[computer_id] = computer.send_events() + + return results + + def clear_alerts_and_warnings(self): + """ + Clear any alerts or warnings for all computers in this group + """ + return self.manager.clear_alerts_and_warnings_from_computers(self.computers.keys()) + + def scan_for_malware(self): + """ + Request a malware scan be run on all computers in this group + """ + return self.manager.scan_computers_for_malware(self.computers.keys()) + + def scan_for_integrity(self): + """ + Request an integrity scan be run on all computers in this group + """ + return self.manager.scan_computers_for_integrity(self.computers.keys()) + + def scan_for_recommendations(self): + """ + Request a recommendation scan be run on all computers in this group + """ + return self.manager.scan_computers_for_recommendations(self.computers.keys()) + + def assign_policy(self, policy_id): + """ + Assign the specified policy to all computers in this group + """ + return self.manager.assign_policy_to_computers(policy_id, self.computers.keys()) + + def get_recommended_rules(self): + """ + Recommend a set of rules to apply for each computer in this group + """ + results = {} + + for computer_id in self.computers.keys(): + results[computer_id] = self.computers[computer_id].get_recommended_rules() + + return results \ No newline at end of file diff --git a/src/deepsecurity/core.py b/src/deepsecurity/core.py new file mode 100755 index 0000000..4090df8 --- /dev/null +++ b/src/deepsecurity/core.py @@ -0,0 +1,558 @@ +# standard library +import collections +import json +import logging +import re +import ssl +from urllib.request import build_opener, HTTPSHandler, Request +from urllib.parse import urlencode + +# 3rd party libraries +import xmltodict + +# project libraries +import translation + +class CoreApi(object): + def __init__(self): + self.API_TYPE_REST = 'REST' + self.API_TYPE_SOAP = 'SOAP' + self._rest_api_endpoint = '' + self._soap_api_endpoint = '' + self._sessions = { self.API_TYPE_REST: None, self.API_TYPE_SOAP: None } + self.ignore_ssl_validation = False + self._log_at_level = logging.WARNING + self.logger = self._set_logging() + + # ******************************************************************* + # properties + # ******************************************************************* + @property + def log_at_level(self): return self._log_at_level + + @log_at_level.setter + def log_at_level(self, value): + """ + Make sure logging is always set at a valid level + """ + if value in [ + logging.CRITICAL, + logging.DEBUG, + logging.ERROR, + logging.FATAL, + logging.INFO, + logging.WARNING, + ]: + self._log_at_level = value + self._set_logging() + else: + if not self._log_at_level: + self._log_at_level = logging.WARNING + self._set_logging() + + # ******************************************************************* + # methods + # ******************************************************************* + def _set_logging(self): + """ + Setup the overall logging environment + """ + # Based on tips from http://www.blog.pythonlibrary.org/2012/08/02/python-101-an-intro-to-logging/ + logging.basicConfig(level=self.log_at_level) + + # setup module logging + logger = logging.getLogger("DeepSecurity.API") + logger.setLevel(self.log_at_level) + + # reset any existing handlers + logging.root.handlers = [] # @TODO evaluate impact to other modules + logger.handlers = [] + + # add the desired handler + formatter = logging.Formatter('[%(asctime)s]\t%(message)s', '%Y-%m-%d %H:%M:%S') + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(formatter) + logger.addHandler(stream_handler) + + return logger + + def _get_request_format(self, api=None, call=None, use_cookie_auth=False): + if not api: api = self.API_TYPE_SOAP + return { + 'api': api, + 'call': call, + 'use_cookie_auth': use_cookie_auth, + 'query': None, + 'data': None, + } + + def _request(self, request, auth_required=True): + """ + Make an HTTP(S) request to an API endpoint based on what's specified in the + request object passed + + ## Input + + Required request keys: + api + Either REST or SOAP + + call + Name of the SOAP method or relative path of the REST URL + + Optional keys: + query + Contents of the query string passed as a dict + + data + Data to post. For SOAP API calls this will be the SOAP envelope. For + REST API calls this will be a dict converted to JSON automatically + by this method + + use_cookie_auth + Whether or not to use an HTTP Cookie in lieu of a querystring for authorization + + ## Output + + Returns a dict: + status + Number HTTP status code returned by the response, if any + + raw + The raw contents of the response, if any + + data + A python dict representing the data contained in the response, if any + """ + for required_key in [ + 'api', + 'call' + ]: + if not (required_key in request and request[required_key]): + self.log("All requests are required to have a key [{}] with a value".format(required_key), level='critical') + return None + + url = None + if request['api'] == self.API_TYPE_REST: + url = "{}/{}".format(self._rest_api_endpoint, request['call'].lstrip('/')) + else: + url = self._soap_api_endpoint + + self.log("Making a request to {}".format(url), level='debug') + + # add the authentication parameters + if auth_required: + if request['api'] == self.API_TYPE_REST: + if not request['use_cookie_auth']: # sID is a query string + if not request['query']: request['query'] = {} + request['query']['sID'] = self._sessions[self.API_TYPE_REST] + elif request['api'] == self.API_TYPE_SOAP: + # sID is part of the data + if not request['data']: request['data'] = {} + request['data']['sID'] = self._sessions[self.API_TYPE_SOAP] + + # remove any blank request keys + for k, v in request.items(): + if not v: request[k] = None + + # prep the query string + if 'query' in request and request['query']: + # get with query string + qs = {} + for k, v in request['query'].items(): # strip out null entries + if v: qs[k] = v + + url += '?%s' % urlencode(qs) + self.log("Added query string. Full URL is now {}".format(url), level='debug') + + self.log("URL to request is: {}".format(url)) + + # Prep the SSL context + ssl_context = ssl.create_default_context() + if self.ignore_ssl_validation: + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + self.log("SSL certificate validation has been disabled for this call", level='warning') + + # Prep the URL opener + url_opener = build_opener(HTTPSHandler(context=ssl_context)) + + # Prep the request + request_type = 'GET' + headers = { + 'Accept': 'application/json,text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*', + 'Content-Type': 'application/json', + } + + # authentication calls don't accept the Accept header + if request['call'].startswith('authentication'): del(headers['Accept']) + + # some rest calls use a cookie to pass the sID + if request['api'] == self.API_TYPE_REST and request['use_cookie_auth']: + headers['Cookie'] = 'sID="{}"'.format(self._sessions[self.API_TYPE_REST]) + + if request['api'] == self.API_TYPE_REST and request['call'] in [ + 'apiVersion', + 'status/manager/ping' + ]: + headers = { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*', + 'Content-Type': 'text/plain', + } + + if request['api'] == self.API_TYPE_SOAP: + # always a POST + headers = { + 'SOAPAction': '', + 'content-type': 'application/soap+xml' + } + data = self._prep_data_for_soap(request['call'], request['data']) + url_request = Request(url, data=data, headers=headers) + request_type = 'POST' + self.log("Making a SOAP request with headers {}".format(headers), level='debug') + self.log(" and data {}".format(data), level='debug') + elif request['call'] == 'authentication/logout': + url_request = Request(url, headers=headers) + setattr(url_request, 'get_method', lambda: 'DELETE') # make this request use the DELETE HTTP verb + request_type = 'DELETE' + self.log("Making a REST DELETE request with headers {}".format(headers), level='debug') + elif 'data' in request and request['data']: + # POST + url_request = Request(url, data=json.dumps(request['data']).encode('utf-8'), headers=headers) + request_type = 'POST' + self.log("Making a REST POST request with headers {}".format(headers), level='debug') + self.log(" and data {}".format(request['data']), level='debug') + else: + # GET + url_request = Request(url, headers=headers) + self.log("Making a REST GET request with headers {}".format(headers), level='debug') + + # Make the request + response = None + try: + response = url_opener.open(url_request) + except Exception as url_err: + self.log("Failed to make {} {} call [{}]".format(request['api'].upper(), request_type, request['call'].lstrip('/')), err=url_err) + + # Convert the request from JSON + result = { + 'status': response.getcode() if response else None, + 'raw': response.read() if response else None, + 'headers': dict(response.headers) if response else dict(), + 'data': None + } + bytes_of_data = len(result['raw']) if result['raw'] else 0 + self.log("Call returned HTTP status {} and {} bytes of data".format(result['status'], bytes_of_data), level='debug') + + if response: + if request['api'] == self.API_TYPE_SOAP: + # XML response + try: + if result['raw']: + full_data = xmltodict.parse(result['raw']) + if 'soapenv:Envelope' in full_data and 'soapenv:Body' in full_data['soapenv:Envelope']: + result['data'] = full_data['soapenv:Envelope']['soapenv:Body'] + if '{}Response'.format(request['call']) in result['data']: + if '{}Return'.format(request['call']) in result['data']['{}Response'.format(request['call'])]: + result['data'] = result['data']['{}Response'.format(request['call'])]['{}Return'.format(request['call'])] + else: + result['data'] = result['data']['{}Response'.format(request['call'])] + else: + result['data'] = full_data + except Exception as xmltodict_err: + self.log("Could not convert response from call {}".format(request['call']), err=xmltodict_err) + else: + # JSON response + try: + if result['raw'] and result['status'] != 204: + result['type'] = result['headers']['content-type'] + result['data'] = json.loads(result['raw']) if 'json' in result['type'] else None + except Exception as json_err: + # report the exception as 'info' because it's not fatal and the data is + # still captured in result['raw'] + self.log("Could not convert response from call {} to JSON. Threw exception:\n\t{}".format(request['call'], json_err), level='info') + + return result + + def _prefix_keys(self, prefix, d): + """ + Add the specified XML namespace prefix to all keys in the + passed dict + """ + if not type(d) == type({}): return d + new_d = d.copy() + for k,v in d.items(): + new_key = u"{}:{}".format(prefix, k) + new_v = v + if type(v) == type({}): new_v = self._prefix_keys(prefix, v) + new_d[new_key] = new_v + del(new_d[k]) + + return new_d + + def _prep_data_for_soap(self, call, details): + """ + Prepare the complete XML SOAP envelope + """ + data = xmltodict.unparse(self._prefix_keys('ns1', { call: details }), pretty=False, full_document=False) + soap_xml = """ + + + + + {} + + + """.format(data).strip() + + # convert any nil values to the proper format + soap_xml = re.sub(r'<([^>]+)>', r'<\1 xsi:nil="true" />', soap_xml) + + return soap_xml.encode('utf-8') + + def log(self, message='', err=None, level='info'): + """ + Log a message + """ + if not level.lower() in [ + 'critical', + 'debug', + 'error', + 'fatal', + 'info', + 'warning' + ]: level = 'info' + + if err: + level = 'error' + message += ' Threw exception:\n\t{}'.format(err) + + try: + func = getattr(self.logger, level.lower()) + func(message) + except Exception as log_err: + self.logger.critical("Could not write to log. Threw exception:\n\t{}".format(log_err)) + +class CoreDict(dict): + def __init__(self): + self._exempt_from_find = [] + + def get(self): pass + + def find(self, **kwargs): + """ + Find any keys where the values match the cumulative kwargs patterns + + If a keyword's value is a list, .find will match on any value for that keyword + + .find(id=1) + >>> returns any item with a property 'id' and value in [1] + possibilities: + { 'id': 1, 'name': 'One'} + { 'id': 1, 'name': 'Two'} + + .find(id=[1,2]) + >>> returns any item with a property 'id' and value in [1,2] + possibilities: + { 'id': 1, 'name': 'One'} + { 'id': 2, 'name': 'One'} + { 'id': 1, 'name': 'Two'} + { 'id': 2, 'name': 'Two'} + + .find(id=1, name='One') + >>> returns any item with a property 'id' and value in [1] AND a property 'name' and value in ['One'] + possibilities: + { 'id': 1, 'name': 'One'} + + .find(id=[1,2], name='One') + >>> returns any item with a property 'id' and value in [1,2] AND a property 'name' and value in ['One'] + possibilities: + { 'id': 1, 'name': 'One'} + { 'id': 2, 'name': 'One'} + + .find(id=[1,2], name=['One,Two']) + >>> returns any item with a property 'id' and value in [1,2] AND a property 'name' and value in ['One','Two'] + possibilities: + { 'id': 1, 'name': 'One'} + { 'id': 2, 'name': 'One'} + { 'id': 1, 'name': 'Two'} + { 'id': 2, 'name': 'Two'} + """ + results = [] + + if kwargs: + for item_id, item in self.items(): + item_matches = False + for match_attr, match_attr_vals in kwargs.items(): + if not type(match_attr_vals) == type([]): match_attr_vals = [match_attr_vals] + + # does the current item have the property + attr_to_check = None + if match_attr in dir(item): + attr_to_check = getattr(item, match_attr) + elif isinstance(item, dict) and match_attr in item: + attr_to_check = item[match_attr] + + if attr_to_check: + # does the property match the specified values? + for match_attr_val in match_attr_vals: + if type(attr_to_check) == type(''): + # string comparison + match = re.search(r'{}'.format(match_attr_val), attr_to_check) + if match: + item_matches = True + break # and move on to the new kwarg + else: + item_matches = False + elif type(attr_to_check) == type([]): + # check for the match in the list + if match_attr_val in attr_to_check: + item_matches = True + break # and move on to the new kwarg + else: + item_matches = False + else: + # object comparison + if attr_to_check == match_attr_val: + item_matches = True + break # and move on to the new kwarg + else: + item_matches = False + + if item_matches: results.append(item_id) + + return results + +class CoreObject(object): + def _set_properties(self, api_response, log_func): + """ + Convert the API keypairs to object properties + """ + for k, v in api_response.items(): + val = v + if isinstance(v, dict) and '@xsi:nil' in v and v['@xsi:nil'] == 'true': + val = None + + new_key = translation.Terms.get(k) + + # make sure any integer IDs are stored as an int + if new_key == 'id' and re.search('^\d+$', v.strip()): val = int(v) + if new_key == 'policy_id': + if '@xsi:nil' in "{}".format(v): + val = None + elif re.search('^\d+$', "".join(v.strip())): + val = int(v) + + try: + setattr(self, new_key, val) + except Exception as err: + if log_func: + log_func("Could not set property {} to value {} for object {}".format(k, v, s)) + try: + setattr(self, log, log_func) + except: pass + + def to_dict(self): + """ + Convert the object properties to API keypairs + """ + result = {} + + api_properties = translation.Terms.api_to_new.values() + + for p in dir(self): + if p in api_properties: + key = translation.Terms.get_reverse(p) + val = getattr(self, p) + result[key] = val + + return result + +class CoreList(list): + def __init__(self, *args): + super(CoreList, self).__init__(args) + self._exempt_from_find = [] + + def find(self, **kwargs): + """ + Find any items where the values match the cumulative kwargs patterns and return their indices + + If a keyword's value is a list, .find will match on any value for that keyword + + .find(id=1) + >>> returns any item with a property 'id' and value in [1] + possibilities: + { 'id': 1, 'name': 'One'} + { 'id': 1, 'name': 'Two'} + + .find(id=[1,2]) + >>> returns any item with a property 'id' and value in [1,2] + possibilities: + { 'id': 1, 'name': 'One'} + { 'id': 2, 'name': 'One'} + { 'id': 1, 'name': 'Two'} + { 'id': 2, 'name': 'Two'} + + .find(id=1, name='One') + >>> returns any item with a property 'id' and value in [1] AND a property 'name' and value in ['One'] + possibilities: + { 'id': 1, 'name': 'One'} + + .find(id=[1,2], name='One') + >>> returns any item with a property 'id' and value in [1,2] AND a property 'name' and value in ['One'] + possibilities: + { 'id': 1, 'name': 'One'} + { 'id': 2, 'name': 'One'} + + .find(id=[1,2], name=['One,Two']) + >>> returns any item with a property 'id' and value in [1,2] AND a property 'name' and value in ['One','Two'] + possibilities: + { 'id': 1, 'name': 'One'} + { 'id': 2, 'name': 'One'} + { 'id': 1, 'name': 'Two'} + { 'id': 2, 'name': 'Two'} + """ + results = [] + + if kwargs: + for item_id, item in enumerate(self): + item_matches = False + for match_attr, match_attr_vals in kwargs.items(): + if not type(match_attr_vals) == type([]): match_attr_vals = [match_attr_vals] + + # does the current item have the property + attr_to_check = None + if match_attr in dir(item): + attr_to_check = getattr(item, match_attr) + elif isinstance(item, dict) and match_attr in item: + attr_to_check = item[match_attr] + + if attr_to_check: + # does the property match the specified values? + for match_attr_val in match_attr_vals: + if type(attr_to_check) == type(''): + # string comparison + match = re.search(r'{}'.format(match_attr_val), attr_to_check) + if match: + item_matches = True + break # and move on to the new kwarg + else: + item_matches = False + elif type(attr_to_check) == type([]): + # check for the match in the list + if match_attr_val in attr_to_check: + item_matches = True + break # and move on to the new kwarg + else: + item_matches = False + else: + # object comparison + if attr_to_check == match_attr_val: + item_matches = True + break # and move on to the new kwarg + else: + item_matches = False + + if item_matches: results.append(item_id) + + return results \ No newline at end of file diff --git a/src/deepsecurity/credentials.py b/src/deepsecurity/credentials.py new file mode 100644 index 0000000..94913b4 --- /dev/null +++ b/src/deepsecurity/credentials.py @@ -0,0 +1,27 @@ +import boto3 + + +class Credentials(object): + + def __init__(self, + username_key=None, + password_key=None): + + self.username_key = username_key + self.password_key = password_key + + self.ssm = boto3.client('ssm') + + def get_username(self): + param_info = self.ssm.get_parameter( + Name=self.username_key, + WithDecryption=True + ) + return param_info['Parameter']['Value'] + + def get_password(self): + param_info = self.ssm.get_parameter( + Name=self.password_key, + WithDecryption=True + ) + return param_info['Parameter']['Value'] diff --git a/src/deepsecurity/dsm.py b/src/deepsecurity/dsm.py new file mode 100755 index 0000000..59512ab --- /dev/null +++ b/src/deepsecurity/dsm.py @@ -0,0 +1,437 @@ +# standard library +import datetime +import os +import re + +# 3rd party libraries + +# project libraries +import core +import computers +import environments +import policies +import translation + +class Manager(core.CoreApi): + def __init__(self, + hostname='app.deepsecurity.trendmicro.com', + port='4119', + tenant=None, + username=None, + password=None, + prefix="", + ignore_ssl_validation=False + ): + core.CoreApi.__init__(self) + self._hostname = None + self._port = port + self._tenant = None + self._username = None + self._password = None + self._prefix = prefix + self.ignore_ssl_validation = ignore_ssl_validation + self.hostname = hostname + + self._get_local_config_file() + + # allow for explicit override + if tenant: + self._tenant = str(tenant, "utf-8") if not type(tenant) == type(str("")) else tenant + if username: + self._username = str(username, "utf-8") if not type(username) == type(str("")) else username + if password: + self._password = str(password, "utf-8") if not type(password) == type(str("")) else password + + self.computer_groups = computers.ComputerGroups(manager=self) + self.computers = computers.Computers(manager=self) + self.policies = policies.Policies(manager=self) + self.rules = policies.Rules(manager=self) + self.ip_lists = policies.IPLists(manager=self) + self.cloud_accounts = environments.CloudAccounts(manager=self) + + def __del__(self): + """ + Try to gracefully clean up the session + """ + try: + self.sign_out() + except Exception as err: pass + + def __str__(self): + """ + Return a better string representation + """ + dsm_port = ":{}".format(self.port) if self.port else "" + return "Manager <{}{}>".format(self.hostname, dsm_port) + + # ******************************************************************* + # properties + # ******************************************************************* + @property + def hostname(self): return self._hostname + + @hostname.setter + def hostname(self, value): + if not value: + self.log(message="Null/invalid hostname provided for DSM. This is required for API usage. Defaulting to Deep Security as a Service", level='critical') + value = 'app.deepsecurity.trendmicro.com' + if value == 'app.deepsecurity.trendmicro.com': # Deep Security as a Service + self.port = 443 + self._hostname = value + self._set_endpoints() + + @property + def port(self): return self._port + + @port.setter + def port(self, value): + self._port = int(value) if value else None + self._set_endpoints() + + @property + def tenant(self): return self._tenant + + @tenant.setter + def tenant(self, value): + self._tenant = value + self._reset_session() + + @property + def username(self): return self._username + + @username.setter + def username(self, value): + self._username = value + self._reset_session() + + @property + def password(self): return self._password + + @password.setter + def password(self, value): + self._password = value + self._reset_session() + + @property + def prefix(self): return self._prefix + + @prefix.setter + def prefix(self, value): + if not value or type(value) != type(""): value = "" + self._prefix = value + + # ******************************************************************* + # methods + # ******************************************************************* + def _set_endpoints(self): + """ + Set the API endpoints based on the current configuration + """ + dsm_port = ":{}".format(self.port) if self.port else "" # allow for endpoints with no port specified + self._rest_api_endpoint = "https://{}{}/{}rest".format(self.hostname, dsm_port, self.prefix) + self._soap_api_endpoint = "https://{}{}/{}webservice/Manager".format(self.hostname, dsm_port, self.prefix) + + def _reset_session(self): + """ + Reset the current session due to a credentials change + """ + self.sign_out() + self.sign_in() + + def _get_local_config_file(self): + """ + Look for a local config file containing the credentials similar to the AWS CLI + + Path checked is ( via os.path.expanduser(path) ): + ~/.deepsecurity/credentials + C:\\Users\\USERNAME\.deepsecurity\credentials + + !!! Remember that by storing credentials on the local disk you are increasing the + risk of compromise as you've expanded the attack surface. If an attacker gains + access to your local machine then can now get the credentials to your Deep Security + installation and compromise the security of other systems. + + Use the role-based access control in Deep Security to ensure that you reduce the + permissions assigned to the account your using to automate the system + """ + user_credentials_path = os.path.expanduser('~/.deepsecurity/credentials') + if os.path.exists(user_credentials_path): + self.log("Found local credentials file at [{}]".format(user_credentials_path)) + credentials = { + 'username': None, + 'password': None, + 'tenant': None, + } + try: + credential_line_pattern = re.compile(r'(?P\w+) = (?P[^\n]+)') + with open(user_credentials_path, 'r') as fh: + for line in fh: + m = credential_line_pattern.search(line) + if m: + if m.group('key') not in credentials: credentials[m.group('key')] = None + credentials[m.group('key')] = m.group('val') + except Exception as err: + self.log("Could not read and process local credentials file.", err=err) + + # verify credentials + for k, v in credentials.items(): + if v: + if k in dir(self): + try: + setattr(self, "_{}".format(k), v) + self.log("Loaded {} from local credentials file".format(k)) + except Exception as err: + self.log("Unable to load {} from local credentials file".format(k)) + + def sign_in(self): + """ + Sign in to the Deep Security APIs + """ + # first the SOAP API + soap_call = self._get_request_format() + soap_call['data'] = { + 'username': self.username, + 'password': self.password, + } + if self.tenant: + soap_call['call'] = 'authenticateTenant' + soap_call['data']['tenantName'] = self.tenant + else: + soap_call['call'] = 'authenticate' + + response = self._request(soap_call, auth_required=False) + if response and response['data']: self._sessions[self.API_TYPE_SOAP] = response['data'] + + # then the REST API + rest_call = self._get_request_format(api=self.API_TYPE_REST) + rest_call['data'] = { + 'dsCredentials': + { + 'userName': self.username, + 'password': self.password, + } + } + if self.tenant: + rest_call['call'] = 'authentication/login' + rest_call['data']['dsCredentials']['tenantName'] = self.tenant + else: + rest_call['call'] = 'authentication/login/primary' + + response = self._request(rest_call, auth_required=False) + if response and response['raw']: self._sessions[self.API_TYPE_REST] = response['raw'] + + if self._sessions[self.API_TYPE_REST] and self._sessions[self.API_TYPE_SOAP]: + return True + else: + return False + + def sign_out(self): + """ + Sign out to the Deep Security APIs + """ + # first the SOAP API + soap_call = self._get_request_format(call='endSession') + if self._sessions[self.API_TYPE_SOAP]: + response = self._request(soap_call) + if response and response['status'] == 200: self._sessions[self.API_TYPE_SOAP] = None + + # then the REST API + rest_call = self._get_request_format(api=self.API_TYPE_REST, call='authentication/logout') + if self._sessions[self.API_TYPE_REST]: + response = self._request(rest_call) + if response and response['status'] == 200: self._sessions[self.API_TYPE_REST] = None + + if self._sessions[self.API_TYPE_REST] or self._sessions[self.API_TYPE_SOAP]: + return False + else: + return True + + def get_api_version(self): + """ + Get the version of the REST and SOAP APIs current running on the Manager + """ + versions = { + self.API_TYPE_REST: None, + self.API_TYPE_SOAP: None, + } + + # first the SOAP API + soap_call = self._get_request_format(call='getApiVersion') + response = self._request(soap_call, auth_required=False) + if response and response['status'] == 200 and response['data']: + versions[self.API_TYPE_SOAP] = response['data'] + + # then the REST API + rest_call = self._get_request_format(api=self.API_TYPE_REST, call='apiVersion') + response = self._request(rest_call, auth_required=False) + if response and response['status'] == 200 and response['data']: + versions[self.API_TYPE_REST] = response['data'] + + return versions + + def get_time(self): + """ + Get the current time as set on the Manager + """ + result = None + soap_call = self._get_request_format(call='getManagerTime') + response = self._request(soap_call, auth_required=False) + if response and response['status'] == 200 and '#text' in response['data']: + result = datetime.datetime.strptime(response['data']['#text'], "%Y-%m-%dT%H:%M:%S.%fZ") + + return result + + def is_up(self): + """ + Check to see if the Manager is up and responding to requests + """ + result = None + rest_call = self._get_request_format(api=self.API_TYPE_REST, call='status/manager/ping') + response = self._request(rest_call, auth_required=False) + if response and response['status'] == 200: + result = True + else: + result = False + + return result + + # ******************************************************************* + # mirrored on the computers.Computer and computers.ComputerGroup + # objects + # ******************************************************************* + def request_events_from_computer(self, computer_id): + """ + Ask the computer to send the latest events it's seen to the DSM + """ + result = False + + soap_call = self._get_request_format(call='hostGetEventsNow') + soap_call['data'] = { + 'hostID': computer_id + } + response = self._request(soap_call) + if response and response['status'] == 200: result = True + + return result + + def clear_alerts_and_warnings_from_computers(self, computer_ids): + """ + Clear any alerts or warnings for the specified computers + """ + result = False + + if not type(computer_ids) == type([]): computer_ids = [computer_ids] + + soap_call = self._get_request_format(call='hostClearWarningsErrors') + soap_call['data'] = { + 'hostIDs': computer_ids + } + response = self._request(soap_call) + if response and response['status'] == 200: result = True + + return result + + def scan_computers_for_malware(self, computer_ids): + """ + Request a malware scan be run on the specified computers + """ + result = False + + if not type(computer_ids) == type([]): computer_ids = [computer_ids] + + soap_call = self._get_request_format(call='hostAntiMalwareScan') + soap_call['data'] = { + 'hostIDs': computer_ids + } + response = self._request(soap_call) + if response and response['status'] == 200: result = True + + return result + + def scan_computers_for_integrity(self, computer_ids): + """ + Request an integrity scan be run on the specified computers + """ + result = False + + if not type(computer_ids) == type([]): computer_ids = [computer_ids] + + soap_call = self._get_request_format(call='hostIntegrityScan') + soap_call['data'] = { + 'hostIDs': computer_ids + } + response = self._request(soap_call) + if response and response['status'] == 200: result = True + + return result + + def scan_computers_for_recommendations(self, computer_ids): + """ + Request a recommendation scan be run on the specified computers + """ + result = False + + if not type(computer_ids) == type([]): computer_ids = [computer_ids] + + soap_call = self._get_request_format(call='hostRecommendationScan') + soap_call['data'] = { + 'hostIDs': computer_ids + } + response = self._request(soap_call) + if response and response['status'] == 200: result = True + + return result + + def assign_policy_to_computers(self, policy_id, computer_ids): + """ + Assign the specified policy to the specified computers + """ + result = False + + if not type(computer_ids) == type([]): computer_ids = [computer_ids] + + soap_call = self._get_request_format(call='securityProfileAssignToHost') + soap_call['data'] = { + 'hostIDs': computer_ids, + 'securityProfileID': policy_id, + } + response = self._request(soap_call) + if response and response['status'] == 200: result = True + + return result + + def get_rule_recommendations_for_computer(self, computer_id): + """ + Get the recommended rule set (applied or not) for the specified computer + """ + results = { + 'total_recommedations': 0 + } + + rules_types = { # values align with rule type ENUM + 'DPIRuleRetrieveAll': 2, + 'firewallRuleRetrieveAll': 3, + 'integrityRuleRetrieveAll': 4, + 'logInspectionRuleRetrieveAll': 5, + 'applicationTypeRetrieveAll': 1, + } + + for rule_type, type_enum_val in rules_types.items(): + rule_key = translation.Terms.get(rule_type).replace('_retrieve_all', '').replace('_rule', '') + results[rule_key] = [] + + soap_call = self._get_request_format(call='hostRecommendationRuleIDsRetrieve') + soap_call['data'] = { + 'hostID': computer_id, + 'type': type_enum_val, + 'onlyunassigned': False, + } + response = self._request(soap_call) + if response and response['status'] == 200: + # response contains the internal rule ID + for internal_rule_id in response['data']: + if internal_rule_id == '@xmlns': continue + results[rule_key].append(internal_rule_id) + results['total_recommedations'] += 1 + + return results \ No newline at end of file diff --git a/src/deepsecurity/environments.py b/src/deepsecurity/environments.py new file mode 100755 index 0000000..bb62a0f --- /dev/null +++ b/src/deepsecurity/environments.py @@ -0,0 +1,81 @@ +# standard library +import datetime + +# 3rd party libraries + +# project libraries +import core + +class CloudAccounts(core.CoreDict): + def __init__(self, manager=None): + core.CoreDict.__init__(self) + self.manager = manager + self.log = self.manager.log if self.manager else None + + def get(self): + """ + Get a list of all of the current configured cloud accounts + """ + call = self.manager._get_request_format(api=self.manager.API_TYPE_REST, call='cloudaccounts') + response = self.manager._request(call) + if response and response['status'] == 200: + if response['data'] and 'cloudAccountListing' in response['data'] and 'cloudAccounts' in response['data']['cloudAccountListing']: + for cloud_account in response['data']['cloudAccountListing']['cloudAccounts']: + cloud_account_obj = CloudAccount(self.manager, cloud_account, self.log) + self[cloud_account_obj.cloud_account_id] = cloud_account_obj + + def add_aws_account(self, name, aws_access_key=None, aws_secret_key=None, region="all"): + """ + Add an AWS Cloud account to Deep Security + """ + responses = {} + + regions = { + 'us-east-1': 'amazon.cloud.region.key.1', # N. Virginia + 'us-west-1': 'amazon.cloud.region.key.2', # N. California + 'us-west-2': 'amazon.cloud.region.key.3', # Oregon + 'eu-west-1': 'amazon.cloud.region.key.4', # Ireland + 'ap-southeast-1': 'amazon.cloud.region.key.5', # Singapore + 'ap-northeast-1': 'amazon.cloud.region.key.6', # Tokyo + 'sa-east-1': 'amazon.cloud.region.key.7', # Sao Paulo + 'ap-southeast-2': 'amazon.cloud.region.key.8', # Sydney + # need to add: + # ap-south-1 / Mumbai + # ap-northeast-2 / Seoul + # eu-central-1 / Frankfurt + } + + regions_to_add = [] + if region in regions: + regions_to_add.append(region) + elif region == 'all': + regions_to_add = regions.keys() + else: + self.log("A region must be specified when add an AWS account to Deep Security") + + for region_to_add in regions_to_add: + call = self.manager._get_request_format(api=self.manager.API_TYPE_REST, call='cloudaccounts') + call['data'] = { + 'createCloudAccountRequest': { + 'sessionId': self.manager._sessions[self.manager.API_TYPE_REST], + 'cloudAccountElement': { + 'accessKey': aws_access_key, + 'secretKey': aws_secret_key, + 'cloudType': 'AMAZON', + 'name': '{} / {}'.format(name, region_to_add), + 'cloudRegion': regions[region_to_add], + }, + } + } + + responses[region_to_add] = self.manager._request(call) + if not responses[region_to_add] and responses[region_to_add]['status'] == 200: + if responses[region_to_add]['raw'] and 'Cloud Account Region/Partition already present' in responses[region_to_add]['raw']: + self.log("The account/region you request has already been added to Deep Security. A specific account/region combination can only be added once") + + return responses + +class CloudAccount(core.CoreObject): + def __init__(self, manager=None, api_response=None, log_func=None): + self.manager = manager + if api_response: self._set_properties(api_response, log_func) \ No newline at end of file diff --git a/src/deepsecurity/policies.py b/src/deepsecurity/policies.py new file mode 100755 index 0000000..6ab7350 --- /dev/null +++ b/src/deepsecurity/policies.py @@ -0,0 +1,309 @@ +# standard library +import datetime + +# 3rd party libraries + +# project libraries +import core +import translation + +class Policies(core.CoreDict): + def __init__(self, manager=None): + core.CoreDict.__init__(self) + self.manager = manager + self.log = self.manager.log if self.manager else None + + def get(self): + """ + Get all of the policies from Deep Security + """ + call = self.manager._get_request_format(call='securityProfileRetrieveAll') + response = self.manager._request(call) + + if response and response['status'] == 200: + if not type(response['data']) == type([]): response['data'] = [response['data']] + for policy in response['data']: + policy_obj = Policy(manager=self.manager, api_response=policy, log_func=self.log) + if policy_obj: + try: + self[policy_obj.id] = policy_obj + self.log("Added Policy {}".format(policy_obj.id), level='debug') + except Exception as err: + self.log("Could not add Policy {}".format(policy_obj), level='warning', err=err) + + return len(self) + + def create(self, name, parent_profile_id=None, + enable_anti_malware=True, + enable_firewall=False, + enable_intrusion_prevention=True, + enable_integrity_monitoring=True, + enable_log_inspection=True, + description=None + ): + """ + Create a new policy + + name + - the name of the new policy + + parent_profile_id + - the ID of the parent policy + + enable_anti_malware + - if True, enable the anti-malware module + - if 'parent_profile_id' is set, the new policy will + inherit this value from the parent + + enable_firewall + - if True, enable the firewall module + - if 'parent_profile_id' is set, the new policy will + inherit this value from the parent + + enable_intrusion_prevention + - if True, enable the intrusion prevention module + - if 'parent_profile_id' is set, the new policy will + inherit this value from the parent + + enable_integrity_monitoring + - if True, enable the integrity monitoring module + - if 'parent_profile_id' is set, the new policy will + inherit this value from the parent + + enable_log_inspection + - if True, enable the log inspection module + - if 'parent_profile_id' is set, the new policy will + inherit this value from the parent + + description + - the description of the new policy + + Returns the ID of the new policy is successful. False if not successful in + creating the new policy + """ + result = None + + # set the state for each supported module + anti_malware_state = 'ON' if enable_anti_malware else 'OFF' + firewall_state = 'ON' if enable_firewall else 'OFF' + intrusion_prevention_state = 'ON' if enable_intrusion_prevention else 'OFF' + integrity_monitoring_state = 'ON' if enable_integrity_monitoring else 'OFF' + log_inspection_state = 'ON' if enable_log_inspection else 'OFF' + + # inherit all states if a parent policy is specified + if parent_profile_id: + anti_malware_state = 'INHERITED' + firewall_state = 'INHERITED' + intrusion_prevention_state = 'INHERITED' + integrity_monitoring_state = 'INHERITED' + log_inspection_state = 'INHERITED' + + call = self.manager._get_request_format(call='securityProfileSave') + call['data'] = { 'sp': { + 'DPIRuleIDs': None, + 'DPIState': intrusion_prevention_state, + 'ID': None, + 'antiMalwareManualID': None, + 'antiMalwareManualInherit': 'true', + 'antiMalwareRealTimeID': None, + 'antiMalwareRealTimeInherit': 'true', + 'antiMalwareRealTimeScheduleID': None, + 'antiMalwareScheduledID': None, + 'antiMalwareScheduledInherit': 'true', + 'antiMalwareState': anti_malware_state, + 'applicationTypeIDs': None, + 'description': description, + 'firewallRuleIDs': None, + 'firewallState': firewall_state, + 'integrityRuleIDs': None, + 'integrityState': integrity_monitoring_state, + 'logInspectionRuleIDs': None, + 'logInspectionState': log_inspection_state, + 'name': name, + 'parentSecurityProfileID': parent_profile_id if parent_profile_id else None, + 'recommendationState': None, + 'scheduleID': None, + 'statefulConfigurationID': None + } + } + + response = self.manager._request(call) + if response and response['status'] == 200: + try: + new_policy = Policy(api_response=response['data'], manager=self.manager, log_func=self.log) + if new_policy: + self[new_policy.id] = new_policy + result = new_policy.id + self.log("Added new policy #{}".format(new_policy.id)) + except Exception as err: + self.log("Could not create new policy from API response", err=err) + else: + result = False + + return result + +class Rules(core.CoreDict): + def __init__(self, manager=None): + core.CoreDict.__init__(self) + self.manager = manager + self.log = self.manager.log if self.manager else None + + def get(self, intrusion_prevention=True, firewall=True, integrity_monitoring=True, log_inspection=True, web_reputation=True, application_types=True): + """ + Get all of the rules from Deep Security + """ + # determine which rules to get from the Manager() + rules_to_get = { + 'DPIRuleRetrieveAll': intrusion_prevention, + 'firewallRuleRetrieveAll': firewall, + 'integrityRuleRetrieveAll': integrity_monitoring, + 'logInspectionRuleRetrieveAll': log_inspection, + 'applicationTypeRetrieveAll': application_types, + } + + for call, get in rules_to_get.items(): + rule_key = translation.Terms.get(call).replace('_retrieve_all', '').replace('_rule', '') + self[rule_key] = core.CoreDict() + + if get: + soap_call = self.manager._get_request_format(call=call) + if call == 'DPIRuleRetrieveAll': + self.log("Calling {}. This may take 15-30 seconds as the call returns a substantial amount of data".format(call), level='warning') + + response = self.manager._request(soap_call) + if response and response['status'] == 200: + if not type(response['data']) == type([]): response['data'] = [response['data']] + for i, rule in enumerate(response['data']): + rule_obj = Rule(self.manager, rule, self.log, rule_type=rule_key) + if rule_obj: + if rule_key == 'intrusion_prevention' and rule_obj.cve_numbers: + rule_obj.cve_numbers = rule_obj.cve_numbers.split(', ') + if type(rule_obj.cve_numbers) == type(''): rule_obj.cve_numbers = [ rule_obj.cve_numbers ] + + rule_id = '{}-{: >10}'.format(rule_key, i) + if 'id' in dir(rule_obj): rule_id = rule_obj.id + elif 'tbuid' in dir(rule_obj): rule_id = rule_obj.tbuid + self[rule_key][rule_id] = rule_obj + self.log("Added Rule {} from call {}".format(rule_id, call), level='debug') + + return len(self) + +class IPLists(core.CoreDict): + def __init__(self, manager=None): + core.CoreDict.__init__(self) + self.manager = manager + self.log = self.manager.log if self.manager else None + + def get(self): + """ + Get all of the IP Lists from Deep Security + """ + soap_call = self.manager._get_request_format(call='IPListRetrieveAll') + response = self.manager._request(soap_call) + if response and response['status'] == 200: + for ip_list in response['data']: + ip_list_obj = IPList(self.manager, ip_list, self.log) + self[ip_list_obj.id] = ip_list_obj + + return len(self) + +class Policy(core.CoreObject): + def __init__(self, manager=None, api_response=None, log_func=None): + self.manager = manager + self.computers = core.CoreDict() + self.rules = core.CoreDict() + if api_response: self._set_properties(api_response, log_func) + #self._flatten_rules() + + def _flatten_rules(self): + """ + Flatten the various module rules into a master list + """ + for rule_type in [ + 'intrusion_prevention_rule_ids', + 'firewall_rule_ids', + 'integrity_monitoring_rule_ids', + 'log_inspection_rule_ids', + ]: + rules = getattr(self, rule_type) + if rules: + for rule in rules['item']: + self.rules['{}-{}'.format(rule_type.replace('rule_ids', ''), rule)] = None + + def save(self): + """ + Save any changes made to the policy + """ + result = False + + soap_call = self.manager._get_request_format(call='securityProfileSave') + soap_call['data'] = { 'sp': self.to_dict() } + + if 'manager' in soap_call['data']['sp']: + del(soap_call['data']['sp']['manager']) + + response = self.manager._request(soap_call) + if response['status'] == 200: + result = True + else: + result = False + if 'log' in dir(self): + self.log("Could not save the policy. Returned: {}".format(response), level='error') + + return result + + def get_application_control_settings(self): + """ + Get the details for the application control settings for this policy + """ + return self.manager.application_control.get_policy_settings(self.id) + + def set_application_control_settings(self, policy_id, lockdown=None, ruleset_id=None, state=None, whitelist_mode=None): + """ + Set the details for the application control settings for this policy + + lockdown: + - if set to None, no changes are made + - if set to True, lockdown mode is enabled and anything that's not on the whitelist will be blocked + - if set to False, lockdown mode is disabled and only things on the blacklist will be blocked + + ruleset_id: + - if set to None, no changes are made + - the ID of the ruleset to use for this application control policy + + state: + - if set to None, no changes are made + - if set to "on", application control is turned on for this policy + - if set to "off", application control is turned off for this policy + - if set to "inherit", the application control state inherited from this policy's parent (if one exists) + + whitelist_mode: + - if set to None, no changes are made + - if set to "local-inventory", application control is turned on for this policy + - if set to "shared", application control is turned off for this policy + - if set to "inherit", the application control state inherited from this policy's parent (if one exists) + """ + return self.manager.application_control.set_policy_settings(self.id, lockdown=lockdown, ruleset_id=ruleset_id, state=state, whitelist_mode=whitelist_mode) + +class Rule(core.CoreObject): + def __init__(self, manager=None, api_response=None, log_func=None, rule_type=None): + self.manager = manager + self.rule_type = rule_type + self.policies = core.CoreDict() + if api_response: self._set_properties(api_response, log_func) + +class IPList(core.CoreObject): + def __init__(self, manager=None, api_response=None, log_func=None): + self.manager = manager + self.addresses = [] + if api_response: self._set_properties(api_response, log_func) + self._split_items() + + def _split_items(self): + """ + Split the individual items in an IP List into entries + """ + if getattr(self, 'items') and "\n" in self.items: + self.addresses = self.items.split('\n') + else: + self.addresses.append(self.items.strip()) \ No newline at end of file diff --git a/src/deepsecurity/terms.txt b/src/deepsecurity/terms.txt new file mode 100755 index 0000000..e3f58c4 --- /dev/null +++ b/src/deepsecurity/terms.txt @@ -0,0 +1,437 @@ +ID +ackStormDropConnection +ackStormProtection +ackStormProtectionThreshold +action +actionPerformedBy +alertDate +alertErrorNum +alertMinSeverity +alertType +alertWarningNum +allowIncomingActiveFTP +allowIncomingPassiveFTP +allowOnChange +allowOutgoingActiveFTP +allowOutgoingPassiveFTP +antiMalwareClassicPatternVersion +antiMalwareConfigID +antiMalwareEngineVersion +antiMalwareEventID +antiMalwareEvents +antiMalwareIntelliTrapExceptionVersion +antiMalwareIntelliTrapVersion +antiMalwareManualID +antiMalwareManualInherit +antiMalwareQuarantinedFileID +antiMalwareRealTimeID +antiMalwareRealTimeInherit +antiMalwareRealTimeScheduleID +antiMalwareScheduledID +antiMalwareScheduledInherit +antiMalwareSmartScanPatternVersion +antiMalwareSpywareItemID +antiMalwareSpywarePatternVersion +antiMalwareState +anyFlags +applianceID +applianceName +ApplicationTypeID +applicationTypeID +applicationTypeIDs +applicationTypesAdded +applicationTypesDeleted +applicationTypesUpdated +appliedState +attributes +authoritative +averageTimeOpen +change +cloudObjectImageId +cloudObjectInstanceId +cloudObjectInternalUniqueId +cloudObjectSecurityGroupIds +cloudObjectType +command +complete +componentInfoTransports +componentKlasses +componentNames +componentTypes +componentVersions +configurationType +content +content +contentSummary +country +cpuUsage +criticalHosts +currentVersion +cveNumbers +cvssScore +data +dataFlags +dataIndex +denyFragmentedPackets +denyTcpCwrEceFlags +deployed +description +destinationIP +destinationIPListID +destinationIPMask +destinationIPNot +destinationIPRangeFrom +destinationIPRangeTo +destinationIPType +destinationMAC +destinationMACListID +destinationMACNot +destinationMACType +destinationPort +destinationPortListID +destinationPortNot +destinationPorts +destinationPortType +destinationSingleIP +destinationUser +detailedSummary +detectedEventNum +detectOnly +dhcp +direction +disabledLog +disableEvent +displayName +downloaded +DPIEventID +DPIEvents +DPIRuleID +DPIRuleIDs +DPIRulesAdded +DPIRulesAddedAndAssigned +DPIRulesDeleted +DPIRulesUpdated +DPIState +dpiStatus +driverTime +emailAddress +enableICMPStatefulInspection +enableICMPStatefulLogging +enableTCPStatefulInspection +enableTCPStatefulLogging +enableUDPStatefulInspection +enableUDPStatefulLogging +endTime +error +errorCode +esxServerFastPathDriverVersion +esxServerID +esxServerName +esxServerVersion +eventID +eventOnPacketDrop +eventOnPacketModify +eventOrigin +excludeScanDirectoryListID +excludeScanFileExtListID +excludeScanFileListID +excludeScanProcessFileListID +external +externalID +featureName +files +fileToScan +fingerprint +firewallEventID +firewallRuleIDs +firewallState +firewallStatus +firstScanAction +flags +flow +folderToScan +frameNot +frameNumber +frameType +friendlyValue +fullEvent +fullName +groups +hostBridgeId +hostExternalID +hostGroupExternalID +hostGroupID +hostGroupName +hostID +hostLight +hostName +hostStatusSummary +hostType +hourOfWeek +icmpCode +icmpNot +icmpType +icon +identifier +iface +ignoreRecommendations +imported +includePacketData +infectedFilePath +infectionSource +integrityEventID +integrityEvents +integrityMonitoringRulesAdded +integrityMonitoringRulesDeleted +integrityMonitoringRulesUpdated +integrityMonitoringStatus +integrityRuleID +integrityRuleIDs +integrityState +intelliTrapEnabled +interfaceTypeId +isEntity +issued +itemID +items +key +language +lastAnitMalwareScheduledScan +lastAntiMalwareEvent +lastAntiMalwareManualScan +lastAntiMalwareScheduledScan +lastDpiEvent +lastFirewallEvent +lastIntegrityMonitoringEvent +lastIPUsed +lastLogInspectionEvent +lastSuccessfulCommunication +lastSuccessfulUpdate +lastUpdate +lastWebReputationEvent +light +limitHalfOpenConnections +limitHalfOpenConnectionsTo +limitIncomingConnections +limitIncomingConnectionsTo +limitOutgoingConnections +limitOutgoingConnectionsTo +location +locked +lockedHosts +lockedOut +logDate +logInspectionDecodersAdded +logInspectionDecodersDeleted +logInspectionDecodersUpdated +logInspectionEventID +logInspectionEvents +logInspectionRuleID +logInspectionRuleIDs +logInspectionRulesAdded +logInspectionRulesDeleted +logInspectionRulesUpdated +logInspectionState +logInspectionStatus +logTime +mac +malwareName +malwareType +managerHostname +message +minAgentVersion +minManagerVersion +mobileNumber +msNumbers +name +nameKey +needDeployed +notAvailable +note +notes +objectInfo +objectType +onlineHosts +operator +overallAntiMalwareStatus +overallDpiStatus +overallFirewallStatus +overallIntegrityMonitoringStatus +overallLastRecommendationScan +overallLastSuccessfulCommunication +overallLastSuccessfulUpdate +overallLastUpdateRequired +overallLogInspectionStatus +overallStatus +overallStatus +overallVersion +overallWebReputationStatus +packetDirection +packetSize +pagerNumber +parentGroupID +parentSecurityProfileID +password +passwordNeverExpires +patternAction +patternCaseSensitive +patternEnd +patternIf +patternPatterns +patternStart +pending +percentOfTotal +percentOfTotalString +percentOpen +phoneNumber +platform +portListID +portListsAdded +portListsUpdated +ports +portType +preventedEventNum +previousDetectedEventNum +previousPreventedEventNum +previousTotalEventNum +previousValue +priority +process +programName +protectedComputerNum +protectionStatusTransports +protectionType +protocol +protocolIcmp +protocolNot +protocolNumber +protocolPortBased +protocolType +protocolType +quarantineRecordID +raiseAlert +rangeFrom +rangeTo +rank +reason +receiveNotifications +recommendationState +released +repeatCount +risk +riskLevel +ruleID +ruleXML +scanAction +scanAction1 +scanAction2 +scanActionForCookie +scanActionForOtherThreats +scanActionForPacker +scanActionForSpyware +scanActionForTrojans +scanActionForVirus +scanCompressed +scanCompressedLayer +scanCompressedNumberOfFiles +scanCompressedSmaller +scanCustomActionForGeneric +scanDirList +scanFilesActivity +scanNetworkFolder +scanOLE +scanOLEExploit +scanOLELayer +scanResultAction +scanResultAction1 +scanResultAction2 +scanType +scheduleID +secondScanAction +SecurityProfileID +securityProfileName +settingKey +settingScope +settingUnit +settingValue +severity +severityText +shortName +signatureAction +signatureCaseSensitive +signatureSignature +sourceHostName +sourceID +sourceIP +sourceIPListID +sourceIPMask +sourceIPNot +sourceIPRangeFrom +sourceIPRangeTo +sourceIPType +sourceMAC +sourceMACListID +sourceMACNot +sourceMACType +sourcePort +sourcePortListID +sourcePortNot +sourcePorts +sourcePortType +sourceSingleIP +sourceUser +specificTime +spywareEnabled +spywareItems +spywareType +startTime +state +stateDescription +statefulConfigurationID +status +summaryScanResult +synFloodProtection +synFloodProtectionThreshold +systemEventID +systemEvents +systemName +tags +target +targetID +targetType +TBUID +tcpFlagACK +tcpFlagFIN +tcpFlagPSH +tcpFlagRST +tcpFlagSYN +tcpFlagURG +tcpNot +templateType +text +time +toScanFileExtListID +totalEventNum +truncated +type +unable +unmanageHosts +unScannableFileAction +url +user +value +valueString +version +virtualDeviceKey +virtualName +virtualUuid +warningHosts +wasEntity +webReputationEventID +webReputationEvents +webReputationStatus +DPIRuleRetrieveAll +firewallRuleRetrieveAll +integrityRuleRetrieveAll +logInspectionRuleRetrieveAll +applicationTypeRetrieveAll \ No newline at end of file diff --git a/src/deepsecurity/translation.py b/src/deepsecurity/translation.py new file mode 100755 index 0000000..dc8d245 --- /dev/null +++ b/src/deepsecurity/translation.py @@ -0,0 +1,62 @@ +# standard library +import os +import re + +# 3rd party libraries + +# project libraries + +class Terms(object): + # these dict are initialized in the module's __init__.py + api_to_new = {} + new_to_api = {} + + @classmethod + def read_terms_file(self): + current_directory_path = os.path.dirname(os.path.realpath(__file__)) + terms_file_path = os.path.join(current_directory_path, "terms.txt") + + if os.path.exists(terms_file_path): + # read each API term and setup the necessary lookups + with open(terms_file_path, 'r') as fh: + for line in fh: + term = line.strip() + term_lower = term.lower() + term_new = re.sub('([A-Z]+)', r'_\1', term).lower().lstrip('_') + + # catch the term_new exceptions + if 'dpi' in term_new: + term_new = re.sub('dpi', '_intrusion_prevention_', term_new, re.IGNORECASE) + if 'host' in term_new: + term_new = re.sub('host', '_computer_', term_new, re.IGNORECASE) + if 'security_profile' in term_new: + term_new = re.sub('security_profile', '_policy_', term_new, re.IGNORECASE) + if 'integrity' in term_new: + term_new = re.sub('((?=integrity(?!_monitoring)))', '_integrity_monitoring_', term_new, re.IGNORECASE) + term_new = term_new.replace('integrity_monitoring_integrity', 'integrity_monitoring') + + term_new = term_new.replace('__', '_').strip('_') + + self.api_to_new[term_lower] = term_new + self.new_to_api[term_new] = term + + @classmethod + def get_reverse(self, term_new): + """ + For the given new term, return the original API term + """ + result = term_new + if term_new in self.new_to_api: + result = self.new_to_api[term_new] + + return result + + @classmethod + def get(self, term): + """ + Return the translation of the specified API term + """ + if term.lower() in self.api_to_new: + return self.api_to_new[term.lower()] + else: + return term \ No newline at end of file diff --git a/src/rules/__init__.py b/src/rules/__init__.py new file mode 100644 index 0000000..020b0a1 --- /dev/null +++ b/src/rules/__init__.py @@ -0,0 +1,8 @@ +CONTROL_NAMES = { + 'anti_malware': 'Anti-Malware', + 'web_reputation': 'Web Reputation', + 'firewall': 'Firewall', + 'intrusion_prevention': 'Intrusion Prevention', + 'integrity_monitoring': 'Integrity Monitoring', + 'log_inspection': 'Log Inspection', +} diff --git a/src/rules/rule.py b/src/rules/rule.py new file mode 100644 index 0000000..8ac84a0 --- /dev/null +++ b/src/rules/rule.py @@ -0,0 +1,132 @@ +import logging +import datetime +import distutils.util +import json +import boto3 +from src.deepsecurity.credentials import Credentials +from src.deepsecurity.dsm import Manager + + +class Rule(object): + def __init__(self, event): + try: + self._rule_parameters = json.loads(event['ruleParameters']) + self._invoking_event = json.loads(event['invokingEvent']) + self._result_token = event['resultToken'] + self._event_left_scope = event['eventLeftScope'] + except Exception: + raise ValueError('Missing a required AWS Config Rules key in the event object. \ + Need [invokingEvent, ruleParameters, resultToken, eventLeftScope]') + + self._initialize() + + def _initialize(self): + self._username_key = self._rule_parameters.get('dsUsernameKey') + if not self._username_key: + raise ValueError('Missing parameter: dsUsernameKey') + + self._password_key = self._rule_parameters.get('dsPasswordKey') + if not self._password_key: + raise ValueError('Missing parameter: dsPasswordKey') + + self._hostname = self._rule_parameters.get('dsHostname') + if not self._hostname: + raise ValueError('Missing parameter: dsHostname') + + self._tenant = self._rule_parameters.get('dsTenant') + self._port = self._rule_parameters.get('dsPort', 443) + self._ignore_ssl_validation = distutils.util.strtobool( + self._rule_parameters.get('dsIgnoreSslValidation', 'False') + ) + + config_item = self._invoking_event.get('configurationItem', {}) + self._resource_type = config_item.get('resourceType') + if not self._resource_type or self._resource_type != 'AWS::EC2::Instance': + raise ValueError('Event is not targeted towards a resourceType of AWS::EC2::Instance') + self._instance_id = config_item.get('resourceId') + logging.info('Target instance [{}]'.format(self._instance_id)) + + self._compliance = 'NON_COMPLIANT' + self._compliance_msg = 'Details missing' + + @property + def username_key(self): + return self._username_key + + @property + def password_key(self): + return self._password_key + + @property + def tenant(self): + return self._tenant + + @property + def hostname(self): + return self._hostname + + @property + def port(self): + return self._port + + @property + def ignore_ssl_validation(self): + return self._ignore_ssl_validation + + @property + def instance_id(self): + return self._instance_id + + @property + def compliance(self): + return self._compliance + + @property + def compliance_msg(self): + return self._compliance_msg + + def execute(self): + user_pass = self._get_credentials() + mgr = Manager(username=user_pass[0], password=user_pass[1], tenant=self._tenant, + hostname=self._hostname, port=self._port, ignore_ssl_validation=self._ignore_ssl_validation) + mgr.sign_in() + logging.info('Successfully authenticated to Deep Security') + + mgr.computers.get() + logging.info('Searching {} computers for event source'.format(len(mgr.computers))) + + for comp_id, details in mgr.computers.items(): + if details.cloud_object_instance_id == self._instance_id: + logging.info('Found matching computer. Deep Security #{}'.format(comp_id)) + self._do_check(details) + logging.info(self._compliance_msg) + + mgr.sign_out() + + return self._respond_to_config() + + def _get_credentials(self): + creds = Credentials(self._username_key, self._password_key) + return creds.get_username(), creds.get_password() + + def _do_check(self, details): + pass + + def _respond_to_config(self): + logging.info('Sending results back to AWS Config') + logging.info('resourceId: {} is {}'.format(self._instance_id, self._compliance)) + + evaluation = { + 'ComplianceResourceType': self._resource_type, + 'ComplianceResourceId': self._instance_id, + 'ComplianceType': self._compliance, + 'OrderingTimestamp': datetime.datetime.now(), + 'Annotation': self._compliance_msg + } + + config_client = boto3.client('config') + response = config_client.put_evaluations( + Evaluations=[evaluation], + ResultToken=self._result_token + ) + return response diff --git a/src/rules/rule_does_instance_have_policy.py b/src/rules/rule_does_instance_have_policy.py new file mode 100644 index 0000000..78d72ec --- /dev/null +++ b/src/rules/rule_does_instance_have_policy.py @@ -0,0 +1,18 @@ +from src.rules.rule import Rule + + +class RuleDoesInstanceHavePolicy(Rule): + def _initialize(self): + super()._initialize() + + self._policy = self._rule_parameters.get('dsPolicy') + if not self._policy: + raise ValueError('Missing parameter: dsPolicy') + + @property + def policy(self): + return self._policy + + def _do_check(self, details): + self._compliance = 'COMPLIANT' if details.policy_name.lower() == self._policy.lower() else 'NON_COMPLIANT' + self._compliance_msg = 'Current policy: {}'.format(details.policy_name) diff --git a/src/rules/rule_is_instance_clear.py b/src/rules/rule_is_instance_clear.py new file mode 100644 index 0000000..3048707 --- /dev/null +++ b/src/rules/rule_is_instance_clear.py @@ -0,0 +1,7 @@ +from src.rules.rule import Rule + + +class RuleIsInstanceClear(Rule): + def _do_check(self, details): + self._compliance = 'COMPLIANT' if details.computer_light.lower() == 'green' else 'NON_COMPLIANT' + self._compliance_msg = 'Current status: {}'.format(details.computer_light) diff --git a/src/rules/rule_is_instance_protected_by.py b/src/rules/rule_is_instance_protected_by.py new file mode 100644 index 0000000..2d1dcca --- /dev/null +++ b/src/rules/rule_is_instance_protected_by.py @@ -0,0 +1,34 @@ +from src.rules import CONTROL_NAMES +from src.rules.rule import Rule + + +class RuleIsInstanceProtectedBy(Rule): + def _initialize(self): + super()._initialize() + + self._control = self._rule_parameters.get('dsControl') + if not self._control: + raise ValueError('Missing parameter: dsControl') + + @property + def control(self): + return self._control + + def _do_check(self, details): + control_prop = 'overall_{}_status'.format(self._control) + control_status = getattr(details, control_prop) + self._check_control(control_status) + self._compliance_msg = '{} status: {}'.format(CONTROL_NAMES[self._control], control_status) + + def _check_control(self, control_status): + self._compliance = 'NON_COMPLIANT' + if self._control in ['anti_malware', 'integrity_monitoring']: + if 'On, Real Time'.lower() in control_status.lower() or \ + 'On, Security Update In Progress, Real Time'.lower() in control_status.lower(): + self._compliance = 'COMPLIANT' + elif self._control in ['intrusion_prevention']: + if 'On, Prevent'.lower() in control_status.lower(): + self._compliance = 'COMPLIANT' + else: + if 'On'.lower() in control_status.lower(): + self._compliance = 'COMPLIANT' diff --git a/src/rules/rule_is_instance_protected_by_anti_malware.py b/src/rules/rule_is_instance_protected_by_anti_malware.py new file mode 100644 index 0000000..fc88896 --- /dev/null +++ b/src/rules/rule_is_instance_protected_by_anti_malware.py @@ -0,0 +1,12 @@ +from src.rules.rule import Rule + + +class RuleIsInstanceProtectedByAntiMalware(Rule): + def _do_check(self, details): + if 'On, Real Time'.lower() in details.overall_anti_malware_status.lower() or \ + 'On, Security Update In Progress, Real Time'.lower() in details.overall_anti_malware_status.lower(): + self._compliance = 'COMPLIANT' + else: + self._compliance = 'NON_COMPLIANT' + + self._compliance_msg = 'Anti-Malware status: {}'.format(details.overall_anti_malware_status) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7f6f4ae --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,59 @@ +import json +import jsonpickle +import os + +from unittest.mock import MagicMock +from pytest import fixture + + +def load_json(file_path): + dir_name = os.path.dirname(os.path.abspath(__file__)) + file_name = os.path.join(dir_name, file_path) + with open(file_name) as f: + data = json.load(f) + return data + + +@fixture +def event(): + return load_json('resources/event.json') + + +@fixture +def event_policy(): + return load_json('resources/event_with_policy.json') + + +@fixture +def event_control(): + return load_json('resources/event_with_control.json') + + +@fixture +def config_response(): + return load_json('resources/config_response.json') + + +@fixture +def config_service(config_response): + service = MagicMock() + service.put_evaluations.return_value = config_response + return service + + +@fixture +def computers(monkeypatch): + data = load_json('resources/computers.json') + computers = jsonpickle.decode(json.dumps(data)) + monkeypatch.setattr(computers, 'get', lambda: None) + return computers + + +@fixture +def manager(computers): + manager = MagicMock() + manager._request.return_value = None + manager.sign_in.return_value = None + manager.sign_out.return_value = None + manager.computers = computers + return manager diff --git a/tests/resources/computers.json b/tests/resources/computers.json new file mode 100644 index 0000000..c1c4f9a --- /dev/null +++ b/tests/resources/computers.json @@ -0,0 +1,150 @@ +{ + "2": { + "anti_malware_classic_pattern_version": "15.279.00", + "anti_malware_engine_version": "N/A", + "anti_malware_intelli_trap_exception_version": "1.631.00", + "anti_malware_intelli_trap_version": "0.251.00", + "anti_malware_smart_scan_pattern_version": "15.277.00", + "anti_malware_spyware_pattern_version": "2.199.00", + "cloud_object_image_id": "ami-0c7a4976cb6fafd3a", + "cloud_object_instance_id": "i-03f48756b2e392fca", + "cloud_object_internal_unique_id": "i-03f48756b2e392fca", + "cloud_object_security_group_ids": null, + "cloud_object_type": "AMAZON_VM", + "component_klasses": null, + "component_names": null, + "component_types": null, + "component_versions": null, + "computer_group_id": "35", + "computer_group_name": "Computers > stelligentlabs - 000000000000 > EU (Ireland) > kubernetes-vpc-VPC (vpc-0f9ca9cd8f25a420e) > kubernetes-vpc-Subnet02 (subnet-016c57baf66a86747)", + "computer_light": "GREEN", + "computer_type": "STANDARD", + "description": null, + "display_name": "kube-twistlock-nodegroup-Node", + "external": "true", + "external_id": null, + "hostInterfaces": null, + "id": 2, + "last_anit_malware_scheduled_scan": null, + "last_anti_malware_event": null, + "last_anti_malware_manual_scan": null, + "last_firewall_event": null, + "last_integrity_monitoring_event": null, + "last_intrusion_prevention_event": null, + "last_ipused": null, + "last_log_inspection_event": null, + "last_web_reputation_event": null, + "light": "0", + "locked": "false", + "manager": { + "API_TYPE_REST": "REST", + "API_TYPE_SOAP": "SOAP", + "_hostname": "0.0.0.0", + "_log_at_level": 30, + "_password": "xxxxxx", + "_port": "443", + "_prefix": "", + "_rest_api_endpoint": "https://0.0.0.0:443/rest", + "_soap_api_endpoint": "https://0.0.0.0:443/webservice/Manager", + "_tenant": null, + "_username": "xxxxxx", + "cloud_accounts": { + "__dict__": { + "_exempt_from_find": [], + "manager": { + "py/id": 2 + } + }, + "py/object": "environments.CloudAccounts" + }, + "computer_groups": { + "__dict__": { + "_exempt_from_find": [ + "computers" + ], + "computers": { + "__dict__": { + "_exempt_from_find": [] + }, + "py/object": "core.CoreDict" + }, + "manager": { + "py/id": 2 + } + }, + "py/object": "computers.ComputerGroups" + }, + "computers": { + "py/id": 0 + }, + "ignore_ssl_validation": 1, + "ip_lists": { + "__dict__": { + "_exempt_from_find": [], + "manager": { + "py/id": 2 + } + }, + "py/object": "policies.IPLists" + }, + "logger": { + "py/reduce": [ + { + "py/function": "logging.getLogger" + }, + { + "py/tuple": [ + "DeepSecurity.API" + ] + } + ] + }, + "policies": { + "__dict__": { + "_exempt_from_find": [], + "manager": { + "py/id": 2 + } + }, + "py/object": "policies.Policies" + }, + "py/object": "src.deepsecurity.dsm.Manager", + "rules": { + "__dict__": { + "_exempt_from_find": [], + "manager": { + "py/id": 2 + } + }, + "py/object": "policies.Rules" + } + }, + "name": "ec2-0-0-0-0.eu-west-1.compute.amazonaws.com", + "overall_anti_malware_status": "Anti-Malware: On, Real Time", + "overall_firewall_status": "Firewall: On, 8 rules", + "overall_integrity_monitoring_status": "Integrity Monitoring: Not Licensed", + "overall_intrusion_prevention_status": "Intrusion Prevention: Not Licensed", + "overall_last_recommendation_scan": null, + "overall_last_successful_communication": null, + "overall_last_successful_update": null, + "overall_last_update_required": "2019-08-06T13:39:28.800Z", + "overall_log_inspection_status": "Log Inspection: Off, not installed, no rules", + "overall_status": "Managed (Online)", + "overall_version": "N/A", + "overall_web_reputation_status": "Web Reputation: Off, not installed", + "platform": "Unknown (64 bit)", + "policy_id": null, + "policy_name": "Linux Server", + "py/object": "computers.Computer", + "recommended_rules": null, + "virtual_name": null, + "virtual_uuid": null + }, + "__dict__": { + "_exempt_from_find": [], + "manager": { + "py/id": 2 + } + }, + "py/object": "computers.Computers" +} \ No newline at end of file diff --git a/tests/resources/config_response.json b/tests/resources/config_response.json new file mode 100644 index 0000000..f0417ca --- /dev/null +++ b/tests/resources/config_response.json @@ -0,0 +1,15 @@ +{ + "FailedEvaluations": [], + "ResponseMetadata": { + "RequestId": "7964b119-ea29-46d1-86c5-b5def3e85b13", + "HTTPStatusCode": 200, + "HTTPHeaders": { + "x-amzn-requestid": "7964b119-ea29-46d1-86c5-b5def3e85b13", + "strict-transport-security": "max-age=86400", + "content-type": "application/x-amz-json-1.1", + "content-length": "24", + "date": "Tue, 06 Aug 2019 16:54:08 GMT" + }, + "RetryAttempts": 0 + } +} \ No newline at end of file diff --git a/tests/resources/event.json b/tests/resources/event.json new file mode 100644 index 0000000..294900f --- /dev/null +++ b/tests/resources/event.json @@ -0,0 +1,12 @@ +{ + "configRuleId": "config-rule-rop19i", + "version": "1.0", + "configRuleName": "dsIsInstanceClear", + "configRuleArn": "arn:aws:config:us-west-1:000000000000:config-rule/config-rule-rop19i", + "invokingEvent": "{\"configurationItemDiff\":null,\"configurationItem\":{\"relatedEvents\":[],\"relationships\":[{\"resourceId\":\"eni-0ac7c43ee76a7009c\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::NetworkInterface\",\"name\":\"Contains NetworkInterface\"},{\"resourceId\":\"sg-020755897c0825901\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::SecurityGroup\",\"name\":\"Is associated with SecurityGroup\"},{\"resourceId\":\"sg-09caa2f0b560c1f7d\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::SecurityGroup\",\"name\":\"Is associated with SecurityGroup\"},{\"resourceId\":\"subnet-07d982e574e75f3e5\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::Subnet\",\"name\":\"Is contained in Subnet\"},{\"resourceId\":\"vol-067a98804f50c6932\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::Volume\",\"name\":\"Is attached to Volume\"},{\"resourceId\":\"vol-08cf33b707496abec\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::Volume\",\"name\":\"Is attached to Volume\"},{\"resourceId\":\"vpc-014188d7689bcf41f\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::VPC\",\"name\":\"Is contained in Vpc\"}],\"configuration\":{\"amiLaunchIndex\":0,\"imageId\":\"ami-0041a7cbd48160980\",\"instanceId\":\"i-03f48756b2e392fca\",\"instanceType\":\"m5.large\",\"kernelId\":null,\"keyName\":\"dsm\",\"launchTime\":\"2019-07-18T17:46:24.000Z\",\"monitoring\":{\"state\":\"disabled\"},\"placement\":{\"availabilityZone\":\"us-west-1a\",\"affinity\":null,\"groupName\":\"\",\"partitionNumber\":null,\"hostId\":null,\"tenancy\":\"default\",\"spreadDomain\":null},\"platform\":null,\"privateDnsName\":\"ip-172-30-0-180.us-west-1.compute.internal\",\"privateIpAddress\":\"172.30.0.180\",\"productCodes\":[{\"productCodeId\":\"xxxxxxxx\",\"productCodeType\":\"marketplace\"}],\"publicDnsName\":\"\",\"publicIpAddress\":\"0.0.0.0\",\"ramdiskId\":null,\"state\":{\"code\":16,\"name\":\"running\"},\"stateTransitionReason\":\"\",\"subnetId\":\"subnet-07d982e574e75f3e5\",\"vpcId\":\"vpc-014188d7689bcf41f\",\"architecture\":\"x86_64\",\"blockDeviceMappings\":[{\"deviceName\":\"/dev/xvda\",\"ebs\":{\"attachTime\":\"2019-07-18T17:46:24.000Z\",\"deleteOnTermination\":true,\"status\":\"attached\",\"volumeId\":\"vol-067a98804f50c6932\"}},{\"deviceName\":\"/dev/sdf\",\"ebs\":{\"attachTime\":\"2019-07-18T17:46:24.000Z\",\"deleteOnTermination\":true,\"status\":\"attached\",\"volumeId\":\"vol-08cf33b707496abec\"}}],\"clientToken\":\"156347198056481272\",\"ebsOptimized\":true,\"enaSupport\":true,\"hypervisor\":\"xen\",\"iamInstanceProfile\":null,\"instanceLifecycle\":null,\"elasticGpuAssociations\":[],\"elasticInferenceAcceleratorAssociations\":[],\"networkInterfaces\":[{\"association\":{\"ipOwnerId\":\"amazon\",\"publicDnsName\":\"\",\"publicIp\":\"0.0.0.0\"},\"attachment\":{\"attachTime\":\"2019-07-18T17:46:24.000Z\",\"attachmentId\":\"eni-attach-0e08f46622bc60e1d\",\"deleteOnTermination\":true,\"deviceIndex\":0,\"status\":\"attached\"},\"description\":\"Primary network interface\",\"groups\":[{\"groupName\":\"Trend Micro Deep Security -BYOL--Deep Security 12-0-300-AutogenByAWSMP-\",\"groupId\":\"sg-09caa2f0b560c1f7d\"},{\"groupName\":\"default\",\"groupId\":\"sg-020755897c0825901\"}],\"ipv6Addresses\":[],\"macAddress\":\"00:00:00:00:00:00\",\"networkInterfaceId\":\"eni-0ac7c43ee76a7009c\",\"ownerId\":\"000000000000\",\"privateDnsName\":null,\"privateIpAddress\":\"172.30.0.180\",\"privateIpAddresses\":[{\"association\":{\"ipOwnerId\":\"amazon\",\"publicDnsName\":\"\",\"publicIp\":\"0.0.0.0\"},\"primary\":true,\"privateDnsName\":null,\"privateIpAddress\":\"172.30.0.180\"}],\"sourceDestCheck\":true,\"status\":\"in-use\",\"subnetId\":\"subnet-07d982e574e75f3e5\",\"vpcId\":\"vpc-014188d7689bcf41f\",\"interfaceType\":\"interface\"}],\"rootDeviceName\":\"/dev/xvda\",\"rootDeviceType\":\"ebs\",\"securityGroups\":[{\"groupName\":\"Trend Micro Deep Security -BYOL--Deep Security 12-0-300-AutogenByAWSMP-\",\"groupId\":\"sg-09caa2f0b560c1f7d\"},{\"groupName\":\"default\",\"groupId\":\"sg-020755897c0825901\"}],\"sourceDestCheck\":true,\"spotInstanceRequestId\":null,\"sriovNetSupport\":null,\"stateReason\":null,\"tags\":[{\"key\":\"Name\",\"value\":\"Deep Security Manager\"}],\"virtualizationType\":\"hvm\",\"cpuOptions\":{\"coreCount\":1,\"threadsPerCore\":2},\"capacityReservationId\":null,\"capacityReservationSpecification\":{\"capacityReservationPreference\":\"open\",\"capacityReservationTarget\":null},\"hibernationOptions\":{\"configured\":false},\"licenses\":[]},\"supplementaryConfiguration\":{},\"tags\":{\"Name\":\"Deep Security Manager\"},\"configurationItemVersion\":\"1.3\",\"configurationItemCaptureTime\":\"2019-07-18T19:24:11.679Z\",\"configurationStateId\":1563477851679,\"awsAccountId\":\"000000000000\",\"configurationItemStatus\":\"OK\",\"resourceType\":\"AWS::EC2::Instance\",\"resourceId\":\"i-03f48756b2e392fca\",\"resourceName\":null,\"ARN\":\"arn:aws:ec2:us-west-1:000000000000:instance/i-03f48756b2e392fca\",\"awsRegion\":\"us-west-1\",\"availabilityZone\":\"us-west-1a\",\"configurationStateMd5Hash\":\"\",\"resourceCreationTime\":\"2019-07-18T17:46:24.000Z\"},\"notificationCreationTime\":\"2019-07-18T21:34:41.397Z\",\"messageType\":\"ConfigurationItemChangeNotification\",\"recordVersion\":\"1.3\"}", + "resultToken": "xxxxxxxxxxxxxxx", + "eventLeftScope": false, + "ruleParameters": "{\"dsUsernameKey\":\"/ds/api_user\",\"dsPasswordKey\":\"/ds/api_password\",\"dsHostname\":\"0.0.0.0\",\"dsPort\":\"443\",\"dsIgnoreSslValidation\":\"true\"}", + "executionRoleArn": "arn:aws:iam::000000000000:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig", + "accountId": "000000000000" +} \ No newline at end of file diff --git a/tests/resources/event_with_control.json b/tests/resources/event_with_control.json new file mode 100644 index 0000000..7ef0f7e --- /dev/null +++ b/tests/resources/event_with_control.json @@ -0,0 +1,12 @@ +{ + "configRuleId": "config-rule-rop19i", + "version": "1.0", + "configRuleName": "dsIsInstanceClear", + "configRuleArn": "arn:aws:config:us-west-1:000000000000:config-rule/config-rule-rop19i", + "invokingEvent": "{\"configurationItemDiff\":null,\"configurationItem\":{\"relatedEvents\":[],\"relationships\":[{\"resourceId\":\"eni-0ac7c43ee76a7009c\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::NetworkInterface\",\"name\":\"Contains NetworkInterface\"},{\"resourceId\":\"sg-020755897c0825901\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::SecurityGroup\",\"name\":\"Is associated with SecurityGroup\"},{\"resourceId\":\"sg-09caa2f0b560c1f7d\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::SecurityGroup\",\"name\":\"Is associated with SecurityGroup\"},{\"resourceId\":\"subnet-07d982e574e75f3e5\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::Subnet\",\"name\":\"Is contained in Subnet\"},{\"resourceId\":\"vol-067a98804f50c6932\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::Volume\",\"name\":\"Is attached to Volume\"},{\"resourceId\":\"vol-08cf33b707496abec\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::Volume\",\"name\":\"Is attached to Volume\"},{\"resourceId\":\"vpc-014188d7689bcf41f\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::VPC\",\"name\":\"Is contained in Vpc\"}],\"configuration\":{\"amiLaunchIndex\":0,\"imageId\":\"ami-0041a7cbd48160980\",\"instanceId\":\"i-03f48756b2e392fca\",\"instanceType\":\"m5.large\",\"kernelId\":null,\"keyName\":\"dsm\",\"launchTime\":\"2019-07-18T17:46:24.000Z\",\"monitoring\":{\"state\":\"disabled\"},\"placement\":{\"availabilityZone\":\"us-west-1a\",\"affinity\":null,\"groupName\":\"\",\"partitionNumber\":null,\"hostId\":null,\"tenancy\":\"default\",\"spreadDomain\":null},\"platform\":null,\"privateDnsName\":\"ip-172-30-0-180.us-west-1.compute.internal\",\"privateIpAddress\":\"172.30.0.180\",\"productCodes\":[{\"productCodeId\":\"xxxxxxxx\",\"productCodeType\":\"marketplace\"}],\"publicDnsName\":\"\",\"publicIpAddress\":\"0.0.0.0\",\"ramdiskId\":null,\"state\":{\"code\":16,\"name\":\"running\"},\"stateTransitionReason\":\"\",\"subnetId\":\"subnet-07d982e574e75f3e5\",\"vpcId\":\"vpc-014188d7689bcf41f\",\"architecture\":\"x86_64\",\"blockDeviceMappings\":[{\"deviceName\":\"/dev/xvda\",\"ebs\":{\"attachTime\":\"2019-07-18T17:46:24.000Z\",\"deleteOnTermination\":true,\"status\":\"attached\",\"volumeId\":\"vol-067a98804f50c6932\"}},{\"deviceName\":\"/dev/sdf\",\"ebs\":{\"attachTime\":\"2019-07-18T17:46:24.000Z\",\"deleteOnTermination\":true,\"status\":\"attached\",\"volumeId\":\"vol-08cf33b707496abec\"}}],\"clientToken\":\"156347198056481272\",\"ebsOptimized\":true,\"enaSupport\":true,\"hypervisor\":\"xen\",\"iamInstanceProfile\":null,\"instanceLifecycle\":null,\"elasticGpuAssociations\":[],\"elasticInferenceAcceleratorAssociations\":[],\"networkInterfaces\":[{\"association\":{\"ipOwnerId\":\"amazon\",\"publicDnsName\":\"\",\"publicIp\":\"0.0.0.0\"},\"attachment\":{\"attachTime\":\"2019-07-18T17:46:24.000Z\",\"attachmentId\":\"eni-attach-0e08f46622bc60e1d\",\"deleteOnTermination\":true,\"deviceIndex\":0,\"status\":\"attached\"},\"description\":\"Primary network interface\",\"groups\":[{\"groupName\":\"Trend Micro Deep Security -BYOL--Deep Security 12-0-300-AutogenByAWSMP-\",\"groupId\":\"sg-09caa2f0b560c1f7d\"},{\"groupName\":\"default\",\"groupId\":\"sg-020755897c0825901\"}],\"ipv6Addresses\":[],\"macAddress\":\"00:00:00:00:00:00\",\"networkInterfaceId\":\"eni-0ac7c43ee76a7009c\",\"ownerId\":\"000000000000\",\"privateDnsName\":null,\"privateIpAddress\":\"172.30.0.180\",\"privateIpAddresses\":[{\"association\":{\"ipOwnerId\":\"amazon\",\"publicDnsName\":\"\",\"publicIp\":\"0.0.0.0\"},\"primary\":true,\"privateDnsName\":null,\"privateIpAddress\":\"172.30.0.180\"}],\"sourceDestCheck\":true,\"status\":\"in-use\",\"subnetId\":\"subnet-07d982e574e75f3e5\",\"vpcId\":\"vpc-014188d7689bcf41f\",\"interfaceType\":\"interface\"}],\"rootDeviceName\":\"/dev/xvda\",\"rootDeviceType\":\"ebs\",\"securityGroups\":[{\"groupName\":\"Trend Micro Deep Security -BYOL--Deep Security 12-0-300-AutogenByAWSMP-\",\"groupId\":\"sg-09caa2f0b560c1f7d\"},{\"groupName\":\"default\",\"groupId\":\"sg-020755897c0825901\"}],\"sourceDestCheck\":true,\"spotInstanceRequestId\":null,\"sriovNetSupport\":null,\"stateReason\":null,\"tags\":[{\"key\":\"Name\",\"value\":\"Deep Security Manager\"}],\"virtualizationType\":\"hvm\",\"cpuOptions\":{\"coreCount\":1,\"threadsPerCore\":2},\"capacityReservationId\":null,\"capacityReservationSpecification\":{\"capacityReservationPreference\":\"open\",\"capacityReservationTarget\":null},\"hibernationOptions\":{\"configured\":false},\"licenses\":[]},\"supplementaryConfiguration\":{},\"tags\":{\"Name\":\"Deep Security Manager\"},\"configurationItemVersion\":\"1.3\",\"configurationItemCaptureTime\":\"2019-07-18T19:24:11.679Z\",\"configurationStateId\":1563477851679,\"awsAccountId\":\"000000000000\",\"configurationItemStatus\":\"OK\",\"resourceType\":\"AWS::EC2::Instance\",\"resourceId\":\"i-03f48756b2e392fca\",\"resourceName\":null,\"ARN\":\"arn:aws:ec2:us-west-1:000000000000:instance/i-03f48756b2e392fca\",\"awsRegion\":\"us-west-1\",\"availabilityZone\":\"us-west-1a\",\"configurationStateMd5Hash\":\"\",\"resourceCreationTime\":\"2019-07-18T17:46:24.000Z\"},\"notificationCreationTime\":\"2019-07-18T21:34:41.397Z\",\"messageType\":\"ConfigurationItemChangeNotification\",\"recordVersion\":\"1.3\"}", + "resultToken": "xxxxxxxxxxxxxxx", + "eventLeftScope": false, + "ruleParameters": "{\"dsUsernameKey\":\"/ds/api_user\",\"dsPasswordKey\":\"/ds/api_password\",\"dsHostname\":\"0.0.0.0\",\"dsPort\":\"443\",\"dsIgnoreSslValidation\":\"true\",\"dsControl\":\"firewall\"}", + "executionRoleArn": "arn:aws:iam::000000000000:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig", + "accountId": "000000000000" +} \ No newline at end of file diff --git a/tests/resources/event_with_policy.json b/tests/resources/event_with_policy.json new file mode 100644 index 0000000..9a707f8 --- /dev/null +++ b/tests/resources/event_with_policy.json @@ -0,0 +1,12 @@ +{ + "configRuleId": "config-rule-rop19i", + "version": "1.0", + "configRuleName": "dsIsInstanceClear", + "configRuleArn": "arn:aws:config:us-west-1:000000000000:config-rule/config-rule-rop19i", + "invokingEvent": "{\"configurationItemDiff\":null,\"configurationItem\":{\"relatedEvents\":[],\"relationships\":[{\"resourceId\":\"eni-0ac7c43ee76a7009c\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::NetworkInterface\",\"name\":\"Contains NetworkInterface\"},{\"resourceId\":\"sg-020755897c0825901\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::SecurityGroup\",\"name\":\"Is associated with SecurityGroup\"},{\"resourceId\":\"sg-09caa2f0b560c1f7d\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::SecurityGroup\",\"name\":\"Is associated with SecurityGroup\"},{\"resourceId\":\"subnet-07d982e574e75f3e5\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::Subnet\",\"name\":\"Is contained in Subnet\"},{\"resourceId\":\"vol-067a98804f50c6932\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::Volume\",\"name\":\"Is attached to Volume\"},{\"resourceId\":\"vol-08cf33b707496abec\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::Volume\",\"name\":\"Is attached to Volume\"},{\"resourceId\":\"vpc-014188d7689bcf41f\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::VPC\",\"name\":\"Is contained in Vpc\"}],\"configuration\":{\"amiLaunchIndex\":0,\"imageId\":\"ami-0041a7cbd48160980\",\"instanceId\":\"i-03f48756b2e392fca\",\"instanceType\":\"m5.large\",\"kernelId\":null,\"keyName\":\"dsm\",\"launchTime\":\"2019-07-18T17:46:24.000Z\",\"monitoring\":{\"state\":\"disabled\"},\"placement\":{\"availabilityZone\":\"us-west-1a\",\"affinity\":null,\"groupName\":\"\",\"partitionNumber\":null,\"hostId\":null,\"tenancy\":\"default\",\"spreadDomain\":null},\"platform\":null,\"privateDnsName\":\"ip-172-30-0-180.us-west-1.compute.internal\",\"privateIpAddress\":\"172.30.0.180\",\"productCodes\":[{\"productCodeId\":\"xxxxxxxx\",\"productCodeType\":\"marketplace\"}],\"publicDnsName\":\"\",\"publicIpAddress\":\"0.0.0.0\",\"ramdiskId\":null,\"state\":{\"code\":16,\"name\":\"running\"},\"stateTransitionReason\":\"\",\"subnetId\":\"subnet-07d982e574e75f3e5\",\"vpcId\":\"vpc-014188d7689bcf41f\",\"architecture\":\"x86_64\",\"blockDeviceMappings\":[{\"deviceName\":\"/dev/xvda\",\"ebs\":{\"attachTime\":\"2019-07-18T17:46:24.000Z\",\"deleteOnTermination\":true,\"status\":\"attached\",\"volumeId\":\"vol-067a98804f50c6932\"}},{\"deviceName\":\"/dev/sdf\",\"ebs\":{\"attachTime\":\"2019-07-18T17:46:24.000Z\",\"deleteOnTermination\":true,\"status\":\"attached\",\"volumeId\":\"vol-08cf33b707496abec\"}}],\"clientToken\":\"156347198056481272\",\"ebsOptimized\":true,\"enaSupport\":true,\"hypervisor\":\"xen\",\"iamInstanceProfile\":null,\"instanceLifecycle\":null,\"elasticGpuAssociations\":[],\"elasticInferenceAcceleratorAssociations\":[],\"networkInterfaces\":[{\"association\":{\"ipOwnerId\":\"amazon\",\"publicDnsName\":\"\",\"publicIp\":\"0.0.0.0\"},\"attachment\":{\"attachTime\":\"2019-07-18T17:46:24.000Z\",\"attachmentId\":\"eni-attach-0e08f46622bc60e1d\",\"deleteOnTermination\":true,\"deviceIndex\":0,\"status\":\"attached\"},\"description\":\"Primary network interface\",\"groups\":[{\"groupName\":\"Trend Micro Deep Security -BYOL--Deep Security 12-0-300-AutogenByAWSMP-\",\"groupId\":\"sg-09caa2f0b560c1f7d\"},{\"groupName\":\"default\",\"groupId\":\"sg-020755897c0825901\"}],\"ipv6Addresses\":[],\"macAddress\":\"00:00:00:00:00:00\",\"networkInterfaceId\":\"eni-0ac7c43ee76a7009c\",\"ownerId\":\"000000000000\",\"privateDnsName\":null,\"privateIpAddress\":\"172.30.0.180\",\"privateIpAddresses\":[{\"association\":{\"ipOwnerId\":\"amazon\",\"publicDnsName\":\"\",\"publicIp\":\"0.0.0.0\"},\"primary\":true,\"privateDnsName\":null,\"privateIpAddress\":\"172.30.0.180\"}],\"sourceDestCheck\":true,\"status\":\"in-use\",\"subnetId\":\"subnet-07d982e574e75f3e5\",\"vpcId\":\"vpc-014188d7689bcf41f\",\"interfaceType\":\"interface\"}],\"rootDeviceName\":\"/dev/xvda\",\"rootDeviceType\":\"ebs\",\"securityGroups\":[{\"groupName\":\"Trend Micro Deep Security -BYOL--Deep Security 12-0-300-AutogenByAWSMP-\",\"groupId\":\"sg-09caa2f0b560c1f7d\"},{\"groupName\":\"default\",\"groupId\":\"sg-020755897c0825901\"}],\"sourceDestCheck\":true,\"spotInstanceRequestId\":null,\"sriovNetSupport\":null,\"stateReason\":null,\"tags\":[{\"key\":\"Name\",\"value\":\"Deep Security Manager\"}],\"virtualizationType\":\"hvm\",\"cpuOptions\":{\"coreCount\":1,\"threadsPerCore\":2},\"capacityReservationId\":null,\"capacityReservationSpecification\":{\"capacityReservationPreference\":\"open\",\"capacityReservationTarget\":null},\"hibernationOptions\":{\"configured\":false},\"licenses\":[]},\"supplementaryConfiguration\":{},\"tags\":{\"Name\":\"Deep Security Manager\"},\"configurationItemVersion\":\"1.3\",\"configurationItemCaptureTime\":\"2019-07-18T19:24:11.679Z\",\"configurationStateId\":1563477851679,\"awsAccountId\":\"000000000000\",\"configurationItemStatus\":\"OK\",\"resourceType\":\"AWS::EC2::Instance\",\"resourceId\":\"i-03f48756b2e392fca\",\"resourceName\":null,\"ARN\":\"arn:aws:ec2:us-west-1:000000000000:instance/i-03f48756b2e392fca\",\"awsRegion\":\"us-west-1\",\"availabilityZone\":\"us-west-1a\",\"configurationStateMd5Hash\":\"\",\"resourceCreationTime\":\"2019-07-18T17:46:24.000Z\"},\"notificationCreationTime\":\"2019-07-18T21:34:41.397Z\",\"messageType\":\"ConfigurationItemChangeNotification\",\"recordVersion\":\"1.3\"}", + "resultToken": "xxxxxxxxxxxxxxx", + "eventLeftScope": false, + "ruleParameters": "{\"dsUsernameKey\":\"/ds/api_user\",\"dsPasswordKey\":\"/ds/api_password\",\"dsHostname\":\"0.0.0.0\",\"dsPort\":\"443\",\"dsIgnoreSslValidation\":\"true\",\"dsPolicy\":\"Linux Server\"}", + "executionRoleArn": "arn:aws:iam::000000000000:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig", + "accountId": "000000000000" +} \ No newline at end of file diff --git a/tests/test_credentials.py b/tests/test_credentials.py new file mode 100644 index 0000000..30dea6e --- /dev/null +++ b/tests/test_credentials.py @@ -0,0 +1,45 @@ +import datetime +from unittest.mock import patch, MagicMock +from src.deepsecurity.credentials import Credentials + + +@patch('boto3.client') +def test_get_username(mock_client): + ssm = MagicMock() + ssm.get_parameter.return_value = { + 'Parameter': { + 'ARN': u'arn:aws:ssm:us-west-1:000000000000:parameter/ds/api_user', + 'LastModifiedDate': datetime.datetime(2019, 7, 23, 15, 43, 26, 375000), + 'Name': u'/ds/api_user', + 'Type': u'SecureString', + 'Value': u'admin', + 'Version': 1 + } + } + mock_client.return_value = ssm + + credentials = Credentials('/ds/api_user', '/ds/api_password') + username = credentials.get_username() + + assert username == 'admin' + + +@patch('boto3.client') +def test_get_password(mock_client): + ssm = MagicMock() + ssm.get_parameter.return_value = { + 'Parameter': { + 'ARN': u'arn:aws:ssm:us-west-1:000000000000:parameter/ds/api_password', + 'LastModifiedDate': datetime.datetime(2019, 7, 23, 15, 43, 26, 375000), + 'Name': u'/ds/api_password', + 'Type': u'SecureString', + 'Value': u'password', + 'Version': 1 + } + } + mock_client.return_value = ssm + + credentials = Credentials('/ds/api_user', '/ds/api_password') + password = credentials.get_password() + + assert password == 'password' diff --git a/tests/test_rule.py b/tests/test_rule.py new file mode 100644 index 0000000..e5f3efd --- /dev/null +++ b/tests/test_rule.py @@ -0,0 +1,31 @@ +from unittest.mock import patch, MagicMock +from src.rules.rule import Rule + + +def test_requirement(event): + rule = Rule(event) + assert rule.username_key + assert rule.password_key + assert rule.hostname + + +@patch('src.deepsecurity.credentials.Credentials.get_password') +@patch('src.deepsecurity.credentials.Credentials.get_username') +def test_get_credentials(mock_get_username, mock_get_password, event): + mock_get_username.return_value = 'user' + mock_get_password.return_value = 'pass' + + rule = Rule(event) + creds = rule._get_credentials() + assert creds == ('user', 'pass') + + +@patch('boto3.client') +def test_respond_to_config(mock_client, config_response, event): + config = MagicMock() + config.put_evaluations.return_value = config_response + mock_client.return_value = config + + rule = Rule(event) + response = rule._respond_to_config() + assert response == config_response diff --git a/tests/test_rule_does_instance_have_policy.py b/tests/test_rule_does_instance_have_policy.py new file mode 100644 index 0000000..c02ceaf --- /dev/null +++ b/tests/test_rule_does_instance_have_policy.py @@ -0,0 +1,23 @@ +from unittest.mock import patch +from src.rules.rule_does_instance_have_policy import RuleDoesInstanceHavePolicy + + +def test_requirement(event_policy): + rule = RuleDoesInstanceHavePolicy(event_policy) + assert rule.username_key + assert rule.password_key + assert rule.hostname + assert rule.policy + + +@patch('boto3.client') +@patch('src.rules.rule.Manager') +def test_execute(mock_dsm, mock_client, manager, config_service, event_policy): + mock_dsm.return_value = manager + mock_client.return_value = config_service + + rule = RuleDoesInstanceHavePolicy(event_policy) + rule.execute() + + assert rule.compliance == 'COMPLIANT' + assert rule.compliance_msg == 'Current policy: Linux Server' diff --git a/tests/test_rule_is_instance_clear.py b/tests/test_rule_is_instance_clear.py new file mode 100644 index 0000000..5fb0718 --- /dev/null +++ b/tests/test_rule_is_instance_clear.py @@ -0,0 +1,15 @@ +from unittest.mock import patch +from src.rules.rule_is_instance_clear import RuleIsInstanceClear + + +@patch('boto3.client') +@patch('src.rules.rule.Manager') +def test_execute(mock_dsm, mock_client, manager, config_service, event): + mock_dsm.return_value = manager + mock_client.return_value = config_service + + rule = RuleIsInstanceClear(event) + rule.execute() + + assert rule.compliance == 'COMPLIANT' + assert rule.compliance_msg == 'Current status: GREEN' diff --git a/tests/test_rule_is_instance_protected_by.py b/tests/test_rule_is_instance_protected_by.py new file mode 100644 index 0000000..2c93770 --- /dev/null +++ b/tests/test_rule_is_instance_protected_by.py @@ -0,0 +1,23 @@ +from unittest.mock import patch +from src.rules.rule_is_instance_protected_by import RuleIsInstanceProtectedBy + + +def test_requirement(event_control): + rule = RuleIsInstanceProtectedBy(event_control) + assert rule.username_key + assert rule.password_key + assert rule.hostname + assert rule.control + + +@patch('boto3.client') +@patch('src.rules.rule.Manager') +def test_execute(mock_dsm, mock_client, manager, config_service, event_control): + mock_dsm.return_value = manager + mock_client.return_value = config_service + + rule = RuleIsInstanceProtectedBy(event_control) + rule.execute() + + assert rule.compliance == 'COMPLIANT' + assert rule.compliance_msg == 'Firewall status: Firewall: On, 8 rules' diff --git a/tests/test_rule_is_instance_protected_by_anti_malware.py b/tests/test_rule_is_instance_protected_by_anti_malware.py new file mode 100644 index 0000000..61d051d --- /dev/null +++ b/tests/test_rule_is_instance_protected_by_anti_malware.py @@ -0,0 +1,15 @@ +from unittest.mock import patch +from src.rules.rule_is_instance_protected_by_anti_malware import RuleIsInstanceProtectedByAntiMalware + + +@patch('boto3.client') +@patch('src.rules.rule.Manager') +def test_execute(mock_dsm, mock_client, manager, config_service, event): + mock_dsm.return_value = manager + mock_client.return_value = config_service + + rule = RuleIsInstanceProtectedByAntiMalware(event) + rule.execute() + + assert rule.compliance == 'COMPLIANT' + assert rule.compliance_msg == 'Anti-Malware status: Anti-Malware: On, Real Time'