├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── COVERAGE.md ├── LICENSE ├── README.md ├── auto_remediate ├── __init__.py ├── config_rules.py ├── custom_rules.py ├── data │ ├── cloud_trail_cloud_watch_logs_enabled_policy.json │ ├── cloud_trail_cloud_watch_logs_enabled_trust_relationship.json │ ├── cloud_trail_encryption_enabled_kms_policy.json │ └── s3_bucket_ssl_requests_only_policy.json ├── lambda_handler.py ├── security_hub_rules.py ├── sns_logging_handler.py └── test │ ├── __init__.py │ ├── test_config_rds.py │ ├── test_config_s3.py │ ├── test_securityhub_ec2.py │ ├── test_securityhub_iam.py │ ├── test_securityhub_kms.py │ ├── test_securityhub_s3.py │ └── test_securityhub_static.py ├── auto_remediate_dlq ├── __init__.py ├── lambda_handler.py └── test │ ├── __init__.py │ ├── data │ └── config_payload.json │ └── test_dlq.py ├── auto_remediate_setup ├── __init__.py ├── data │ ├── auto-remediate-settings.json │ ├── config_rules │ │ ├── cloudtrail-enabled.json │ │ ├── db-instance-backup-enabled.json │ │ ├── dynamodb-table-encryption-enabled.json │ │ ├── ec2-instances-in-vpc.json │ │ ├── encrypted-volumes.json │ │ ├── guardduty-enabled-centralized.json │ │ ├── lambda-function-public-access-prohibited.json │ │ ├── rds-instance-public-access-check.json │ │ ├── rds-multi-az-support.json │ │ ├── rds-snapshots-public-prohibited.json │ │ ├── rds-storage-encrypted.json │ │ ├── s3-bucket-server-side-encryption-enabled.json │ │ └── s3-bucket-ssl-requests-only.json │ └── custom_rules │ │ └── __init__.py ├── lambda_handler.py └── test │ ├── __init__.py │ ├── data │ ├── auto-remediate-settings-deploy.json │ ├── auto-remediate-settings-remove.json │ └── mock_rules │ │ └── cloudtrail-enabled.json │ └── test_setup.py ├── images └── auto-remediate.svg ├── package.json ├── requirements.txt └── serverless.yml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Stacktrace** 27 | If applicable, add stacktraces to help explain your problem. 28 | 29 | ```python 30 | 31 | ``` 32 | 33 | **Versions (please complete the following information):** 34 | 35 | - Serverless Framework: [e.g. 1.42.3] 36 | - boto3: [e.g. 1.9.156] 37 | - botocore: [e.g. 1.12.156] 38 | - moto: [e.g. 1.3.8] 39 | - pytest: [e.g. 4.4.1] 40 | 41 | **AWS (please complete the following information):** 42 | 43 | - Region: [e.g. ap-southeast-2] 44 | 45 | **Additional context** 46 | Add any other context about the problem here. 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | A clear and concise description of what the pull request is. 4 | 5 | ### Related issue(s) (if applicable) 6 | 7 | - Fixes # 8 | 9 | ## Checklist 10 | 11 | ### Generic 12 | 13 | - [ ] Have you followed the guidelines in our [Contributing](https://github.com/servian/aws-auto-remediate/blob/master/CONTRIBUTING.md) document? 14 | - [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/servian/aws-auto-remediate/pulls) for the same update/change? 15 | 16 | ### Development 17 | 18 | - [ ] Have you deployed your changes to AWS and triggered an AWS Config compliance rule change? 19 | - [ ] Have you added comments to all relevant changes within the code? 20 | - [ ] Have you lint your code locally prior to submission? 21 | - [ ] Have you formatted (Python Black and Prettier) your code locally prior to submission? 22 | 23 | ### Testing 24 | 25 | - [ ] Have you created new tests for your submission? 26 | - [ ] Does your submission pass all tests? 27 | - [ ] Does your submission improve or at the very least keep code coverage at the same percentage? 28 | 29 | ### Documentation 30 | 31 | - [ ] Have you added or changed any and all applicable documentation? 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # .idea/ 2 | .idea/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | 109 | # logs 110 | logs 111 | *.log 112 | npm-debug.log 113 | 114 | # os 115 | .DS_Store 116 | .tmp 117 | 118 | # node 119 | node_modules 120 | 121 | # runtime data 122 | pids 123 | *.pid 124 | *.seed 125 | dist 126 | 127 | # serverless 128 | admin.env 129 | .env 130 | _meta 131 | .serverless/ 132 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - "3.7" 5 | install: 6 | - pip install awscli --upgrade --quiet 7 | - pip install codacy-coverage --upgrade --quiet 8 | - pip install coverage --upgrade --quiet 9 | - pip install moto --upgrade --quiet 10 | - pip install --requirement requirements.txt --upgrade --quiet 11 | - npm install serverless --global 12 | - npm install serverless-iam-roles-per-function 13 | - serverless plugin install --name serverless-python-requirements 14 | before_script: 15 | - export BOTO_CONFIG=/dev/null 16 | script: 17 | - coverage run --module pytest --disable-warnings 18 | - coverage xml 19 | - python-codacy-coverage --report coverage.xml 20 | - serverless deploy --stage $TRAVIS_BUILD_NUMBER --region ap-southeast-2 21 | - serverless remove --stage $TRAVIS_BUILD_NUMBER --region ap-southeast-2 22 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dracula-theme.theme-dracula", 4 | "drewbourne.vscode-remark-lint", 5 | "eamodio.gitlens", 6 | "esbenp.prettier-vscode", 7 | "kevinrose.vsc-python-indent", 8 | "ms-python.python", 9 | "ms-vsliveshare.vsliveshare", 10 | "njpwerner.autodocstring", 11 | "pkief.material-icon-theme", 12 | "visualstudioexptteam.vscodeintellicode" 13 | ], 14 | "unwantedRecommendations": [] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "python.formatting.provider": "black" 4 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlevit/aws-auto-remediate/949adb55809a122278a55c6503e19c9032b378ca/CHANGELOG.md -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at marat.levit@servian.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, 71 | available at 72 | 73 | For answers to common questions about this code of conduct, see 74 | 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to AWS Auto Remediate. To start contributing first fork our repository into your own Github account, and create a local clone of it. The latter will be used to get new features implemented or bugs fixed. Once done and you have the code locally on the disk, you can get started. We advice to not work directly on the master branch, but to create a separate branch for each issue you are working on. That way you can easily switch between different work, and you can update each one for latest changes on upstream master individually. 4 | 5 | ## Writing Code 6 | 7 | ### Developing 8 | 9 | Before developing your remediation function, please refer to our [COVERAGE.md](COVERAGE.md) page which includes all AWS Config rules and their development/testing status'. Select an AWS Config rule (either based on the priority assigned by repository maintainers or one of your choosing) and create the relevant function to remediate it. 10 | 11 | Please ensure you create a **single** remediation function with the same name as the remediation rule (replacing hyphens with underscores). 12 | 13 | Each function should contain a [Python docstring](https://www.python.org/dev/peps/pep-0257/) matching the formatting found inside the repository. 14 | 15 | Example: 16 | 17 | ```python 18 | def s3_bucket_ssl_requests_only(self, resource_id): 19 | """Adds Bucket Policy to force SSL only connections 20 | 21 | Arguments: 22 | resource_id {string} -- S3 Bucket name 23 | 24 | Returns: 25 | boolean -- True if remediation was successful 26 | """ 27 | ``` 28 | 29 | ### Testing 30 | 31 | AWS Auto Remediate utilises the [Moto Python library](https://github.com/spulec/moto/) for automated testing via [pytest](https://docs.pytest.org/en/latest/). For each new function written to remediate a security issue, please ensure a new test class is created within the `test` directory. This class should incorporate functions for both positive and negative tests. See [Moto's Implementation Coverage](https://github.com/spulec/moto/blob/master/IMPLEMENTATION_COVERAGE.md) page for all supported mock API calls. 32 | 33 | If the API calls within your new remediation function are not covered by Moto, please ensure the [COVERAGE.md](COVERAGE.md) is updated with `No Moto support` for your particular remediation. 34 | 35 | #### Local testing 36 | 37 | 1. Install `pytest` 38 | 39 | ```bash 40 | pip install pytest --upgrade --user 41 | ``` 42 | 43 | 2. Run `pytest` 44 | 45 | ```bash 46 | pytest 47 | ``` 48 | 49 | #### Local testing with coverage 50 | 51 | 1. Install `pytest` 52 | 53 | ```bash 54 | pip install pytest --upgrade --user 55 | ``` 56 | 57 | 2. Install `coverage` 58 | 59 | ```bash 60 | pip install coverage --upgrade --user 61 | ``` 62 | 63 | 4. Run `pytest` with `coverage` 64 | 65 | ```bash 66 | coverage run --source . -m pytest 67 | ``` 68 | 69 | 5. View coverage report 70 | 71 | ```bash 72 | coverage report 73 | ``` 74 | 75 | ### Formatting 76 | 77 | AWS Auto Remediate is using the [Python Black](https://github.com/python/black) code formatter for Python and [Prettier](https://prettier.io/) code formatter for all YAML and JSON formatting. Please ensure your code is correctly formatted before submitting a pull request. If you're unclear about how to correctly format your code just look at existing code base for inspiration. 78 | 79 | ## Submitting Changes 80 | 81 | When you think your code is ready for review create a pull request within GitHub. Maintainers of the repository will watch out for new PR‘s and review them at regular intervals. 82 | 83 | Each pull request will automatically trigger pytests, code coverage, and AWS deployment via [Travis CI](https://travis-ci.org/servian/aws-auto-remediate) as well as a code quality review and code coverage via [Codacy](https://app.codacy.com/project/servian/aws-auto-remediate/dashboard). If either Travis CI or Codacy fails make sure to address the failures immediately as the pull requests will be unmergeable. 84 | 85 | If comments have been given in a review, they have to get integrated. For those changes a separate commit should be created and pushed to your remote development branch. Don’t forget to add a comment in the PR afterward, so everyone gets notified by GitHub. Keep in mind that reviews can span multiple cycles until the maintainers are happy with the code. 86 | -------------------------------------------------------------------------------- /COVERAGE.md: -------------------------------------------------------------------------------- 1 | # Coverage 2 | 3 | Below tables represent the coverage of Auto Remediate. Automated testing of Auto Remediate is done using the [Moto](https://github.com/spulec/moto) Python library. 4 | 5 | ## Security Hub Rules 6 | 7 | Development coverage: **24 of 24** 8 | 9 | Test coverage: **10 of 24** 10 | 11 | | Rule | Development Status | Testing Status | 12 | | ------------------------------------------------------ | ------------------ | --------------- | 13 | | securityhub-access-keys-rotated | Done | Done | 14 | | securityhub-cloud-trail-cloud-watch-logs-enabled | Done ​ | No Moto support | 15 | | securityhub-cloud-trail-encryption-enabled | Done | No Moto support | 16 | | securityhub-cloud-trail-log-file-validation | Done | No Moto support | 17 | | securityhub-cmk-backing-key-rotation-enabled | Done | Done | 18 | | securityhub-iam-password-policy-ensure-expires | Done | No Moto support | 19 | | securityhub-iam-password-policy-lowercase-letter-check | Done | No Moto support | 20 | | securityhub-iam-password-policy-minimum-length-check | Done | No Moto support | 21 | | securityhub-iam-password-policy-number-check | Done | No Moto support | 22 | | securityhub-iam-password-policy-prevent-reuse-check | Done | No Moto support | 23 | | securityhub-iam-password-policy-symbol-check | Done | No Moto support | 24 | | securityhub-iam-password-policy-uppercase-letter-check | Done | No Moto support | 25 | | securityhub-iam-policy-no-statements-with-admin-access | Done | Done | 26 | | securityhub-iam-root-access-key-check | Not possible | N/A | 27 | | securityhub-iam-user-no-policies-check | Done | Done | 28 | | securityhub-iam-user-unused-credentials-check | Done | | 29 | | securityhub-mfa-enabled-for-iam-console-access | Done | Done | 30 | | securityhub-multi-region-cloud-trail-enabled | Done | No Moto support | 31 | | securityhub-restricted-rdp | Done | Done | 32 | | securityhub-restricted-ssh | Done | Done | 33 | | securityhub-root-account-hardware-mfa-enabled | Not possible | N/A | 34 | | securityhub-root-account-mfa-enabled | Not possible | N/A | 35 | | securityhub-s3-bucket-logging-enabled | Done | No Moto support | 36 | | securityhub-s3-bucket-public-read-prohibited | Done | Done | 37 | | securityhub-s3-bucket-public-write-prohibited | Done | Done | 38 | | securityhub-vpc-default-security-group-closed | Done | Done | 39 | | securityhub-vpc-flow-logs-enabled | Done | No Moto support | 40 | 41 | ## AWS Config Managed Rules 42 | 43 | Development coverage: **1 of 40** 44 | 45 | Test coverage: **0 of 40** 46 | 47 | | Rule | Priority | Development Status | Testing Status | 48 | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------ | --------------- | 49 | | [access-keys-rotated](https://docs.aws.amazon.com/config/latest/developerguide/access-keys-rotated.html) | | Security Hub | N/A | 50 | | [acm-certificate-expiration-check](https://docs.aws.amazon.com/config/latest/developerguide/acm-certificate-expiration-check.html) | | | | 51 | | [approved-amis-by-id](https://docs.aws.amazon.com/config/latest/developerguide/approved-amis-by-id.html) | | | | 52 | | [approved-amis-by-tag](https://docs.aws.amazon.com/config/latest/developerguide/approved-amis-by-tag.html) | | | | 53 | | [autoscaling-group-elb-healthcheck-required](https://docs.aws.amazon.com/config/latest/developerguide/autoscaling-group-elb-healthcheck-required.html) | | | | 54 | | [cloud-trail-cloud-watch-logs-enabled](https://docs.aws.amazon.com/config/latest/developerguide/cloud-trail-cloud-watch-logs-enabled.html) | | Security Hub | N/A | 55 | | [cloud-trail-encryption-enabled](https://docs.aws.amazon.com/config/latest/developerguide/cloud-trail-encryption-enabled.html) | | Security Hub | N/A | 56 | | [cloud-trail-log-file-validation-enabled](https://docs.aws.amazon.com/config/latest/developerguide/cloud-trail-log-file-validation-enabled.html) | | | | 57 | | [cloudformation-stack-drift-detection-check](https://docs.aws.amazon.com/config/latest/developerguide/cloudformation-stack-drift-detection-check.html) | | | | 58 | | [cloudformation-stack-notification-check](https://docs.aws.amazon.com/config/latest/developerguide/cloudformation-stack-notification-check.html) | | | | 59 | | [cloudtrail-enabled](https://docs.aws.amazon.com/config/latest/developerguide/cloudtrail-enabled.html) | 1 | | | 60 | | [cloudwatch-alarm-action-check](https://docs.aws.amazon.com/config/latest/developerguide/cloudwatch-alarm-action-check.html) | | | | 61 | | [cloudwatch-alarm-resource-check](https://docs.aws.amazon.com/config/latest/developerguide/cloudwatch-alarm-resource-check.html) | | | | 62 | | [cloudwatch-alarm-settings-check](https://docs.aws.amazon.com/config/latest/developerguide/cloudwatch-alarm-settings-check.html) | | | | 63 | | [cmk-backing-key-rotation-enabled](https://docs.aws.amazon.com/config/latest/developerguide/cmk-backing-key-rotation-enabled.html) | | Security Hub | N/A | 64 | | [codebuild-project-envvar-awscred-check](https://docs.aws.amazon.com/config/latest/developerguide/codebuild-project-envvar-awscred-check.html) | | | | 65 | | [codebuild-project-source-repo-url-check](https://docs.aws.amazon.com/config/latest/developerguide/codebuild-project-source-repo-url-check.html) | | | | 66 | | [codepipeline-deployment-count-check](https://docs.aws.amazon.com/config/latest/developerguide/codepipeline-deployment-count-check.html) | | | | 67 | | [codepipeline-region-fanout-check](https://docs.aws.amazon.com/config/latest/developerguide/codepipeline-region-fanout-check.html) | | | | 68 | | [db-instance-backup-enabled](https://docs.aws.amazon.com/config/latest/developerguide/db-instance-backup-enabled.html) | 1 | | | 69 | | [desired-instance-tenancy](https://docs.aws.amazon.com/config/latest/developerguide/desired-instance-tenancy.html) | | | | 70 | | [desired-instance-type](https://docs.aws.amazon.com/config/latest/developerguide/desired-instance-type.html) | | | | 71 | | [dynamodb-autoscaling-enabled](https://docs.aws.amazon.com/config/latest/developerguide/dynamodb-autoscaling-enabled.html) | 2 | | | 72 | | [dynamodb-table-encryption-enabled](https://docs.aws.amazon.com/config/latest/developerguide/dynamodb-table-encryption-enabled.html) | 1 | | | 73 | | [dynamodb-throughput-limit-check](https://docs.aws.amazon.com/config/latest/developerguide/dynamodb-throughput-limit-check.html) | | | | 74 | | [ebs-optimized-instance](https://docs.aws.amazon.com/config/latest/developerguide/ebs-optimized-instance.html) | | | | 75 | | [ec2-instance-detailed-monitoring-enabled](https://docs.aws.amazon.com/config/latest/developerguide/ec2-instance-detailed-monitoring-enabled.html) | 2 | | | 76 | | [ec2-instance-managed-by-systems-manager](https://docs.aws.amazon.com/config/latest/developerguide/ec2-instance-managed-by-systems-manager.html) | | | | 77 | | [ec2-instances-in-vpc](https://docs.aws.amazon.com/config/latest/developerguide/ec2-instances-in-vpc.html) | 1 | | | 78 | | [ec2-managedinstance-applications-blacklisted](https://docs.aws.amazon.com/config/latest/developerguide/ec2-managedinstance-applications-blacklisted.html) | | | | 79 | | [ec2-managedinstance-applications-required](https://docs.aws.amazon.com/config/latest/developerguide/ec2-managedinstance-applications-required.html) | | | | 80 | | [ec2-managedinstance-association-compliance-status-check](https://docs.aws.amazon.com/config/latest/developerguide/ec2-managedinstance-association-compliance-status-check.html) | | | | 81 | | [ec2-managedinstance-inventory-blacklisted](https://docs.aws.amazon.com/config/latest/developerguide/ec2-managedinstance-inventory-blacklisted.html) | | | | 82 | | [ec2-managedinstance-patch-compliance-status-check](https://docs.aws.amazon.com/config/latest/developerguide/ec2-managedinstance-patch-compliance-status-check.html) | | | | 83 | | [ec2-managedinstance-platform-check](https://docs.aws.amazon.com/config/latest/developerguide/ec2-managedinstance-platform-check.html) | | | | 84 | | [ec2-volume-inuse-check](https://docs.aws.amazon.com/config/latest/developerguide/ec2-volume-inuse-check.html) | 2 | | | 85 | | [eip-attached](https://docs.aws.amazon.com/config/latest/developerguide/eip-attached.html) | 2 | | | 86 | | [elb-acm-certificate-required](https://docs.aws.amazon.com/config/latest/developerguide/elb-acm-certificate-required.html) | | | | 87 | | [elb-custom-security-policy-ssl-check](https://docs.aws.amazon.com/config/latest/developerguide/elb-custom-security-policy-ssl-check.html) | | | | 88 | | [elb-logging-enabled](https://docs.aws.amazon.com/config/latest/developerguide/elb-logging-enabled.html) | 2 | | | 89 | | [elb-predefined-security-policy-ssl-check](https://docs.aws.amazon.com/config/latest/developerguide/elb-predefined-security-policy-ssl-check.html) | | | | 90 | | [encrypted-volumes](https://docs.aws.amazon.com/config/latest/developerguide/encrypted-volumes.html) | 1 | Not feasible | N/A | 91 | | [fms-shield-resource-policy-check](https://docs.aws.amazon.com/config/latest/developerguide/fms-shield-resource-policy-check.html) | | | | 92 | | [fms-webacl-resource-policy-check](https://docs.aws.amazon.com/config/latest/developerguide/fms-webacl-resource-policy-check.html) | | | | 93 | | [fms-webacl-rulegroup-association-check](https://docs.aws.amazon.com/config/latest/developerguide/fms-webacl-rulegroup-association-check.html) | | | | 94 | | [guardduty-enabled-centralized](https://docs.aws.amazon.com/config/latest/developerguide/guardduty-enabled-centralized.html) | 1 | | | 95 | | [iam-group-has-users-check](https://docs.aws.amazon.com/config/latest/developerguide/iam-group-has-users-check.html) | | | | 96 | | [iam-password-policy](https://docs.aws.amazon.com/config/latest/developerguide/iam-password-policy.html) | | Security Hub | N/A | 97 | | [iam-policy-blacklisted-check](https://docs.aws.amazon.com/config/latest/developerguide/iam-policy-blacklisted-check.html) | | | | 98 | | [iam-policy-no-statements-with-admin-access](https://docs.aws.amazon.com/config/latest/developerguide/iam-policy-no-statements-with-admin-access.html) | | Security Hub | N/A | 99 | | [iam-role-managed-policy-check](https://docs.aws.amazon.com/config/latest/developerguide/iam-role-managed-policy-check.html) | | | | 100 | | [iam-root-access-key-check](https://docs.aws.amazon.com/config/latest/developerguide/iam-root-access-key-check.html) | | Security Hub | N/A | 101 | | [iam-user-group-membership-check](https://docs.aws.amazon.com/config/latest/developerguide/iam-user-group-membership-check.html) | | | | 102 | | [iam-user-mfa-enabled](https://docs.aws.amazon.com/config/latest/developerguide/iam-user-mfa-enabled.html) | | | | 103 | | [iam-user-no-policies-check](https://docs.aws.amazon.com/config/latest/developerguide/iam-user-no-policies-check.html) | | Security Hub | N/A | 104 | | [iam-user-unused-credentials-check](https://docs.aws.amazon.com/config/latest/developerguide/iam-user-unused-credentials-check.html) | | Security Hub | N/A | 105 | | [lambda-function-public-access-prohibited](https://docs.aws.amazon.com/config/latest/developerguide/lambda-function-public-access-prohibited.html) | 1 | | | 106 | | [lambda-function-settings-check](https://docs.aws.amazon.com/config/latest/developerguide/lambda-function-settings-check.html) | | | | 107 | | [mfa-enabled-for-iam-console-access](https://docs.aws.amazon.com/config/latest/developerguide/mfa-enabled-for-iam-console-access.html) | | Security Hub | N/A | 108 | | [multi-region-cloud-trail-enabled](https://docs.aws.amazon.com/config/latest/developerguide/multi-region-cloud-trail-enabled.html) | | Security Hub | N/A | 109 | | [rds-instance-public-access-check](https://docs.aws.amazon.com/config/latest/developerguide/rds-instance-public-access-check.html) | 1 | Done | No Moto support | 110 | | [rds-multi-az-support](https://docs.aws.amazon.com/config/latest/developerguide/rds-multi-az-support.html) | 1 | | | 111 | | [rds-snapshots-public-prohibited](https://docs.aws.amazon.com/config/latest/developerguide/rds-snapshots-public-prohibited.html) | 1 | | | 112 | | [rds-storage-encrypted](https://docs.aws.amazon.com/config/latest/developerguide/rds-storage-encrypted.html) | 1 | | | 113 | | [redshift-cluster-configuration-check](https://docs.aws.amazon.com/config/latest/developerguide/redshift-cluster-configuration-check.html) | | | | 114 | | [redshift-cluster-maintenancesettings-check](https://docs.aws.amazon.com/config/latest/developerguide/redshift-cluster-maintenancesettings-check.html) | | | | 115 | | [required-tags](https://docs.aws.amazon.com/config/latest/developerguide/required-tags.html) | | | | 116 | | [restricted-common-ports](https://docs.aws.amazon.com/config/latest/developerguide/restricted-common-ports.html) | | | | 117 | | [restricted-ssh](https://docs.aws.amazon.com/config/latest/developerguide/restricted-ssh.html) | | Security Hub | N/A | 118 | | [root-account-hardware-mfa-enabled](https://docs.aws.amazon.com/config/latest/developerguide/root-account-hardware-mfa-enabled.html) | | Security Hub | N/A | 119 | | [root-account-mfa-enabled](https://docs.aws.amazon.com/config/latest/developerguide/root-account-mfa-enabled.html) | | Security Hub | N/A | 120 | | [s3-blacklisted-actions-prohibited](https://docs.aws.amazon.com/config/latest/developerguide/s3-blacklisted-actions-prohibited.html) | | | | 121 | | [s3-bucket-logging-enabled](https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-logging-enabled.html) | | Security Hub | N/A | 122 | | [s3-bucket-policy-grantee-check](https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-policy-grantee-check.html) | | | | 123 | | [s3-bucket-policy-not-more-permissive](https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-policy-not-more-permissive.html) | | | | 124 | | [s3-bucket-public-read-prohibited](https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-public-read-prohibited.html) | | Security Hub | N/A | 125 | | [s3-bucket-public-write-prohibited](https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-public-write-prohibited.html) | | Security Hub | N/A | 126 | | [s3-bucket-replication-enabled](https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-replication-enabled.html) | | | | 127 | | [s3-bucket-server-side-encryption-enabled](https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-server-side-encryption-enabled.html) | 1 | Done | No Moto support | 128 | | [s3-bucket-ssl-requests-only](https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-ssl-requests-only.html) | 1 | Done | No Moto support | 129 | | [s3-bucket-versioning-enabled](https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-versioning-enabled.html) | | | | 130 | | [vpc-default-security-group-closed](https://docs.aws.amazon.com/config/latest/developerguide/vpc-default-security-group-closed.html) | | Security Hub | N/A | 131 | | [vpc-flow-logs-enabled](https://docs.aws.amazon.com/config/latest/developerguide/vpc-flow-logs-enabled.html) | | Security Hub | N/A | 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Auto Remediate 2 | 3 | [![Build Status](https://travis-ci.org/servian/aws-auto-remediate.svg?branch=master)](https://travis-ci.org/servian/aws-auto-remediate) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/5bce55175d32494c89f0648b27719f43)](https://www.codacy.com/app/servian/aws-auto-remediate?utm_source=github.com&utm_medium=referral&utm_content=servian/aws-auto-remediate&utm_campaign=Badge_Grade) [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/5bce55175d32494c89f0648b27719f43)](https://www.codacy.com/app/servian/aws-auto-remediate?utm_source=github.com&utm_medium=referral&utm_content=servian/aws-auto-remediate&utm_campaign=Badge_Coverage) 4 | 5 | ![Release](https://img.shields.io/github/release/servian/aws-auto-remediate.svg) ![Pre-release Date](https://img.shields.io/github/release-date-pre/servian/aws-auto-remediate.svg) 6 | 7 | ![Language](https://img.shields.io/github/languages/top/servian/aws-auto-remediate.svg) [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) [![Python Black](https://img.shields.io/badge/code%20style-black-000000.svg?label=Python%20code%20style)](https://github.com/python/black) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?label=Markdown%2FYAML%20code%20style)](https://github.com/prettier/prettier) 8 | 9 | Open source application to instantly remediate common security issues through the use of AWS Config. 10 | 11 | ![auto-remediate](images/auto-remediate.svg) 12 | 13 | ## Table of Contents 14 | 15 | - [About](#about) 16 | - [Setup](#setup) 17 | - [Deployment](#deployment) 18 | - [Update](#update) 19 | - [Removal](#removal) 20 | - [Settings](#settings) 21 | - [Config Rules](#config-rules) 22 | - [AWS Config Managed Rules](#aws-config-managed-rules) 23 | - [AWS Security Hub Rules](#aws-security-hub-rules) 24 | - [Resources](#resources) 25 | - [Coverage](#coverage) 26 | - [Contributing](CONTRIBUTING.md) 27 | 28 | ## About 29 | 30 | ### Auto Remediate 31 | 32 | The Auto Remediate function is triggered via an SQS Queue `auto-remediate-config-compliance`. The SQS Queue is populated with a compliance payload from AWS Config via a CloudWatch Event `auto-remediate-config-compliance`. The purpose of the CloudWatch Event is to filter out all non-compliance related messages that AWS Config generates. 33 | 34 | Once the Lambda function has been triggered it will attempt to remediate the security concern. If the remediation was unsuccessful, the event payload will be sent to the dead letter queue (DQL) SQS Queue `auto-remediate-dlq`. Each time a payload is sent is sent to the DLQ, an attribute `try_count` is incremented to the SQS message. Once that count exceeds `RETRYCOUNT` variable attached to the Lambda Function, the message will no longer be sent to the DLQ. 35 | 36 | If no remediation exists for the incoming AWS Config event, the AWS Config payload will be sent to an SNS Topic `auto-remediate-missing-remediation` which can be subscribed to by administrators or other AWS services. 37 | 38 | ### Auto Remediate DLQ 39 | 40 | The Auto Remediate DLQ function is triggered on a schedule (defined in the `serverless.yml` file). When the function is run, it will retrieve messages from SQS Queue `auto-remediate-dlq` and sends the message to the compliance SQS Queue `auto-remediate-config-compliance`. 41 | 42 | ### Auto Remediate Setup 43 | 44 | The Auto Remediate Setup function is triggered manually by the user. The purpose of this function is to invoke CloudFormation Stacks for each of the AWS Config Rules that will monitor for security issues as well as create/insert records into the DynamoDB settings table used to control the actions of the Auto Remediate function. 45 | 46 | ## Setup 47 | 48 | ### New Account 49 | 50 | Proceed to the [Deployment](#deployment) section below. 51 | 52 | ### Existing Account 53 | 54 | Auto Remediate utilises the compliance event triggers made by AWS Config. Due to the fact that AWS Config will trigger a compliance event **only** when the compliance status of a resource changes state (i.e., COMPLIANT to NON_COMPLIANT or vice versa) it is advised that you **disable** the `CIS AWS Foundations` compliance standards within AWS Security Hub (and ensure all AWS Config rules starting with `securityhub` are removed from your account) before proceeding. 55 | 56 | Once AWS Config is cleared of all AWS Security Hub related rules, you may proceed to deploy Auto Remediate and enable the `CIS AWS Foundations` compliance standards within AWS Security Hub. 57 | 58 | ### Deployment 59 | 60 | 1. Install the [Serverless Framework](https://serverless.com/) 61 | 62 | ```bash 63 | npm install serverless --global 64 | ``` 65 | 66 | 2. Install [AWS CLI](https://aws.amazon.com/cli/) 67 | 68 | ```bash 69 | pip3 install awscli --upgrade --user 70 | ``` 71 | 72 | 3. Configure the AWS CLI following the instruction at [Quickly Configuring the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html#cli-quick-configuration). Ensure the user you're configuring has the appropriate IAM permissions to create Lambda Functions, S3 Buckets, IAM Roles, and CloudFormation Stacks. It is best for administrators to deploy Auto Remediate. 73 | 74 | 4. Install Auto Remediate 75 | 76 | ```bash 77 | serverless create --template-url https://github.com/servian/aws-auto-remediate --path aws-auto-remediate 78 | ``` 79 | 80 | 5. Change into the Auto Remediate directory 81 | 82 | ```bash 83 | cd aws-auto-remediate 84 | ``` 85 | 86 | 8. Install Serverless plugins needed for deployment 87 | 88 | ```bash 89 | serverless plugin install --name serverless-python-requirements 90 | ``` 91 | 92 | ```bash 93 | npm install serverless-iam-roles-per-function 94 | ``` 95 | 96 | 9. Deploy Auto Remediate to your AWS account 97 | 98 | ```bash 99 | serverless deploy [--region ] [--aws-profile ] 100 | ``` 101 | 102 | 10. Invoke Auto Remediate Setup for the first time to create the necessary AWS Config rules and settings 103 | 104 | ```bash 105 | serverless invoke -f AutoRemediateSetup [--region ] [--aws-profile ] 106 | ``` 107 | 108 | 11. Check Auto Remediate Setup logs 109 | 110 | ```bash 111 | serverless logs -f AutoRemediateSetup [--region ] [--aws-profile ] 112 | ``` 113 | 114 | ### Update 115 | 116 | 1. Remove existing Auto Remediate directory 117 | 118 | 2. Install Auto Remediate 119 | 120 | ```bash 121 | serverless create --template-url https://github.com/servian/aws-auto-remediate --path aws-auto-remediate 122 | ``` 123 | 124 | 3. Deploy Auto Remediate update to your AWS account 125 | 126 | ```bash 127 | serverless deploy [--region ] [--aws-profile ] 128 | ``` 129 | 130 | 4. Invoke Auto Remediate Setup to deploy new AWS Config rules and settings 131 | 132 | ```bash 133 | serverless invoke --function AutoRemediateSetup [--region ] [--aws-profile ] 134 | ``` 135 | 136 | ### Removal 137 | 138 | Auto Remediate is deployed using the Serverless Framework which under the hood creates an AWS CloudFormation Stack allowing for a clean and simple removal process. 139 | 140 | To remove Auto Remediate from your AWS account, follow the below steps: 141 | 142 | 1. Change into the Auto Remediate directory 143 | 144 | ```bash 145 | cd aws-auto-remediate 146 | ``` 147 | 148 | 2. Remove Auto Remediate from your AWS account 149 | 150 | ```bash 151 | serverless remove [--region ] [--aws-profile ] 152 | ``` 153 | 154 | ## Settings 155 | 156 | Auto Remediate uses a DynamoDB settings table `auto-remediate-settings` that allows the user to control which rule should be remediated by the tool. Once Auto Remediate Setup has been run, head on over to DynamoDB and inspect the `rules` key where you can then set the `remediate` key to `false` if you'd like to disable automatic remediate. 157 | 158 | For rules deployed by Auto Remediate Setup (e.g., `auto-remediate-rds-instance-public-access-check`) an extra key `deploy` can be found in the settings table. Although not functional at the moment, this will allow users to control which Auto Remediate deployed rules should be deployed and which should be skipped. 159 | 160 | ## Config Rules 161 | 162 | The tables below detail the auto remediated rules and scenarios. 163 | 164 | :warning: All remediations tagged with a warning symbol may break existing functionality. 165 | 166 | ### AWS Config Managed Rules 167 | 168 | #### Database 169 | 170 | | Rule | Description | Remediation | 171 | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | 172 | | RDS Instance Public Access Check | Check whether the Amazon Relational Database Service instances are not publicly accessible.
The rule is NON_COMPLIANT if the `publiclyAccessible` field is true in the instance configuration item. | :warning: Sets `publiclyAccessible` field to `False` | 173 | 174 | #### Storage 175 | 176 | | Rule | Description | Remediation | 177 | | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | 178 | | S3 Bucket Service Side Encryption Enabled | Checks that your Amazon S3 bucket either has Amazon S3 default encryption enabled or that the S3 bucket policy explicitly denies `put-object` requests without server side encryption. | Enables SSE | 179 | | S3 Bucket SSL Requests Only | Checks whether S3 buckets have policies that require requests to use Secure Socket Layer (SSL). | Adds Bucket Policy to force SSL only connections | 180 | 181 | ### AWS Security Hub Rules 182 | 183 | #### Compute 184 | 185 | | Rule | Description | Remediation | 186 | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | 187 | | Restricted RDP | Checks whether the incoming RDP traffic is allowed from `0.0.0.0/0` or `::/0`. This rule is compliant when incoming RDP traffic is restricted. | :warning: Deletes offending inbound rule | 188 | | Restricted SSH | Checks whether the incoming SSH traffic is allowed from `0.0.0.0/0` or `::/0`. This rule is compliant when incoming SSH traffic is restricted. | :warning: Deletes offending inbound rule | 189 | 190 | #### Management and Governance 191 | 192 | | Rule | Description | Remediation | 193 | | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | 194 | | CloudTrail CloudWatch Logs Enabled | Checks whether AWS CloudTrail trails are configured to send logs to Amazon CloudWatch logs. | Enables CloudWatch logs to Log Group `cloudtrail/` | 195 | | CloudTrail Encryption Enabled | Ensure CloudTrail logs are encrypted at rest using KMS CMKs. | Enables CloudWatch encryption with KMS CMK `cloudtrail/` | 196 | | CloudTrail Log File Validation Enabled | Checks whether AWS CloudTrail creates a signed digest file with logs. AWS recommends that the file validation must be enabled on all trails. The rule is NON_COMPLIANT if the validation is not enabled. | Enables CloudTrail Validation | 197 | | Multi Region Cloud Trail Enabled | Checks that there is at least one multi-region AWS CloudTrail. The rule is NON_COMPLIANT if the trails do not match inputs parameters. | Enables Multi Region CloudTrail | 198 | 199 | #### Network and Content Delivery 200 | 201 | | Rule | Description | Remediation | 202 | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | 203 | | VPC Flow Logs Enables | Checks whether Amazon Virtual Private Cloud flow logs are found and enabled for Amazon VPC. | Creates new S3 Bucket `--flow-logs` for logging with a prefix of `/` | 204 | | VPC Default Security Group Closed | Checks that the default security group of any Amazon Virtual Private Cloud (VPC) does not allow inbound or outbound traffic. The rule is NON_COMPLIANT if the default security group has one or more inbound or outbound traffic. | Deletes all egress and ingress rules | 205 | 206 | #### Security, Identity & Compliance 207 | 208 | | Rule | Description | Remediation | 209 | | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 210 | | Access Keys Rotated | Checks whether the active access keys are rotated within the number of days specified in 90 days. | :warning: Deletes Access Key | 211 | | Customer Managed Key Rotation Enabled | Checks that key rotation is enabled for customer created customer master key (CMK). | Enables key rotation | 212 | | IAM Password Policy Ensure Expires | Checks whether the IAM password policy ensures that passwords expire. | Enables password expiration | 213 | | IAM Password Policy Lowercase Letter Check | Checks whether the IAM password policy enforces the inclusion of a lowercase letter. | Enables "Require at least one lowercase letter" option | 214 | | IAM Password Policy Minimum Length Check | Checks whether the IAM password policy enforces a minimum length. | Sets minimum password length to 14. | 215 | | IAM Password Policy Number Check | Checks whether the IAM password policy enforces the inclusion of a number. | Enables "Require at least one number" option | 216 | | IAM Password Policy Prevent Reuse Check | Checks whether the IAM password policy prevents password reuse. | Sets number of passwords to remember to 24. | 217 | | IAM Password Policy Symbol Check | Checks whether the IAM password policy enforces the inclusion of a symbol. | Enables "Require at least one non-alphanumeric character" option | 218 | | IAM Password Policy Uppercase Letter Check | Checks whether the account password policy for IAM users requires at least one uppercase character in password. | Enables "Require at least one uppercase letter" option | 219 | | IAM Policy No Statements with Admin Access | Checks whether the default version of AWS Identity and Access Management (IAM) policies do not have administrator access.
If any statement has `"Effect": "Allow"` with `"Action": "*"` over `"Resource": "*"`, the rule is NON_COMPLIANT. | :warning: Creates new Policy with offending Statements removed | 220 | | IAM User No Policies Check | Checks that none of your IAM users have policies attached. IAM users must inherit permissions from IAM groups or roles. | Detaches Managed Policies from offending IAM User | 221 | | IAM User Unused Credentials Check | Checks whether AWS Identity and Access Management (IAM) users have passwords or active access keys that have not been used within 90 days. | :warning: Deletes Access Key / Login Profile | 222 | | MFA Enabled for IAM Console Access | Checks whether AWS Multi-Factor Authentication (MFA) is enabled for all AWS Identity and Access Management (IAM) users that use a console password. | :warning: Deletes user's Login Profile only. [Deleting a user's password does not prevent a user from accessing AWS through the command line interface or the API.](https://docs.aws.amazon.com/cli/latest/reference/iam/delete-login-profile.html) | 223 | 224 | #### Storage 225 | 226 | | Rule | Description | Remediation | 227 | | --------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | 228 | | S3 Bucket Logging Enabled | Checks whether logging is enabled for your S3 buckets. | Creates new S3 Bucket `--access-logs` for logging with a prefix of `/` | 229 | | S3 Bucket Public Read Prohibited | Checks to see if S3 buckets are publicly readable. | :warning: Sets S3 Bucket ACL to `private` | 230 | | S3 Bucket Public Write Prohibited | Checks to see if S3 buckets allow public write. | :warning: Sets S3 Bucket ACL to `private` | 231 | 232 | ## Resources 233 | 234 | The table below details all AWS resources created when deploying the application. 235 | 236 | | Service | Resource ID | 237 | | --------------------- | ---------------------------------------------------------------------------------------------------- | 238 | | CloudFormation Stack | `auto-remediate` | 239 | | CloudWatch Event Rule | `auto-remediate-config-compliance` | 240 | | DynamoDB Table | `auto-remediate-settings` | 241 | | Lambda Function | `auto-remediate` | 242 | | | `auto-remediate-dlq` | 243 | | | `auto-remediate-setup` | 244 | | SNS Topic | `auto-remediate-log` (not functional [#19](https://github.com/servian/aws-auto-remediate/issues/19)) | 245 | | | `auto-remediate-missing-remediation` | 246 | | SQS Queue | `auto-remediate-config-compliance` | 247 | | | `auto-remediate-dlq` | 248 | 249 | ## Coverage 250 | 251 | [Full list of development and automated testing coverage found here.](COVERAGE.md) 252 | -------------------------------------------------------------------------------- /auto_remediate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlevit/aws-auto-remediate/949adb55809a122278a55c6503e19c9032b378ca/auto_remediate/__init__.py -------------------------------------------------------------------------------- /auto_remediate/config_rules.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | 8 | class ConfigRules: 9 | def __init__(self, logging): 10 | self.logging = logging 11 | 12 | self._client_rds = None 13 | self._client_s3 = None 14 | self._client_sts = None 15 | 16 | @property 17 | def client_rds(self): 18 | if not self._client_rds: 19 | self._client_rds = boto3.client("rds") 20 | return self._client_rds 21 | 22 | @property 23 | def client_s3(self): 24 | if not self._client_s3: 25 | self._client_s3 = boto3.client("s3") 26 | return self._client_s3 27 | 28 | @property 29 | def client_sts(self): 30 | if not self._client_sts: 31 | self._client_sts = boto3.client("sts") 32 | return self._client_sts 33 | 34 | @property 35 | def account_number(self): 36 | return self.client_sts.get_caller_identity()["Account"] 37 | 38 | @property 39 | def account_arn(self): 40 | return self.client_sts.get_caller_identity()["Arn"] 41 | 42 | @property 43 | def region(self): 44 | if self.client_sts.meta.region_name != "aws-global": 45 | return self.client_sts.meta.region_name 46 | else: 47 | return "us-east-1" 48 | 49 | def rds_instance_public_access_check(self, resource_id): 50 | """Sets Publicly Accessible option to False for public RDS Instances 51 | 52 | Arguments: 53 | resource_id {DbiResourceId} -- The AWS Region-unique, immutable identifier for the DB instance 54 | 55 | Returns: 56 | boolean -- True if remediation was successful 57 | """ 58 | try: 59 | paginator = self.client_rds.get_paginator("describe_db_instances") 60 | response = paginator.paginate(DBInstanceIdentifier=resource_id) 61 | except: 62 | self.logging.error("Could not describe RDS DB Instances.") 63 | return False 64 | else: 65 | for instance in response["DBInstances"]: 66 | try: 67 | self.client_rds.modify_db_instance( 68 | DBInstanceIdentifier=instance["DBInstanceIdentifier"], 69 | PubliclyAccessible=False, 70 | ) 71 | self.logging.info( 72 | f"Disabled Public Accessibility for RDS Instance '{resource_id}'." 73 | ) 74 | return True 75 | except: 76 | self.logging.error( 77 | f"Could not disable Public Accessibility for RDS Instance '{resource_id}'." 78 | ) 79 | self.logging.error(sys.exc_info()[1]) 80 | return False 81 | 82 | def s3_bucket_server_side_encryption_enabled(self, resource_id): 83 | """Enables Server-side Encryption for an S3 Bucket 84 | 85 | Arguments: 86 | resource_id {string} -- S3 Bucket name 87 | 88 | Returns: 89 | boolean -- True if remediation is successful 90 | """ 91 | try: 92 | self.client_s3.put_bucket_encryption( 93 | Bucket=resource_id, 94 | ServerSideEncryptionConfiguration={ 95 | "Rules": [ 96 | { 97 | "ApplyServerSideEncryptionByDefault": { 98 | "SSEAlgorithm": "AES256" 99 | } 100 | } 101 | ] 102 | }, 103 | ) 104 | self.logging.info( 105 | f"Enabled Server-side Encryption for S3 Bucket '{resource_id}'." 106 | ) 107 | return True 108 | except: 109 | self.logging.info( 110 | f"Could not enable Server-side Encryption for S3 Bucket '{resource_id}'." 111 | ) 112 | self.logging.error(sys.exc_info()[1]) 113 | return False 114 | 115 | def s3_bucket_ssl_requests_only(self, resource_id): 116 | """Adds Bucket Policy to force SSL only connections 117 | 118 | Arguments: 119 | resource_id {string} -- S3 Bucket name 120 | 121 | Returns: 122 | boolean -- True if remediation was successful 123 | """ 124 | 125 | # get SSL policy 126 | policy_file = "auto_remediate/data/s3_bucket_ssl_requests_only_policy.json" 127 | with open(policy_file, "r") as file: 128 | policy = file.read() 129 | 130 | policy = json.loads(policy.replace("_BUCKET_", resource_id)) 131 | 132 | try: 133 | response = self.client_s3.get_bucket_policy(Bucket=resource_id) 134 | except ClientError as error: 135 | if error.response["Error"]["Code"] == "NoSuchBucketPolicy": 136 | return self.set_bucket_policy(resource_id, json.dumps(policy)) 137 | else: 138 | self.logging.error( 139 | f"Could not set SSL requests only policy to S3 Bucket '{resource_id}'." 140 | ) 141 | self.logging.error(sys.exc_info()[1]) 142 | return False 143 | except: 144 | self.logging.error( 145 | f"Could not retrieve existing policy to S3 Bucket '{resource_id}'." 146 | ) 147 | else: 148 | existing_policy = json.loads(response["Policy"]) 149 | existing_policy["Statement"].append(policy["Statement"][0]) 150 | 151 | return self.set_bucket_policy(resource_id, json.dumps(existing_policy)) 152 | 153 | def set_bucket_policy(self, bucket, policy): 154 | """Attempts to set an S3 Bucket Policy. If returned error is Access Denied, 155 | then the Public Access Block is removed before placing a new S3 Bucket Policy 156 | 157 | Arguments: 158 | bucket {string} -- S3 Bucket Name 159 | policy {string} -- S3 Bucket Policy 160 | 161 | Returns: 162 | boolean -- True if S3 Bucket Policy was set 163 | """ 164 | try: 165 | self.client_s3.put_bucket_policy(Bucket=bucket, Policy=policy) 166 | self.logging.info(f"Set SSL requests only policy to S3 Bucket '{bucket}'.") 167 | return True 168 | except ClientError as error: 169 | if error.response["Error"]["Code"] == "AccessDenied": 170 | try: 171 | # disable Public Access Block 172 | self.client_s3.put_public_access_block( 173 | Bucket=bucket, 174 | PublicAccessBlockConfiguration={ 175 | "BlockPublicPolicy": False, 176 | "RestrictPublicBuckets": False, 177 | }, 178 | ) 179 | 180 | # put Bucket Policy 181 | self.client_s3.put_bucket_policy(Bucket=bucket, Policy=policy) 182 | 183 | # enable Public Access Block 184 | self.client_s3.put_public_access_block( 185 | Bucket=bucket, 186 | PublicAccessBlockConfiguration={ 187 | "BlockPublicPolicy": True, 188 | "RestrictPublicBuckets": True, 189 | }, 190 | ) 191 | 192 | self.logging.info( 193 | f"Set SSL requests only policy to S3 Bucket '{bucket}'." 194 | ) 195 | return True 196 | except: 197 | self.logging.error( 198 | f"Could not set SSL requests only policy to S3 Bucket '{bucket}'." 199 | ) 200 | self.logging.error(sys.exc_info()[1]) 201 | return False 202 | else: 203 | self.logging.error( 204 | f"Could not set SSL requests only policy to S3 Bucket '{bucket}'." 205 | ) 206 | self.logging.error(sys.exc_info()[1]) 207 | return False 208 | except: 209 | self.logging.error( 210 | f"Could not set SSL requests only policy to S3 Bucket '{bucket}'." 211 | ) 212 | self.logging.error(sys.exc_info()[1]) 213 | return False 214 | -------------------------------------------------------------------------------- /auto_remediate/custom_rules.py: -------------------------------------------------------------------------------- 1 | # import boto3 2 | 3 | 4 | class CustomRules: 5 | def __init__(self, logging): 6 | self.logging = logging 7 | -------------------------------------------------------------------------------- /auto_remediate/data/cloud_trail_cloud_watch_logs_enabled_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "AWSCloudTrailCreateLogStream20141101", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "logs:CreateLogStream" 9 | ], 10 | "Resource": [ 11 | "arn:aws:logs:_REGION_:_ACCOUNT_NUMBER_:log-group:_LOG_GROUP_:log-stream:_ACCOUNT_NUMBER__CloudTrail__REGION_*" 12 | ] 13 | }, 14 | { 15 | "Sid": "AWSCloudTrailPutLogEvents20141101", 16 | "Effect": "Allow", 17 | "Action": [ 18 | "logs:PutLogEvents" 19 | ], 20 | "Resource": [ 21 | "arn:aws:logs:_REGION_:_ACCOUNT_NUMBER_:log-group:_LOG_GROUP_:log-stream:_ACCOUNT_NUMBER__CloudTrail__REGION_*" 22 | ] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /auto_remediate/data/cloud_trail_cloud_watch_logs_enabled_trust_relationship.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "", 6 | "Effect": "Allow", 7 | "Principal": { 8 | "Service": "cloudtrail.amazonaws.com" 9 | }, 10 | "Action": "sts:AssumeRole" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /auto_remediate/data/cloud_trail_encryption_enabled_kms_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Id": "Key policy created by Auto Remediate", 4 | "Statement": [ 5 | { 6 | "Sid": "Enable IAM User Permissions", 7 | "Effect": "Allow", 8 | "Principal": { 9 | "AWS": [ 10 | "_ACCOUNT_ARN_", 11 | "arn:aws:iam::_ACCOUNT_NUMBER_:root" 12 | ] 13 | }, 14 | "Action": "kms:*", 15 | "Resource": "*" 16 | }, 17 | { 18 | "Sid": "Allow CloudTrail to encrypt logs", 19 | "Effect": "Allow", 20 | "Principal": { 21 | "Service": "cloudtrail.amazonaws.com" 22 | }, 23 | "Action": "kms:GenerateDataKey*", 24 | "Resource": "*", 25 | "Condition": { 26 | "StringLike": { 27 | "kms:EncryptionContext:aws:cloudtrail:arn": "arn:aws:cloudtrail:*:_ACCOUNT_NUMBER_:trail/*" 28 | } 29 | } 30 | }, 31 | { 32 | "Sid": "Allow CloudTrail to describe key", 33 | "Effect": "Allow", 34 | "Principal": { 35 | "Service": "cloudtrail.amazonaws.com" 36 | }, 37 | "Action": "kms:DescribeKey", 38 | "Resource": "*" 39 | }, 40 | { 41 | "Sid": "Allow principals in the account to decrypt log files", 42 | "Effect": "Allow", 43 | "Principal": { 44 | "AWS": "*" 45 | }, 46 | "Action": [ 47 | "kms:Decrypt", 48 | "kms:ReEncryptFrom" 49 | ], 50 | "Resource": "*", 51 | "Condition": { 52 | "StringEquals": { 53 | "kms:CallerAccount": "_ACCOUNT_NUMBER_" 54 | }, 55 | "StringLike": { 56 | "kms:EncryptionContext:aws:cloudtrail:arn": "arn:aws:cloudtrail:*:_ACCOUNT_NUMBER_:trail/*" 57 | } 58 | } 59 | }, 60 | { 61 | "Sid": "Enable cross account log decryption", 62 | "Effect": "Allow", 63 | "Principal": { 64 | "AWS": "*" 65 | }, 66 | "Action": [ 67 | "kms:Decrypt", 68 | "kms:ReEncryptFrom" 69 | ], 70 | "Resource": "*", 71 | "Condition": { 72 | "StringEquals": { 73 | "kms:CallerAccount": "_ACCOUNT_NUMBER_" 74 | }, 75 | "StringLike": { 76 | "kms:EncryptionContext:aws:cloudtrail:arn": "arn:aws:cloudtrail:*:_ACCOUNT_NUMBER_:trail/*" 77 | } 78 | } 79 | } 80 | ] 81 | } -------------------------------------------------------------------------------- /auto_remediate/data/s3_bucket_ssl_requests_only_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Deny", 6 | "Principal": "*", 7 | "Action": "*", 8 | "Resource": "arn:aws:s3:::_BUCKET_/*", 9 | "Condition": { 10 | "Bool": { 11 | "aws:SecureTransport": "false" 12 | } 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /auto_remediate/lambda_handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import sys 5 | 6 | import boto3 7 | from dynamodb_json import json_util as dynamodb_json 8 | 9 | from config_rules import * 10 | from custom_rules import * 11 | from security_hub_rules import * 12 | from sns_logging_handler import * 13 | 14 | 15 | class Remediate: 16 | def __init__(self, logging, event): 17 | # parameters 18 | self.logging = logging 19 | self.event = event 20 | 21 | # event payload 22 | self.logging.debug(f"Event payload: {self.event}") 23 | 24 | # variables 25 | self.settings = self.get_settings() 26 | 27 | # classes 28 | self.config = ConfigRules(self.logging) 29 | self.security_hub = SecurityHubRules(self.logging) 30 | self.custom = CustomRules(self.logging) 31 | # remediation function dict 32 | self.remediation_functions = { 33 | # config 34 | "dynamodb-table-encryption-enabled": pass, 35 | "rds-instance-public-access-check": self.config.rds_instance_public_access_check, 36 | "s3-bucket-server-side-encryption-enabled": self.config.s3_bucket_server_side_encryption_enabled, 37 | "s3-bucket-ssl-requests-only": self.config.s3_bucket_ssl_requests_only, 38 | # security hub 39 | "securityhub-access-keys-rotated": self.security_hub.access_keys_rotated, 40 | "securityhub-cloud-trail-cloud-watch-logs-enabled": self.security_hub.cloud_trail_cloud_watch_logs_enabled, 41 | "securityhub-cloud-trail-encryption-enabled": self.security_hub.cloud_trail_encryption_enabled, 42 | "securityhub-cloud-trail-log-file-validation-enabled": self.security_hub.cloud_trail_log_file_validation_enabled, 43 | "securityhub-cmk-backing-key-rotation-enabled": self.security_hub.cmk_backing_key_rotation_enabled, 44 | "securityhub-iam-password-policy-ensure-expires": self.security_hub.iam_password_policy, 45 | "securityhub-iam-password-policy-lowercase-letter-check": self.security_hub.iam_password_policy, 46 | "securityhub-iam-password-policy-minimum-length-check": self.security_hub.iam_password_policy, 47 | "securityhub-iam-password-policy-number-check": self.security_hub.iam_password_policy, 48 | "securityhub-iam-password-policy-prevent-reuse-check": self.security_hub.iam_password_policy, 49 | "securityhub-iam-password-policy-symbol-check": self.security_hub.iam_password_policy, 50 | "securityhub-iam-password-policy-uppercase-letter-check": self.security_hub.iam_password_policy, 51 | "securityhub-iam-policy-no-statements-with-admin-access": self.security_hub.iam_policy_no_statements_with_admin_access, 52 | "securityhub-iam-user-no-policies-check": self.security_hub.iam_user_no_policies_check, 53 | "securityhub-iam-user-unused-credentials-check": self.security_hub.iam_user_unused_credentials_check, 54 | "securityhub-mfa-enabled-for-iam-console-access": self.security_hub.mfa_enabled_for_iam_console_access, 55 | "securityhub-multi-region-cloud-trail-enabled": self.security_hub.multi_region_cloud_trail_enabled, 56 | "securityhub-restricted-rdp": self.security_hub.restricted_rdp, 57 | "securityhub-restricted-ssh": self.security_hub.restricted_ssh, 58 | "securityhub-s3-bucket-logging-enabled": self.security_hub.s3_bucket_logging_enabled, 59 | "securityhub-s3-bucket-public-read-prohibited": self.security_hub.s3_bucket_public_read_prohibited, 60 | "securityhub-s3-bucket-public-write-prohibited": self.security_hub.s3_bucket_public_write_prohibited, 61 | "securityhub-vpc-default-security-group-closed": self.security_hub.vpc_default_security_group_closed, 62 | "securityhub-vpc-flow-logs-enabled": self.security_hub.vpc_flow_logs_enabled 63 | # custom 64 | } 65 | 66 | def remediate(self): 67 | for record in self.event["Records"]: 68 | config_payload = json.loads(record["body"]) 69 | config_rule_name = Remediate.get_config_rule_name(config_payload) 70 | config_rule_compliance = Remediate.get_config_rule_compliance( 71 | config_payload 72 | ) 73 | config_rule_resource_id = Remediate.get_config_rule_resource_id( 74 | config_payload 75 | ) 76 | 77 | if config_rule_compliance == "NON_COMPLIANT": 78 | if self.intend_to_remediate(config_rule_name): 79 | remediation_function = self.remediation_functions.get( 80 | config_rule_name, None 81 | ) 82 | 83 | if remediation_function is not None: 84 | if not remediation_function(config_rule_resource_id): 85 | self.send_to_dead_letter_queue( 86 | config_payload, Remediate.get_try_count(record) 87 | ) 88 | else: 89 | self.logging.warning( 90 | f"No remediation available for Config Rule " 91 | f"'{config_rule_name}' with payload '{config_payload}'." 92 | ) 93 | self.send_to_missing_remediation_topic( 94 | config_rule_name, config_payload 95 | ) 96 | else: 97 | self.logging.info( 98 | f"Config Rule '{config_rule_name}' was not remediated based on user preferences." 99 | ) 100 | else: 101 | self.logging.info( 102 | f"Resource '{config_rule_resource_id}' is compliant for Config Rule '{config_rule_name}'." 103 | ) 104 | 105 | @staticmethod 106 | def get_config_rule_compliance(config_payload): 107 | """Retrieves the AWS Config rule compliance variable 108 | 109 | Arguments: 110 | config_payload {dictionary} -- AWS Config payload 111 | 112 | Returns: 113 | string -- COMPLIANT | NON_COMPLIANT 114 | """ 115 | return config_payload["detail"]["newEvaluationResult"]["complianceType"] 116 | 117 | @staticmethod 118 | def get_config_rule_name(config_payload): 119 | """Retrieves the AWS Config rule name variable. For Security Hub rules, the random 120 | suffixed alphanumeric characters will be removed. 121 | 122 | Arguments: 123 | config_payload {dictionary} -- AWS Config payload 124 | 125 | Returns: 126 | string -- AWS Config rule name 127 | """ 128 | config_rule_name = config_payload["detail"]["configRuleName"] 129 | if "securityhub" in config_rule_name: 130 | # remove random alphanumeric string suffixed to each 131 | # Security Hub rule 132 | return config_rule_name[: config_rule_name.rfind("-")] 133 | else: 134 | return config_rule_name 135 | 136 | @staticmethod 137 | def get_config_rule_resource_id(config_payload): 138 | """Retrieves the AWS Config Resource ID from the AWS Config payload 139 | 140 | Arguments: 141 | config_payload {dictionary} -- AWS Config payload 142 | 143 | Returns: 144 | string -- Resource ID relating to the AWS Resource that triggered the AWS Config Rule 145 | """ 146 | return config_payload["detail"]["resourceId"] 147 | 148 | def get_settings(self): 149 | """Return the DynamoDB aws-auto-remediate-settings table in a Python dict format 150 | 151 | Returns: 152 | dict -- aws-auto-remediate-settings table 153 | """ 154 | settings = {} 155 | try: 156 | for record in boto3.client("dynamodb").scan( 157 | TableName=os.environ["SETTINGSTABLE"] 158 | )["Items"]: 159 | record_json = dynamodb_json.loads(record, True) 160 | settings[record_json["key"]] = record_json["value"] 161 | except: 162 | self.logging.error( 163 | f"Could not read DynamoDB table '{os.environ['SETTINGSTABLE']}'." 164 | ) 165 | self.logging.error(sys.exc_info()[1]) 166 | 167 | return settings 168 | 169 | @staticmethod 170 | def get_try_count(record): 171 | """Retrieves the "try_count" key from the SQS Record payload from a custom 172 | SQS Message Attribute 173 | 174 | Arguments: 175 | record {dictionary} -- SQS Record payload 176 | 177 | Returns: 178 | string -- Number of attempted remediations for a given AWS Config Rule 179 | """ 180 | return ( 181 | record.get("messageAttributes", {}) 182 | .get("try_count", {}) 183 | .get("stringValue", "0") 184 | ) 185 | 186 | def intend_to_remediate(self, config_rule_name): 187 | """Returns whether an AWS Config Rule should be remediated based on user preferences. 188 | 189 | Arguments: 190 | config_rule_name {string} -- AWS Config Rule name 191 | 192 | Returns: 193 | boolean -- True | False 194 | """ 195 | return ( 196 | self.settings.get("rules", {}) 197 | .get(config_rule_name, {}) 198 | .get("remediate", True) 199 | ) 200 | 201 | def send_to_dead_letter_queue(self, config_payload, try_count): 202 | """Sends the AWS Config payload to an SQS Queue (DLQ) if after incrementing 203 | the "try_count" variable it is below the user defined "RETRYCOUNT" setting. 204 | 205 | Arguments: 206 | config_payload {dictionary} -- AWS Config payload 207 | try_count {string} -- Number of previos remediation attemps for this AWS Config payload 208 | """ 209 | client = boto3.client("sqs") 210 | 211 | try_count = int(try_count) + 1 212 | if try_count < int(os.environ.get("RETRYCOUNT", 3)): 213 | try: 214 | client.send_message( 215 | QueueUrl=os.environ["DEADLETTERQUEUE"], 216 | MessageBody=json.dumps(config_payload), 217 | MessageAttributes={ 218 | "try_count": { 219 | "StringValue": str(try_count), 220 | "DataType": "Number", 221 | } 222 | }, 223 | ) 224 | 225 | self.logging.debug( 226 | f"Remediation failed. Payload has been sent to SQS DLQ '{os.environ['DEADLETTERQUEUE']}'." 227 | ) 228 | except: 229 | self.logging.error( 230 | f"Could not send payload to SQS DLQ '{os.environ['DEADLETTERQUEUE']}'." 231 | ) 232 | self.logging.error(sys.exc_info()[1]) 233 | else: 234 | self.logging.warning( 235 | f"Could not remediate Config change within an " 236 | f"acceptable number of retries for payload '{config_payload}'." 237 | ) 238 | 239 | def send_to_missing_remediation_topic(self, config_rule_name, config_payload): 240 | """Publishes a message onto the missing remediation SNS Topic. The topic should be subscribed to 241 | by administrators to be aware when their security remediations are not fully covered. 242 | 243 | Arguments: 244 | config_rule_name {string} -- AWS Config Rule name 245 | config_payload {dictionary} -- AWS Config Rule payload 246 | """ 247 | client = boto3.client("sns") 248 | topic_arn = os.environ["MISSINGREMEDIATIONTOPIC"] 249 | 250 | try: 251 | client.publish( 252 | TopicArn=topic_arn, 253 | Message=json.dumps(config_payload), 254 | Subject=f"No remediation available for Config Rule '{config_rule_name}'", 255 | ) 256 | except: 257 | self.logging.error(f"Could not publish to SNS Topic 'topic_arn'.") 258 | 259 | 260 | def lambda_handler(event, context): 261 | loggger = logging.getLogger() 262 | 263 | if loggger.handlers: 264 | for handler in loggger.handlers: 265 | loggger.removeHandler(handler) 266 | 267 | # change logging levels for boto and others to prevent log spamming 268 | logging.getLogger("boto3").setLevel(logging.ERROR) 269 | logging.getLogger("botocore").setLevel(logging.ERROR) 270 | logging.getLogger("urllib3").setLevel(logging.ERROR) 271 | 272 | # set logging format 273 | logging.basicConfig( 274 | format="[%(levelname)s] %(message)s (%(filename)s, %(funcName)s(), line %(lineno)d)", 275 | level=os.environ.get("LOGLEVEL", "WARNING"), 276 | ) 277 | 278 | # add SNS logger 279 | # sns_logger = SNSLoggingHandler(os.environ.get('LOGTOPIC')) 280 | # sns_logger.setLevel(logging.INFO) 281 | # loggger.addHandler(sns_logger) 282 | 283 | # instantiate class 284 | remediate = Remediate(logging, event) 285 | 286 | # run functions 287 | remediate.remediate() 288 | -------------------------------------------------------------------------------- /auto_remediate/sns_logging_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import boto3 4 | 5 | 6 | class SNSLoggingHandler(logging.Handler): 7 | def __init__(self, topic_arn): 8 | logging.Handler.__init__(self) 9 | self.client = boto3.client("sns") 10 | self.topic_arn = topic_arn 11 | 12 | def emit(self, record): 13 | self.client.publish( 14 | TopicArn=self.topic_arn, 15 | Message=f"[{record.levelname}] {record.getMessage()}", 16 | ) 17 | -------------------------------------------------------------------------------- /auto_remediate/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlevit/aws-auto-remediate/949adb55809a122278a55c6503e19c9032b378ca/auto_remediate/test/__init__.py -------------------------------------------------------------------------------- /auto_remediate/test/test_config_rds.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | 4 | import moto 5 | import pytest 6 | 7 | from .. import config_rules 8 | 9 | 10 | class TestRdsInstancePublicAccessCheck: 11 | @pytest.fixture 12 | def cr(self): 13 | with moto.mock_rds(): 14 | cr = config_rules.ConfigRules(logging) 15 | yield cr 16 | 17 | def test_invalid_rds(self, cr): 18 | # validate test 19 | assert not cr.rds_instance_public_access_check("test") 20 | -------------------------------------------------------------------------------- /auto_remediate/test/test_config_s3.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | 4 | import moto 5 | import pytest 6 | 7 | from .. import config_rules 8 | 9 | 10 | class TestS3BucketServerSideEncryptionEnabled: 11 | @pytest.fixture 12 | def cr(self): 13 | with moto.mock_s3(): 14 | cr = config_rules.ConfigRules(logging) 15 | yield cr 16 | 17 | # def test_s3_bucket_sse_enabled(self, cr): 18 | # # create bucket 19 | # cr.client_s3.create_bucket(Bucket="test") 20 | 21 | # # test s3_bucket_server_side_encryption_enabled function 22 | # cr.s3_bucket_server_side_encryption_enabled("test") 23 | 24 | # # validate test 25 | # response = cr.client_s3.get_bucket_encryption(Bucket="test") 26 | # print(response) 27 | # assert ( 28 | # response["ServerSideEncryptionConfiguration"]["Rules"][0][ 29 | # "ApplyServerSideEncryptionByDefault" 30 | # ]["SSEAlgorithm"] 31 | # == "AES256" 32 | # ) 33 | 34 | def test_invalid_bucket(self, cr): 35 | # create bucket 36 | cr.client_s3.create_bucket(Bucket="test") 37 | 38 | # validate test 39 | # TODO Not returning a valid response code 40 | assert cr.s3_bucket_server_side_encryption_enabled("test123") 41 | 42 | 43 | class TestS3BucketSslRequestsOnly: 44 | @pytest.fixture 45 | def cr(self): 46 | with moto.mock_s3(): 47 | cr = config_rules.ConfigRules(logging) 48 | yield cr 49 | 50 | def test_invalid_bucket(self, cr): 51 | # create bucket 52 | cr.client_s3.create_bucket(Bucket="test") 53 | 54 | # validate test 55 | assert not cr.s3_bucket_ssl_requests_only("test123") 56 | -------------------------------------------------------------------------------- /auto_remediate/test/test_securityhub_ec2.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | 4 | import moto 5 | import pytest 6 | 7 | from .. import security_hub_rules 8 | 9 | 10 | class TestSecurityHubRestrictedRDPCheck: 11 | @pytest.fixture 12 | def sh(self): 13 | with moto.mock_ec2(): 14 | sh = security_hub_rules.SecurityHubRules(logging) 15 | yield sh 16 | 17 | def test_rdp_port_statements_removed(self, sh): 18 | """Tests removal of RDP statements from Security Group 19 | 20 | Arguments: 21 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 22 | """ 23 | 24 | # create Security Group 25 | response = sh.client_ec2.create_security_group( 26 | Description="test", GroupName="test" 27 | ) 28 | security_group_id = response["GroupId"] 29 | 30 | # attach statements to Security Group 31 | sh.client_ec2.authorize_security_group_ingress( 32 | GroupId=security_group_id, 33 | IpPermissions=[ 34 | { 35 | "FromPort": 3389, 36 | "ToPort": 3389, 37 | "IpProtocol": "tcp", 38 | "IpRanges": [{"CidrIp": "0.0.0.0/0"}], 39 | }, 40 | { 41 | "FromPort": 3389, 42 | "ToPort": 3389, 43 | "IpProtocol": "tcp", 44 | "Ipv6Ranges": [{"CidrIpv6": "::/0"}], 45 | }, 46 | ], 47 | ) 48 | 49 | # test restricted_rdp function 50 | sh.restricted_rdp(security_group_id) 51 | 52 | # validate test 53 | response = sh.client_ec2.describe_security_groups(GroupIds=[security_group_id]) 54 | assert len(response["SecurityGroups"][0]["IpPermissions"]) == 0 55 | 56 | def test_non_rdp_port_statements_not_removed(self, sh): 57 | """Tests non-RDP port statements are not removed from the Security Group 58 | 59 | Arguments: 60 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 61 | """ 62 | 63 | # create Security Group 64 | response = sh.client_ec2.create_security_group( 65 | Description="test", GroupName="test" 66 | ) 67 | security_group_id = response["GroupId"] 68 | 69 | # attach statements to Security Group 70 | sh.client_ec2.authorize_security_group_ingress( 71 | GroupId=security_group_id, 72 | IpPermissions=[ 73 | { 74 | "FromPort": 1234, 75 | "ToPort": 1234, 76 | "IpProtocol": "tcp", 77 | "IpRanges": [{"CidrIp": "0.0.0.0/0"}], 78 | }, 79 | { 80 | "FromPort": 1234, 81 | "ToPort": 1234, 82 | "IpProtocol": "tcp", 83 | "Ipv6Ranges": [{"CidrIpv6": "::/0"}], 84 | }, 85 | ], 86 | ) 87 | 88 | # test restricted_rdp function 89 | sh.restricted_rdp(security_group_id) 90 | 91 | # validate test 92 | response = sh.client_ec2.describe_security_groups(GroupIds=[security_group_id]) 93 | assert len(response["SecurityGroups"][0]["IpPermissions"]) == 2 94 | 95 | 96 | class TestSecurityHubRestrictedSSHCheck: 97 | @pytest.fixture 98 | def sh(self): 99 | with moto.mock_ec2(): 100 | sh = security_hub_rules.SecurityHubRules(logging) 101 | yield sh 102 | 103 | def test_ssh_port_statements_removed(self, sh): 104 | """Tests removal of SSH statements from Security Group 105 | 106 | Arguments: 107 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 108 | """ 109 | # create Security Group 110 | response = sh.client_ec2.create_security_group( 111 | Description="test", GroupName="test" 112 | ) 113 | security_group_id = response["GroupId"] 114 | 115 | # attach statements to Security Group 116 | sh.client_ec2.authorize_security_group_ingress( 117 | GroupId=security_group_id, 118 | IpPermissions=[ 119 | { 120 | "FromPort": 22, 121 | "ToPort": 22, 122 | "IpProtocol": "tcp", 123 | "IpRanges": [{"CidrIp": "0.0.0.0/0"}], 124 | }, 125 | { 126 | "FromPort": 22, 127 | "ToPort": 22, 128 | "IpProtocol": "tcp", 129 | "Ipv6Ranges": [{"CidrIpv6": "::/0"}], 130 | }, 131 | ], 132 | ) 133 | 134 | # test restricted_rdp function 135 | sh.restricted_ssh(security_group_id) 136 | 137 | # validate test 138 | response = sh.client_ec2.describe_security_groups(GroupIds=[security_group_id]) 139 | assert len(response["SecurityGroups"][0]["IpPermissions"]) == 0 140 | 141 | def test_non_ssh_port_statements_not_removed(self, sh): 142 | """Tests non-SSH port statements are not removed from the Security Group 143 | 144 | Arguments: 145 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 146 | """ 147 | 148 | # create Security Group 149 | response = sh.client_ec2.create_security_group( 150 | Description="test", GroupName="test" 151 | ) 152 | security_group_id = response["GroupId"] 153 | 154 | # attach statements to Security Group 155 | sh.client_ec2.authorize_security_group_ingress( 156 | GroupId=security_group_id, 157 | IpPermissions=[ 158 | { 159 | "FromPort": 1234, 160 | "ToPort": 1234, 161 | "IpProtocol": "tcp", 162 | "IpRanges": [{"CidrIp": "0.0.0.0/0"}], 163 | }, 164 | { 165 | "FromPort": 1234, 166 | "ToPort": 1234, 167 | "IpProtocol": "tcp", 168 | "Ipv6Ranges": [{"CidrIpv6": "::/0"}], 169 | }, 170 | ], 171 | ) 172 | 173 | # test restricted_rdp function 174 | sh.restricted_ssh(security_group_id) 175 | 176 | # validate test 177 | response = sh.client_ec2.describe_security_groups(GroupIds=[security_group_id]) 178 | assert len(response["SecurityGroups"][0]["IpPermissions"]) == 2 179 | 180 | 181 | class TestSecurityHubVPCDefaultSecurityGroupClosedCheck: 182 | @pytest.fixture 183 | def sh(self): 184 | with moto.mock_ec2(): 185 | sh = security_hub_rules.SecurityHubRules(logging) 186 | yield sh 187 | 188 | @pytest.fixture 189 | def security_group_id(self, sh): 190 | """Creates EC2 Security Group 191 | 192 | Arguments: 193 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 194 | """ 195 | response = sh.client_ec2.create_security_group( 196 | Description="test", GroupName="test" 197 | ) 198 | yield response["GroupId"] 199 | 200 | def test_ingress_statements_removed(self, sh, security_group_id): 201 | """Tests ingress statements are removed from Security Group 202 | 203 | Arguments: 204 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 205 | security_group_id {string} -- EC2 Security Group ID 206 | """ 207 | 208 | # attach ingress Statements to Security Group 209 | sh.client_ec2.authorize_security_group_ingress( 210 | GroupId=security_group_id, 211 | IpPermissions=[ 212 | { 213 | "FromPort": 22, 214 | "ToPort": 22, 215 | "IpProtocol": "tcp", 216 | "IpRanges": [{"CidrIp": "0.0.0.0/0"}], 217 | }, 218 | { 219 | "FromPort": 80, 220 | "ToPort": 80, 221 | "IpProtocol": "tcp", 222 | "IpRanges": [{"CidrIp": "0.0.0.0/0"}], 223 | }, 224 | ], 225 | ) 226 | 227 | # test vpc_default_security_group_closed function 228 | sh.vpc_default_security_group_closed(security_group_id) 229 | 230 | # validate test 231 | response = sh.client_ec2.describe_security_groups(GroupIds=[security_group_id]) 232 | assert len(response["SecurityGroups"][0]["IpPermissions"]) == 0 233 | 234 | def test_egress_statements_removed(self, sh, security_group_id): 235 | """Tests egress statements are removed from Security Group 236 | 237 | Arguments: 238 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 239 | security_group_id {string} -- EC2 Security Group ID 240 | """ 241 | 242 | # attach egress Statements to Security Group 243 | sh.client_ec2.authorize_security_group_egress( 244 | GroupId=security_group_id, 245 | IpPermissions=[ 246 | { 247 | "FromPort": 22, 248 | "ToPort": 22, 249 | "IpProtocol": "tcp", 250 | "IpRanges": [{"CidrIp": "0.0.0.0/0"}], 251 | }, 252 | { 253 | "FromPort": 80, 254 | "ToPort": 80, 255 | "IpProtocol": "tcp", 256 | "IpRanges": [{"CidrIp": "0.0.0.0/0"}], 257 | }, 258 | ], 259 | ) 260 | 261 | # test vpc_default_security_group_closed function 262 | sh.vpc_default_security_group_closed(security_group_id) 263 | 264 | # validate test 265 | response = sh.client_ec2.describe_security_groups(GroupIds=[security_group_id]) 266 | assert len(response["SecurityGroups"][0]["IpPermissionsEgress"]) == 0 267 | 268 | def test_invalid_security_group_id(self, sh): 269 | assert not sh.vpc_default_security_group_closed("test") 270 | 271 | 272 | class TestSecurityHubVpcFlowLogsEnabled: 273 | @pytest.fixture 274 | def sh(self): 275 | with moto.mock_ec2(), moto.mock_sts(): 276 | sh = security_hub_rules.SecurityHubRules(logging) 277 | yield sh 278 | 279 | def test_invalid_vpc(self, sh): 280 | assert not sh.vpc_flow_logs_enabled("test") 281 | -------------------------------------------------------------------------------- /auto_remediate/test/test_securityhub_iam.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | 4 | import moto 5 | import pytest 6 | 7 | from .. import security_hub_rules 8 | 9 | 10 | class TestSecurityHubAccessKeysRotated: 11 | @pytest.fixture 12 | def sh(self): 13 | with moto.mock_iam(): 14 | sh = security_hub_rules.SecurityHubRules(logging) 15 | yield sh 16 | 17 | @pytest.fixture 18 | def iam_test_user_name(self, sh): 19 | response = sh.client_iam.create_user(UserName="test") 20 | yield response["User"]["UserName"] 21 | 22 | @pytest.fixture 23 | def iam_test_access_key_id(self, iam_test_user_name, sh): 24 | response = sh.client_iam.create_access_key(UserName=iam_test_user_name) 25 | yield response["AccessKey"]["AccessKeyId"] 26 | 27 | def test_access_key_rotated_check( 28 | self, iam_test_user_name, iam_test_access_key_id, sh 29 | ): 30 | sh.access_keys_rotated(iam_test_access_key_id) 31 | response = sh.client_iam.list_access_keys(UserName=iam_test_user_name) 32 | assert not response["AccessKeyMetadata"] 33 | 34 | def test_iam_user_name_not_found_check(self, sh): 35 | """Tests if an error is thrown if the Access Key ID cannot be found 36 | 37 | Arguments: 38 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 39 | """ 40 | assert not sh.access_keys_rotated("FAKE_KEY_ID") 41 | 42 | 43 | class TestSecurityHubIAMPasswordPolicyEnsureExpires: 44 | @pytest.fixture 45 | def sh(self): 46 | with moto.mock_iam(): 47 | sh = security_hub_rules.SecurityHubRules(logging) 48 | yield sh 49 | 50 | def test_invalid_iam(self, sh): 51 | assert not sh.iam_password_policy("test") 52 | 53 | 54 | class TestSecurityHubIAMPasswordPolicyLowercaseLetterCheck: 55 | @pytest.fixture 56 | def sh(self): 57 | with moto.mock_iam(): 58 | sh = security_hub_rules.SecurityHubRules(logging) 59 | yield sh 60 | 61 | def test_invalid_iam(self, sh): 62 | assert not sh.iam_password_policy("test") 63 | 64 | 65 | class TestSecurityHubIAMPasswordPolicyMinimumLengthCheck: 66 | @pytest.fixture 67 | def sh(self): 68 | with moto.mock_iam(): 69 | sh = security_hub_rules.SecurityHubRules(logging) 70 | yield sh 71 | 72 | def test_invalid_iam(self, sh): 73 | assert not sh.iam_password_policy("test") 74 | 75 | 76 | class TestSecurityHubIAMPasswordPolicyNumberCheck: 77 | @pytest.fixture 78 | def sh(self): 79 | with moto.mock_iam(): 80 | sh = security_hub_rules.SecurityHubRules(logging) 81 | yield sh 82 | 83 | def test_invalid_iam(self, sh): 84 | assert not sh.iam_password_policy("test") 85 | 86 | 87 | class TestSecurityHubIAMPasswordPolicyPreventReuseCheck: 88 | @pytest.fixture 89 | def sh(self): 90 | with moto.mock_iam(): 91 | sh = security_hub_rules.SecurityHubRules(logging) 92 | yield sh 93 | 94 | def test_invalid_iam(self, sh): 95 | assert not sh.iam_password_policy("test") 96 | 97 | 98 | class TestSecurityHubIAMPasswordPolicySymbolCheck: 99 | @pytest.fixture 100 | def sh(self): 101 | with moto.mock_iam(): 102 | sh = security_hub_rules.SecurityHubRules(logging) 103 | yield sh 104 | 105 | def test_invalid_iam(self, sh): 106 | assert not sh.iam_password_policy("test") 107 | 108 | 109 | class TestSecurityHubIAMPasswordPolicyUppercaseLetterCheck: 110 | @pytest.fixture 111 | def sh(self): 112 | with moto.mock_iam(): 113 | sh = security_hub_rules.SecurityHubRules(logging) 114 | yield sh 115 | 116 | def test_invalid_iam(self, sh): 117 | assert not sh.iam_password_policy("test") 118 | 119 | 120 | class TestSecurityHubIamPolicyNoStatementsWithAdminAccess: 121 | @pytest.fixture 122 | def sh(self): 123 | with moto.mock_iam(): 124 | sh = security_hub_rules.SecurityHubRules(logging) 125 | yield sh 126 | 127 | @pytest.fixture 128 | def iam_test_policy_arn(self, sh): 129 | """Creates new IAM Policy with admin access 130 | 131 | Arguments: 132 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 133 | """ 134 | response = sh.client_iam.create_policy( 135 | PolicyName="test", 136 | PolicyDocument='{"Version":"2012-10-17","Statement":[{"Sid":"Test","Effect":"Allow","Action":"*","Resource":"*"}]}', 137 | ) 138 | yield response["Policy"]["Arn"] 139 | 140 | @pytest.fixture 141 | def iam_test_policy_id(self, iam_test_policy_arn, sh): 142 | """Retrieves IAM Policy ID from IAM Policy ARN 143 | 144 | Arguments: 145 | iam_test_policy_arn {string} -- IAM Policy ARN 146 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 147 | """ 148 | response = sh.client_iam.get_policy(PolicyArn=iam_test_policy_arn) 149 | yield response["Policy"]["PolicyId"] 150 | 151 | def test_securityhub_iam_policy_no_statement_with_admin_access_check( 152 | self, iam_test_policy_arn, iam_test_policy_id, sh 153 | ): 154 | """Tests if an IAM Policy Statement with admin access is removed 155 | 156 | Arguments: 157 | iam_test_policy_arn {string} -- IAM Policy ARN 158 | iam_test_policy_id {string} -- IAM Policy ID 159 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 160 | """ 161 | # call remediation function to remove IAM Policy Statement with admin access 162 | sh.iam_policy_no_statements_with_admin_access(iam_test_policy_id) 163 | 164 | # get IAM Policy Default Version 165 | response = sh.client_iam.get_policy(PolicyArn=iam_test_policy_arn) 166 | iam_test_policy_default_version = response.get("Policy").get("DefaultVersionId") 167 | 168 | # get IAM Policy Version that includes Policy Statement 169 | response = sh.client_iam.get_policy_version( 170 | PolicyArn=iam_test_policy_arn, VersionId=iam_test_policy_default_version 171 | ) 172 | 173 | assert ( 174 | response["PolicyVersion"]["Document"]["Statement"][0]["Condition"][ 175 | "ForAnyValue:DateLessThan" 176 | ]["aws:EpochTime"] 177 | == "0000000000" 178 | ) 179 | 180 | 181 | class TestSecurityHubIamUserNoPoliciesCheck: 182 | @pytest.fixture 183 | def sh(self): 184 | with moto.mock_ec2(), moto.mock_s3(), moto.mock_iam(), moto.mock_kms(): 185 | sh = security_hub_rules.SecurityHubRules(logging) 186 | yield sh 187 | 188 | @pytest.fixture 189 | def iam_test_user_id(self, sh): 190 | """Creates IAM User 191 | 192 | Arguments: 193 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 194 | """ 195 | response = sh.client_iam.create_user(UserName="test") 196 | yield response["User"]["UserId"] 197 | 198 | @pytest.fixture 199 | def iam_test_user_with_policy(self, iam_test_user_id, sh): 200 | """Sets up a user with attached user policy to test iam_no_user_policies_check 201 | 202 | Arguments: 203 | iam_test_user_id {string} -- IAM User ID 204 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 205 | """ 206 | sh.client_iam.attach_user_policy( 207 | UserName="test", PolicyArn="arn:aws:iam::aws:policy/IAMReadOnlyAccess" 208 | ) 209 | yield sh 210 | 211 | def test_iam_no_user_policies_check( 212 | self, iam_test_user_id, iam_test_user_with_policy 213 | ): 214 | """Tests that IAM Managed Policies are removed from an IAM User 215 | 216 | Arguments: 217 | iam_test_user_id {string} -- IAM User ID 218 | iam_test_user_with_policy {function} -- Instance of a function 219 | """ 220 | iam_test_user_with_policy.iam_user_no_policies_check(iam_test_user_id) 221 | response = iam_test_user_with_policy.client_iam.list_attached_user_policies( 222 | UserName="test" 223 | ) 224 | assert not response["AttachedPolicies"] 225 | 226 | 227 | class TestSecurityHubMfaEnabledForIamConsoleAccess: 228 | @pytest.fixture 229 | def sh(self): 230 | with moto.mock_iam(): 231 | sh = security_hub_rules.SecurityHubRules(logging) 232 | yield sh 233 | 234 | @pytest.fixture 235 | def iam_test_user_id(self, sh): 236 | """Creates IAM User 237 | 238 | Arguments: 239 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 240 | """ 241 | response = sh.client_iam.create_user(UserName="test") 242 | yield response["User"]["UserId"] 243 | 244 | @pytest.fixture 245 | def iam_test_user_login_profile(self, iam_test_user_id, sh): 246 | """Creates a Login Profile for an IAM User 247 | 248 | Arguments: 249 | iam_test_user_id {string} -- IAM User ID 250 | sh {SecurityHubRules} -- Instance of SecurityHubRules class 251 | """ 252 | sh.client_iam.create_login_profile(UserName="test", Password="!@#$QWERasdf1234") 253 | yield sh 254 | 255 | def test_securityhub_mfa_enabled_for_iam_console_access_check( 256 | self, iam_test_user_login_profile, iam_test_user_id 257 | ): 258 | """Tests that the remediation removes a login profile from a user 259 | 260 | Arguments: 261 | iam_test_user_login_profile {function} -- Instance of function 262 | iam_test_user_id {string} -- IAM User ID 263 | """ 264 | # before remediation, user must have login profile 265 | assert iam_test_user_login_profile.client_iam.get_login_profile(UserName="test") 266 | 267 | # run remediation 268 | iam_test_user_login_profile.mfa_enabled_for_iam_console_access( 269 | resource_id=iam_test_user_id 270 | ) 271 | 272 | # assert user doesn't have login profile after remediation 273 | with pytest.raises( 274 | iam_test_user_login_profile.client_iam.exceptions.NoSuchEntityException 275 | ): 276 | iam_test_user_login_profile.client_iam.get_login_profile(UserName="test") 277 | -------------------------------------------------------------------------------- /auto_remediate/test/test_securityhub_kms.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | 4 | import moto 5 | import pytest 6 | 7 | from .. import security_hub_rules 8 | 9 | 10 | class TestSecurityHubCmkBackingKeyRotationEnabled: 11 | @pytest.fixture 12 | def sh(self): 13 | with moto.mock_kms(), moto.mock_sts(): 14 | sh = security_hub_rules.SecurityHubRules(logging) 15 | yield sh 16 | 17 | def test_kms_cmk_rotation_enabled(self, sh): 18 | """Tests if KMS Customer Managed Key rotation is turned on 19 | 20 | Arguments: 21 | sh {SecurityHubRules} -- Instance of class SecurityHubRules 22 | """ 23 | 24 | # create KMS CMK 25 | response = sh.client_kms.create_key() 26 | kms_key_id = response["KeyMetadata"]["KeyId"] 27 | 28 | # test cmk_backing_key_rotation_enabled 29 | sh.cmk_backing_key_rotation_enabled(kms_key_id) 30 | 31 | # validate test 32 | response = sh.client_kms.get_key_rotation_status(KeyId=kms_key_id) 33 | assert response["KeyRotationEnabled"] 34 | 35 | def test_invalid_invalid_kms_cmk(self, sh): 36 | """Tests invalid KMS CMK 37 | 38 | Arguments: 39 | sh {SecurityHubRules} -- Instance of class SecurityHubRules 40 | """ 41 | # test cmk_backing_key_rotation_enabled 42 | response = sh.cmk_backing_key_rotation_enabled( 43 | "e85f5843-1111-4bcb-b711-7e17fa181804" 44 | ) 45 | assert not response 46 | -------------------------------------------------------------------------------- /auto_remediate/test/test_securityhub_s3.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | 4 | import moto 5 | import pytest 6 | 7 | from .. import security_hub_rules 8 | 9 | 10 | class TestSecurityHubS3BucketPublicReadProhibited: 11 | @pytest.fixture 12 | def sh(self): 13 | with moto.mock_s3(): 14 | sh = security_hub_rules.SecurityHubRules(logging) 15 | yield sh 16 | 17 | def test_s3_bucket_public_read_disabled(self, sh): 18 | """Tests if S3 Bucket public read has been turned off 19 | 20 | Arguments: 21 | s3_test_bucket_public_read {string} -- S3 bucket name 22 | sh {SecurityHubRules} -- Instance of class SecurityHubRules 23 | """ 24 | 25 | # create bucket 26 | sh.client_s3.create_bucket(ACL="public-read", Bucket="test") 27 | 28 | # test s3_bucket_public_read_prohibited function 29 | sh.s3_bucket_public_read_prohibited("test") 30 | 31 | # validate test 32 | response = sh.client_s3.get_bucket_acl(Bucket="test") 33 | assert response["Grants"][0]["Permission"] == "FULL_CONTROL" 34 | 35 | def test_invalid_bucket(self, sh): 36 | # create bucket 37 | sh.client_s3.create_bucket(ACL="public-read", Bucket="test") 38 | 39 | # validate test 40 | assert not sh.s3_bucket_public_read_prohibited("test123") 41 | 42 | 43 | class TestSecurityHubS3BucketLoggingEnabled: 44 | @pytest.fixture 45 | def sh(self): 46 | with moto.mock_s3(), moto.mock_sts(): 47 | sh = security_hub_rules.SecurityHubRules(logging) 48 | yield sh 49 | 50 | def test_invalid_bucket(self, sh): 51 | # create bucket 52 | sh.client_s3.create_bucket(ACL="public-read", Bucket="test") 53 | 54 | # validate test 55 | assert not sh.s3_bucket_logging_enabled("test") 56 | 57 | 58 | class TestSecurityHubS3BucketPublicWriteProhibited: 59 | @pytest.fixture 60 | def sh(self): 61 | with moto.mock_s3(): 62 | sh = security_hub_rules.SecurityHubRules(logging) 63 | yield sh 64 | 65 | def test_s3_bucket_public_write_disabled(self, sh): 66 | """Tests if S3 Bucket public write has been turned off 67 | 68 | Arguments: 69 | s3_test_bucket_public_write {string} -- S3 bucket name 70 | sh {SecurityHubRules} -- Instance of class SecurityHubRules 71 | """ 72 | 73 | # create bucket 74 | sh.client_s3.create_bucket(ACL="public-read", Bucket="test") 75 | 76 | # test s3_bucket_public_read_prohibited function 77 | sh.s3_bucket_public_write_prohibited("test") 78 | 79 | # validate test 80 | response = sh.client_s3.get_bucket_acl(Bucket="test") 81 | assert response["Grants"][0]["Permission"] == "FULL_CONTROL" 82 | 83 | def test_invalid_bucket(self, sh): 84 | # create bucket 85 | sh.client_s3.create_bucket(ACL="public-read", Bucket="test") 86 | 87 | # validate test 88 | assert not sh.s3_bucket_public_write_prohibited("test123") 89 | -------------------------------------------------------------------------------- /auto_remediate/test/test_securityhub_static.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | 4 | import moto 5 | import pytest 6 | 7 | from .. import security_hub_rules 8 | 9 | 10 | class TestSecurityHubStatic: 11 | @pytest.fixture 12 | def sh(self): 13 | yield security_hub_rules.SecurityHubRules(logging) 14 | 15 | def test_convert_to_datetime(self, sh): 16 | assert sh.convert_to_datetime(datetime.date(1999, 12, 31)) == datetime.datetime( 17 | 1999, 12, 31, 0, 0, 0 18 | ) 19 | -------------------------------------------------------------------------------- /auto_remediate_dlq/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlevit/aws-auto-remediate/949adb55809a122278a55c6503e19c9032b378ca/auto_remediate_dlq/__init__.py -------------------------------------------------------------------------------- /auto_remediate_dlq/lambda_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | import boto3 6 | 7 | 8 | class Retry: 9 | def __init__(self, logging): 10 | # parameters 11 | self.logging = logging 12 | 13 | self._client_sts = None 14 | self._client_sqs = None 15 | 16 | @property 17 | def client_sts(self): 18 | if not self._client_sts: 19 | self._client_sts = boto3.client("sts") 20 | return self._client_sts 21 | 22 | @property 23 | def region(self): 24 | if self.client_sts.meta.region_name != "aws-global": 25 | return self.client_sts.meta.region_name 26 | else: 27 | return "us-east-1" 28 | 29 | @property 30 | def client_sqs(self): 31 | if not self._client_sqs: 32 | self._client_sqs = boto3.client("sqs", self.region) 33 | return self._client_sqs 34 | 35 | def retry_security_events(self): 36 | """Retrieves messages from the DLQ and sends them back into the 37 | compliance SQS Queue for reprocessing. 38 | """ 39 | queue_url = os.environ.get("DEADLETTERQUEUE") 40 | response = self.receive_message(queue_url) 41 | 42 | while "Messages" in response: 43 | for message in response.get("Messages"): 44 | receipt_handle = message.get("ReceiptHandle") 45 | body = message.get("Body") 46 | try_count = ( 47 | message.get("MessageAttributes", {}) 48 | .get("try_count", {}) 49 | .get("StringValue", "1") 50 | ) 51 | 52 | if self.send_to_compliance_queue(body, try_count): 53 | self.delete_from_queue(queue_url, receipt_handle) 54 | 55 | response = self.receive_message(queue_url) 56 | 57 | def delete_from_queue(self, queue_url, receipt_handle): 58 | """Delete a Message from an SQS Queue. 59 | 60 | Arguments: 61 | queue_url {string} -- URL of an SQS Queue 62 | receipt_handle {string} -- The receipt handle associated with the message to delete 63 | """ 64 | try: 65 | self.client_sqs.delete_message( 66 | QueueUrl=queue_url, ReceiptHandle=receipt_handle 67 | ) 68 | 69 | self.logging.info( 70 | f"Deleted Message '{receipt_handle}' from SQS Queue URL '{queue_url}'." 71 | ) 72 | return True 73 | except: 74 | self.logging.error( 75 | f"Could not delete Message '{receipt_handle}' from SQS Queue URL '{queue_url}'." 76 | ) 77 | self.logging.error(sys.exc_info()[1]) 78 | return False 79 | 80 | def receive_message(self, queue_url): 81 | """Retrieves 10 messeges from an SQS Queue 82 | 83 | Arguments: 84 | queue_url {string} -- SQS Queue URL 85 | 86 | Returns: 87 | dictionary -- Dictionary of SQS messeges 88 | """ 89 | try: 90 | return self.client_sqs.receive_message( 91 | QueueUrl=queue_url, 92 | MessageAttributeNames=["try_count"], 93 | MaxNumberOfMessages=10, 94 | ) 95 | except: 96 | self.logging.error( 97 | f"Could not retrieve Messages from SQS Queue URL '{queue_url}'." 98 | ) 99 | self.logging.error(sys.exc_info()[1]) 100 | return {} 101 | 102 | def send_to_compliance_queue(self, config_payload, try_count): 103 | """Sends a message to the Config Compliance SQS Queue. 104 | 105 | Arguments: 106 | config_payload {string} -- AWS Config payload 107 | try_count {string} -- Number of attempted remediations for a given AWS Config Rule 108 | 109 | Returns: 110 | boolean -- True if sending message to SQS was successful 111 | """ 112 | queue_url = os.environ.get("COMPLIANCEQUEUE") 113 | 114 | try: 115 | self.client_sqs.send_message( 116 | QueueUrl=queue_url, 117 | MessageBody=config_payload, 118 | MessageAttributes={ 119 | "try_count": {"StringValue": try_count, "DataType": "Number"} 120 | }, 121 | ) 122 | 123 | self.logging.debug(f"Message payload sent to SQS Queue '{queue_url}'.") 124 | return True 125 | except: 126 | self.logging.error(f"Could not send payload to SQS Queue '{queue_url}'.") 127 | self.logging.error(sys.exc_info()[1]) 128 | return False 129 | 130 | @staticmethod 131 | def get_config_rule_compliance(record): 132 | """Retrieves the AWS Config rule compliance variable 133 | 134 | Arguments: 135 | config_payload {JSON} -- AWS Config payload 136 | 137 | Returns: 138 | string -- COMPLIANT | NON_COMPLIANT 139 | """ 140 | return record.get("detail").get("newEvaluationResult").get("complianceType") 141 | 142 | @staticmethod 143 | def get_config_rule_name(record): 144 | """Retrieves the AWS Config rule name variable. For Security Hub rules, the random 145 | suffixed alphanumeric characters will be removed. 146 | 147 | Arguments: 148 | config_payload {JSON} -- AWS Config payload 149 | 150 | Returns: 151 | string -- AWS Config rule name 152 | """ 153 | return record.get("detail").get("configRuleName") 154 | 155 | 156 | def lambda_handler(event, context): 157 | logger = logging.getLogger() 158 | 159 | if logger.handlers: 160 | for handler in logger.handlers: 161 | logger.removeHandler(handler) 162 | 163 | # change logging levels for boto and others 164 | logging.getLogger("boto3").setLevel(logging.ERROR) 165 | logging.getLogger("botocore").setLevel(logging.ERROR) 166 | logging.getLogger("urllib3").setLevel(logging.ERROR) 167 | 168 | # set logging format 169 | logging.basicConfig( 170 | format="[%(levelname)s] %(message)s (%(filename)s, %(funcName)s(), line %(lineno)d)", 171 | level=os.environ.get("LOGLEVEL", "WARNING").upper(), 172 | ) 173 | 174 | # instantiate class 175 | retry = Retry(logging) 176 | 177 | # run functions 178 | retry.retry_security_events() 179 | -------------------------------------------------------------------------------- /auto_remediate_dlq/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlevit/aws-auto-remediate/949adb55809a122278a55c6503e19c9032b378ca/auto_remediate_dlq/test/__init__.py -------------------------------------------------------------------------------- /auto_remediate_dlq/test/data/config_payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "id": "a52f9595-4d85-6383-9371-4cf2ab6d8320", 4 | "detail-type": "Config Rules Compliance Change", 5 | "source": "aws.config", 6 | "account": "914242778834", 7 | "time": "2019-05-07T04:36:55Z", 8 | "region": "ap-southeast-2", 9 | "resources": [], 10 | "detail": { 11 | "resourceId": "vpc-00daeeb328891ac24", 12 | "awsRegion": "ap-southeast-2", 13 | "awsAccountId": "914242778834", 14 | "configRuleName": "securityhub-vpc-flow-logs-enabled-l6dseq", 15 | "recordVersion": "1.0", 16 | "configRuleARN": "arn:aws:config:ap-southeast-2:914242778834:config-rule/aws-service-rule/securityhub.amazonaws.com/config-rule-2fe8me", 17 | "messageType": "ComplianceChangeNotification", 18 | "newEvaluationResult": { 19 | "evaluationResultIdentifier": { 20 | "evaluationResultQualifier": { 21 | "configRuleName": "securityhub-vpc-flow-logs-enabled-l6dseq", 22 | "resourceType": "AWS::EC2::VPC", 23 | "resourceId": "vpc-00daeeb328891ac24" 24 | }, 25 | "orderingTimestamp": "2019-05-07T04:36:36.353Z" 26 | }, 27 | "complianceType": "NON_COMPLIANT", 28 | "resultRecordedTime": "2019-05-07T04:36:54.588Z", 29 | "configRuleInvokedTime": "2019-05-07T04:36:53.970Z" 30 | }, 31 | "notificationCreationTime": "2019-05-07T04:36:55.722Z", 32 | "resourceType": "AWS::EC2::VPC" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /auto_remediate_dlq/test/test_dlq.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import logging 4 | import os 5 | import py 6 | 7 | import moto 8 | import pytest 9 | 10 | from .. import lambda_handler 11 | 12 | 13 | class TestDeleteFromQueue: 14 | @pytest.fixture 15 | def retry(self): 16 | with moto.mock_sqs(): 17 | retry = lambda_handler.Retry(logging) 18 | yield retry 19 | 20 | def test_invalid_queue_url(self, retry): 21 | """Tests sending message to queue with invalid queue URL 22 | 23 | Arguments: 24 | retry {class} -- Instance of Retry class 25 | """ 26 | assert not retry.delete_from_queue("http://invalid_queue_url.com", "12345") 27 | 28 | def test_delete_from_queue(self, retry): 29 | """Tests deletion of a message from a queue 30 | 31 | Arguments: 32 | retry {class} -- Instance of Retry class 33 | """ 34 | # create queue 35 | retry.client_sqs.create_queue(QueueName="DEADLETTERQUEUE") 36 | 37 | # get queue url 38 | response = retry.client_sqs.get_queue_url(QueueName="DEADLETTERQUEUE") 39 | queue_url = response["QueueUrl"] 40 | 41 | # send message to queue 42 | retry.client_sqs.send_message(QueueUrl=queue_url, MessageBody="payload") 43 | 44 | # test message in queue 45 | response = retry.client_sqs.receive_message(QueueUrl=queue_url) 46 | assert len(response["Messages"]) == 1 47 | 48 | # test delete_from_queue function 49 | retry.delete_from_queue(queue_url, response["Messages"][0]["ReceiptHandle"]) 50 | 51 | response = retry.client_sqs.receive_message(QueueUrl=queue_url) 52 | 53 | # test no messages in queue 54 | assert "Messages" not in response 55 | 56 | 57 | class TestRetrySecurityEvents: 58 | @pytest.fixture 59 | def retry(self): 60 | with moto.mock_sqs(): 61 | retry = lambda_handler.Retry(logging) 62 | yield retry 63 | 64 | @pytest.fixture 65 | def test_config_payload(self): 66 | config_payload_file = "auto_remediate_dlq/test/data/config_payload.json" 67 | with open(config_payload_file, "r") as file: 68 | config_payload = file.read() 69 | 70 | yield json.loads(config_payload) 71 | 72 | def test_invalid_queue_url(self, retry): 73 | """Tests sending message to queue with invalid queue URL 74 | 75 | Arguments: 76 | retry {class} -- Instance of Retry class 77 | """ 78 | # create queue 79 | retry.client_sqs.create_queue(QueueName="DEADLETTERQUEUE") 80 | os.environ["DEADLETTERQUEUE"] = "http://invalid_queue_url.com" 81 | 82 | # test retry_security_events function 83 | assert not retry.retry_security_events() 84 | 85 | def test_retry_security_events(self, retry, test_config_payload): 86 | """Tests a "retry" of a security event. The test will retrieve a message from the DEADLETTERQUEUE 87 | and send that message to the COMPLIANCEQUEUE afterwich it'll delete the message 88 | from the DEADLETTERQUEUE 89 | 90 | Arguments: 91 | retry {class} -- Instance of Retry class 92 | test_config_payload {dictionary} -- Mock AWS Config payload 93 | """ 94 | # create queues 95 | retry.client_sqs.create_queue(QueueName="COMPLIANCEQUEUE") 96 | retry.client_sqs.create_queue(QueueName="DEADLETTERQUEUE") 97 | 98 | # get COMPLIANCEQUEUE url 99 | response = retry.client_sqs.get_queue_url(QueueName="COMPLIANCEQUEUE") 100 | compliance_queue_url = response["QueueUrl"] 101 | os.environ["COMPLIANCEQUEUE"] = compliance_queue_url 102 | 103 | # get DEADLETTERQUEUE url 104 | response = retry.client_sqs.get_queue_url(QueueName="DEADLETTERQUEUE") 105 | dlq_queue_url = response["QueueUrl"] 106 | os.environ["DEADLETTERQUEUE"] = dlq_queue_url 107 | 108 | retry.client_sqs.send_message( 109 | QueueUrl=dlq_queue_url, 110 | MessageBody=json.dumps(test_config_payload), 111 | MessageAttributes={"try_count": {"StringValue": "1", "DataType": "Number"}}, 112 | ) 113 | 114 | # test retry_security_events function 115 | retry.retry_security_events() 116 | 117 | # assert 0 messages in queue 118 | response = retry.client_sqs.receive_message(QueueUrl=dlq_queue_url) 119 | assert "Messages" not in response 120 | 121 | # assert 1 message in queue 122 | response = retry.client_sqs.receive_message(QueueUrl=compliance_queue_url) 123 | assert len(response["Messages"]) == 1 124 | 125 | 126 | class TestSendToComplianceQueue: 127 | @pytest.fixture 128 | def retry(self): 129 | with moto.mock_sqs(): 130 | retry = lambda_handler.Retry(logging) 131 | yield retry 132 | 133 | def test_invalid_queue_url(self, retry): 134 | """Tests sending message to queue with invalid queue URL 135 | 136 | Arguments: 137 | retry {class} -- Instance of Retry class 138 | """ 139 | # create queue 140 | retry.client_sqs.create_queue(QueueName="COMPLIANCEQUEUE") 141 | os.environ["COMPLIANCEQUEUE"] = "http://invalid_queue_url.com" 142 | 143 | # test send_to_compliance_queue function 144 | assert not retry.send_to_compliance_queue("payload", "1") 145 | 146 | def test_send_to_compliance_queue(self, retry): 147 | """Tests sending message to queue 148 | 149 | Arguments: 150 | retry {class} -- Instance of Retry class 151 | """ 152 | # create queue 153 | retry.client_sqs.create_queue(QueueName="COMPLIANCEQUEUE") 154 | 155 | # get queue url 156 | response = retry.client_sqs.get_queue_url(QueueName="COMPLIANCEQUEUE") 157 | queue_url = response["QueueUrl"] 158 | 159 | # set environment variable 160 | os.environ["COMPLIANCEQUEUE"] = queue_url 161 | 162 | # test send_to_compliance_queue function 163 | retry.send_to_compliance_queue("payload", "1") 164 | response = retry.client_sqs.receive_message( 165 | QueueUrl=os.environ["COMPLIANCEQUEUE"] 166 | ) 167 | 168 | # assert 1 message in queue 169 | assert len(response["Messages"]) == 1 170 | 171 | # assert payload 172 | assert response["Messages"][0]["Body"] == "payload" 173 | 174 | # assert try_count 175 | assert ( 176 | response["Messages"][0]["MessageAttributes"]["try_count"]["StringValue"] 177 | == "1" 178 | ) 179 | 180 | 181 | class TestStaticMethods: 182 | @pytest.fixture 183 | def retry(self): 184 | retry = lambda_handler.Retry(logging) 185 | yield retry 186 | 187 | @pytest.fixture 188 | def test_config_payload(self): 189 | config_payload_file = "auto_remediate_dlq/test/data/config_payload.json" 190 | with open(config_payload_file, "r") as file: 191 | config_payload = file.read() 192 | 193 | yield json.loads(config_payload) 194 | 195 | def test_get_config_rule_compliance(self, retry, test_config_payload): 196 | """Tests retrieval of Config Rule compliance 197 | 198 | Arguments: 199 | retry {class} -- Instance of Retry class 200 | test_config_payload {dictionary} -- AWS Config Payload 201 | """ 202 | # validate test 203 | assert retry.get_config_rule_compliance(test_config_payload) == "NON_COMPLIANT" 204 | 205 | def test_get_config_rule_name(self, retry, test_config_payload): 206 | """Tests retrieval of Config Rule name 207 | 208 | Arguments: 209 | retry {class} -- Instance of Retry class 210 | test_config_payload {dictionary} -- AWS Config Payload 211 | """ 212 | # validate test 213 | assert ( 214 | retry.get_config_rule_name(test_config_payload) 215 | == "securityhub-vpc-flow-logs-enabled-l6dseq" 216 | ) 217 | -------------------------------------------------------------------------------- /auto_remediate_setup/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlevit/aws-auto-remediate/949adb55809a122278a55c6503e19c9032b378ca/auto_remediate_setup/__init__.py -------------------------------------------------------------------------------- /auto_remediate_setup/data/auto-remediate-settings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": { 4 | "S": "version" 5 | }, 6 | "value": { 7 | "N": "1.0" 8 | } 9 | }, 10 | { 11 | "key": { 12 | "S": "general" 13 | }, 14 | "value": { 15 | "M": { 16 | "dry_run": { 17 | "BOOL": true 18 | } 19 | } 20 | } 21 | }, 22 | { 23 | "key": { 24 | "S": "rules" 25 | }, 26 | "value": { 27 | "M": { 28 | "cloudtrail-enabled": { 29 | "M": { 30 | "remediate": { 31 | "BOOL": true 32 | }, 33 | "deploy": { 34 | "BOOL": true 35 | } 36 | } 37 | }, 38 | "db-instance-backup-enabled": { 39 | "M": { 40 | "remediate": { 41 | "BOOL": true 42 | }, 43 | "deploy": { 44 | "BOOL": true 45 | } 46 | } 47 | }, 48 | "dynamodb-table-encryption-enabled": { 49 | "M": { 50 | "remediate": { 51 | "BOOL": true 52 | }, 53 | "deploy": { 54 | "BOOL": true 55 | } 56 | } 57 | }, 58 | "ec2-instances-in-vpc": { 59 | "M": { 60 | "remediate": { 61 | "BOOL": true 62 | }, 63 | "deploy": { 64 | "BOOL": true 65 | } 66 | } 67 | }, 68 | "encrypted-volumes": { 69 | "M": { 70 | "remediate": { 71 | "BOOL": true 72 | }, 73 | "deploy": { 74 | "BOOL": true 75 | } 76 | } 77 | }, 78 | "guardduty-enabled-centralized": { 79 | "M": { 80 | "remediate": { 81 | "BOOL": true 82 | }, 83 | "deploy": { 84 | "BOOL": true 85 | } 86 | } 87 | }, 88 | "lambda-function-public-access-prohibited": { 89 | "M": { 90 | "remediate": { 91 | "BOOL": true 92 | }, 93 | "deploy": { 94 | "BOOL": true 95 | } 96 | } 97 | }, 98 | "rds-instance-public-access-check": { 99 | "M": { 100 | "remediate": { 101 | "BOOL": true 102 | }, 103 | "deploy": { 104 | "BOOL": true 105 | } 106 | } 107 | }, 108 | "rds-multi-az-support": { 109 | "M": { 110 | "remediate": { 111 | "BOOL": true 112 | }, 113 | "deploy": { 114 | "BOOL": true 115 | } 116 | } 117 | }, 118 | "rds-snapshots-public-prohibited": { 119 | "M": { 120 | "remediate": { 121 | "BOOL": true 122 | }, 123 | "deploy": { 124 | "BOOL": true 125 | } 126 | } 127 | }, 128 | "rds-storage-encrypted": { 129 | "M": { 130 | "remediate": { 131 | "BOOL": true 132 | }, 133 | "deploy": { 134 | "BOOL": true 135 | } 136 | } 137 | }, 138 | "s3-bucket-server-side-encryption-enabled": { 139 | "M": { 140 | "remediate": { 141 | "BOOL": true 142 | }, 143 | "deploy": { 144 | "BOOL": true 145 | } 146 | } 147 | }, 148 | "s3-bucket-ssl-requests-only": { 149 | "M": { 150 | "remediate": { 151 | "BOOL": true 152 | }, 153 | "deploy": { 154 | "BOOL": true 155 | } 156 | } 157 | }, 158 | "securityhub-access-keys-rotated": { 159 | "M": { 160 | "remediate": { 161 | "BOOL": true 162 | } 163 | } 164 | }, 165 | "securityhub-cloud-trail-cloud-watch-logs-enabled": { 166 | "M": { 167 | "remediate": { 168 | "BOOL": true 169 | } 170 | } 171 | }, 172 | "securityhub-cloud-trail-encryption-enabled": { 173 | "M": { 174 | "remediate": { 175 | "BOOL": true 176 | } 177 | } 178 | }, 179 | "securityhub-cloud-trail-log-file-validation-enabled": { 180 | "M": { 181 | "remediate": { 182 | "BOOL": true 183 | } 184 | } 185 | }, 186 | "securityhub-cmk-backing-key-rotation-enabled": { 187 | "M": { 188 | "remediate": { 189 | "BOOL": true 190 | } 191 | } 192 | }, 193 | "securityhub-iam-password-policy-ensure-expires": { 194 | "M": { 195 | "remediate": { 196 | "BOOL": true 197 | } 198 | } 199 | }, 200 | "securityhub-iam-password-policy-lowercase-letter-check": { 201 | "M": { 202 | "remediate": { 203 | "BOOL": true 204 | } 205 | } 206 | }, 207 | "securityhub-iam-password-policy-minimum-length-check": { 208 | "M": { 209 | "remediate": { 210 | "BOOL": true 211 | } 212 | } 213 | }, 214 | "securityhub-iam-password-policy-number-check": { 215 | "M": { 216 | "remediate": { 217 | "BOOL": true 218 | } 219 | } 220 | }, 221 | "securityhub-iam-password-policy-prevent-reuse-check": { 222 | "M": { 223 | "remediate": { 224 | "BOOL": true 225 | } 226 | } 227 | }, 228 | "securityhub-iam-password-policy-symbol-check": { 229 | "M": { 230 | "remediate": { 231 | "BOOL": true 232 | } 233 | } 234 | }, 235 | "securityhub-iam-password-policy-uppercase-letter-check": { 236 | "M": { 237 | "remediate": { 238 | "BOOL": true 239 | } 240 | } 241 | }, 242 | "securityhub-iam-policy-no-statements-with-admin-access": { 243 | "M": { 244 | "remediate": { 245 | "BOOL": true 246 | } 247 | } 248 | }, 249 | "securityhub-iam-user-no-policies-check": { 250 | "M": { 251 | "remediate": { 252 | "BOOL": true 253 | } 254 | } 255 | }, 256 | "securityhub-iam-user-unused-credentials-check": { 257 | "M": { 258 | "remediate": { 259 | "BOOL": true 260 | } 261 | } 262 | }, 263 | "securityhub-mfa-enabled-for-iam-console-access": { 264 | "M": { 265 | "remediate": { 266 | "BOOL": true 267 | } 268 | } 269 | }, 270 | "securityhub-multi-region-cloud-trail-enabled": { 271 | "M": { 272 | "remediate": { 273 | "BOOL": true 274 | } 275 | } 276 | }, 277 | "securityhub-restricted-rdp": { 278 | "M": { 279 | "remediate": { 280 | "BOOL": true 281 | } 282 | } 283 | }, 284 | "securityhub-restricted-ssh": { 285 | "M": { 286 | "remediate": { 287 | "BOOL": true 288 | } 289 | } 290 | }, 291 | "securityhub-s3-bucket-logging-enabled": { 292 | "M": { 293 | "remediate": { 294 | "BOOL": true 295 | } 296 | } 297 | }, 298 | "securityhub-s3-bucket-public-read-prohibited": { 299 | "M": { 300 | "remediate": { 301 | "BOOL": true 302 | } 303 | } 304 | }, 305 | "securityhub-s3-bucket-public-write-prohibited": { 306 | "M": { 307 | "remediate": { 308 | "BOOL": true 309 | } 310 | } 311 | }, 312 | "securityhub-vpc-default-security-group-closed": { 313 | "M": { 314 | "remediate": { 315 | "BOOL": true 316 | } 317 | } 318 | }, 319 | "securityhub-vpc-flow-logs-enabled": { 320 | "M": { 321 | "remediate": { 322 | "BOOL": true 323 | } 324 | } 325 | } 326 | } 327 | } 328 | } 329 | ] 330 | -------------------------------------------------------------------------------- /auto_remediate_setup/data/config_rules/cloudtrail-enabled.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "AWSConfigRule": { 4 | "Type": "AWS::Config::ConfigRule", 5 | "Properties": { 6 | "ConfigRuleName": { 7 | "Ref": "ConfigRuleName" 8 | }, 9 | "Description": "Checks whether AWS CloudTrail is enabled in your AWS account.", 10 | "InputParameters": { 11 | "s3BucketName": { 12 | "Fn::If": [ 13 | "s3BucketName", 14 | { 15 | "Ref": "s3BucketName" 16 | }, 17 | { 18 | "Ref": "AWS::NoValue" 19 | } 20 | ] 21 | }, 22 | "snsTopicArn": { 23 | "Fn::If": [ 24 | "snsTopicArn", 25 | { 26 | "Ref": "snsTopicArn" 27 | }, 28 | { 29 | "Ref": "AWS::NoValue" 30 | } 31 | ] 32 | }, 33 | "cloudWatchLogsLogGroupArn": { 34 | "Fn::If": [ 35 | "cloudWatchLogsLogGroupArn", 36 | { 37 | "Ref": "cloudWatchLogsLogGroupArn" 38 | }, 39 | { 40 | "Ref": "AWS::NoValue" 41 | } 42 | ] 43 | } 44 | }, 45 | "Scope": {}, 46 | "Source": { 47 | "Owner": "AWS", 48 | "SourceIdentifier": "CLOUD_TRAIL_ENABLED" 49 | }, 50 | "MaximumExecutionFrequency": { 51 | "Ref": "MaximumExecutionFrequency" 52 | } 53 | } 54 | } 55 | }, 56 | "Parameters": { 57 | "ConfigRuleName": { 58 | "Type": "String", 59 | "Default": "cloudtrail-enabled", 60 | "Description": "The name that you assign to the AWS Config rule.", 61 | "MinLength": "1", 62 | "ConstraintDescription": "This parameter is required." 63 | }, 64 | "MaximumExecutionFrequency": { 65 | "Type": "String", 66 | "Default": "TwentyFour_Hours", 67 | "Description": "The frequency that you want AWS Config to run evaluations for the rule.", 68 | "MinLength": "1", 69 | "ConstraintDescription": "This parameter is required.", 70 | "AllowedValues": [ 71 | "One_Hour", 72 | "Three_Hours", 73 | "Six_Hours", 74 | "Twelve_Hours", 75 | "TwentyFour_Hours" 76 | ] 77 | }, 78 | "s3BucketName": { 79 | "Type": "String", 80 | "Default": "", 81 | "Description": "Name of S3 bucket for CloudTrail to deliver log files to." 82 | }, 83 | "snsTopicArn": { 84 | "Type": "String", 85 | "Default": "", 86 | "Description": "SNS topic ARN for CloudTrail to use for notifications." 87 | }, 88 | "cloudWatchLogsLogGroupArn": { 89 | "Type": "String", 90 | "Default": "", 91 | "Description": "CloudWatch log group ARN for CloudTrail to send data to." 92 | } 93 | }, 94 | "Metadata": { 95 | "AWS::CloudFormation::Interface": { 96 | "ParameterGroups": [ 97 | { 98 | "Label": { 99 | "default": "Required" 100 | }, 101 | "Parameters": [] 102 | }, 103 | { 104 | "Label": { 105 | "default": "Optional" 106 | }, 107 | "Parameters": [ 108 | "s3BucketName", 109 | "snsTopicArn", 110 | "cloudWatchLogsLogGroupArn" 111 | ] 112 | } 113 | ] 114 | } 115 | }, 116 | "Conditions": { 117 | "s3BucketName": { 118 | "Fn::Not": [ 119 | { 120 | "Fn::Equals": [ 121 | "", 122 | { 123 | "Ref": "s3BucketName" 124 | } 125 | ] 126 | } 127 | ] 128 | }, 129 | "snsTopicArn": { 130 | "Fn::Not": [ 131 | { 132 | "Fn::Equals": [ 133 | "", 134 | { 135 | "Ref": "snsTopicArn" 136 | } 137 | ] 138 | } 139 | ] 140 | }, 141 | "cloudWatchLogsLogGroupArn": { 142 | "Fn::Not": [ 143 | { 144 | "Fn::Equals": [ 145 | "", 146 | { 147 | "Ref": "cloudWatchLogsLogGroupArn" 148 | } 149 | ] 150 | } 151 | ] 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /auto_remediate_setup/data/config_rules/db-instance-backup-enabled.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "AWSConfigRule": { 4 | "Type": "AWS::Config::ConfigRule", 5 | "Properties": { 6 | "ConfigRuleName": { 7 | "Ref": "ConfigRuleName" 8 | }, 9 | "Description": "Checks whether RDS DB instances have backups enabled.", 10 | "InputParameters": { 11 | "backupRetentionPeriod": { 12 | "Fn::If": [ 13 | "backupRetentionPeriod", 14 | { 15 | "Ref": "backupRetentionPeriod" 16 | }, 17 | { 18 | "Ref": "AWS::NoValue" 19 | } 20 | ] 21 | }, 22 | "preferredBackupWindow": { 23 | "Fn::If": [ 24 | "preferredBackupWindow", 25 | { 26 | "Ref": "preferredBackupWindow" 27 | }, 28 | { 29 | "Ref": "AWS::NoValue" 30 | } 31 | ] 32 | }, 33 | "checkReadReplicas": { 34 | "Fn::If": [ 35 | "checkReadReplicas", 36 | { 37 | "Ref": "checkReadReplicas" 38 | }, 39 | { 40 | "Ref": "AWS::NoValue" 41 | } 42 | ] 43 | } 44 | }, 45 | "Scope": { 46 | "ComplianceResourceTypes": ["AWS::RDS::DBInstance"] 47 | }, 48 | "Source": { 49 | "Owner": "AWS", 50 | "SourceIdentifier": "DB_INSTANCE_BACKUP_ENABLED" 51 | } 52 | } 53 | } 54 | }, 55 | "Parameters": { 56 | "ConfigRuleName": { 57 | "Type": "String", 58 | "Default": "db-instance-backup-enabled", 59 | "Description": "The name that you assign to the AWS Config rule.", 60 | "MinLength": "1", 61 | "ConstraintDescription": "This parameter is required." 62 | }, 63 | "backupRetentionPeriod": { 64 | "Type": "String", 65 | "Default": "", 66 | "Description": "Retention period for backups." 67 | }, 68 | "preferredBackupWindow": { 69 | "Type": "String", 70 | "Default": "", 71 | "Description": "Time range in which backups are created." 72 | }, 73 | "checkReadReplicas": { 74 | "Type": "String", 75 | "Default": "", 76 | "Description": "Checks whether RDS DB instances have backups enabled for read replicas." 77 | } 78 | }, 79 | "Metadata": { 80 | "AWS::CloudFormation::Interface": { 81 | "ParameterGroups": [ 82 | { 83 | "Label": { 84 | "default": "Required" 85 | }, 86 | "Parameters": [] 87 | }, 88 | { 89 | "Label": { 90 | "default": "Optional" 91 | }, 92 | "Parameters": [ 93 | "backupRetentionPeriod", 94 | "preferredBackupWindow", 95 | "checkReadReplicas" 96 | ] 97 | } 98 | ] 99 | } 100 | }, 101 | "Conditions": { 102 | "backupRetentionPeriod": { 103 | "Fn::Not": [ 104 | { 105 | "Fn::Equals": [ 106 | "", 107 | { 108 | "Ref": "backupRetentionPeriod" 109 | } 110 | ] 111 | } 112 | ] 113 | }, 114 | "preferredBackupWindow": { 115 | "Fn::Not": [ 116 | { 117 | "Fn::Equals": [ 118 | "", 119 | { 120 | "Ref": "preferredBackupWindow" 121 | } 122 | ] 123 | } 124 | ] 125 | }, 126 | "checkReadReplicas": { 127 | "Fn::Not": [ 128 | { 129 | "Fn::Equals": [ 130 | "", 131 | { 132 | "Ref": "checkReadReplicas" 133 | } 134 | ] 135 | } 136 | ] 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /auto_remediate_setup/data/config_rules/dynamodb-table-encryption-enabled.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "AWSConfigRule": { 4 | "Type": "AWS::Config::ConfigRule", 5 | "Properties": { 6 | "ConfigRuleName": { 7 | "Ref": "ConfigRuleName" 8 | }, 9 | "Description": "Checks whether the Amazon DynamoDB tables are encrypted and checks their status. The rule is compliant if the status is enabled or enabling.", 10 | "InputParameters": {}, 11 | "Scope": { 12 | "ComplianceResourceTypes": ["AWS::DynamoDB::Table"] 13 | }, 14 | "Source": { 15 | "Owner": "AWS", 16 | "SourceIdentifier": "DYNAMODB_TABLE_ENCRYPTION_ENABLED" 17 | } 18 | } 19 | } 20 | }, 21 | "Parameters": { 22 | "ConfigRuleName": { 23 | "Type": "String", 24 | "Default": "dynamodb-table-encryption-enabled", 25 | "Description": "The name that you assign to the AWS Config rule.", 26 | "MinLength": "1", 27 | "ConstraintDescription": "This parameter is required." 28 | } 29 | }, 30 | "Metadata": { 31 | "AWS::CloudFormation::Interface": { 32 | "ParameterGroups": [ 33 | { 34 | "Label": { 35 | "default": "Required" 36 | }, 37 | "Parameters": [] 38 | }, 39 | { 40 | "Label": { 41 | "default": "Optional" 42 | }, 43 | "Parameters": [] 44 | } 45 | ] 46 | } 47 | }, 48 | "Conditions": {} 49 | } 50 | -------------------------------------------------------------------------------- /auto_remediate_setup/data/config_rules/ec2-instances-in-vpc.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "AWSConfigRule": { 4 | "Type": "AWS::Config::ConfigRule", 5 | "Properties": { 6 | "ConfigRuleName": { 7 | "Ref": "ConfigRuleName" 8 | }, 9 | "Description": "Checks whether your EC2 instances belong to a virtual private cloud (VPC).", 10 | "InputParameters": { 11 | "vpcId": { 12 | "Fn::If": [ 13 | "vpcId", 14 | { 15 | "Ref": "vpcId" 16 | }, 17 | { 18 | "Ref": "AWS::NoValue" 19 | } 20 | ] 21 | } 22 | }, 23 | "Scope": { 24 | "ComplianceResourceTypes": ["AWS::EC2::Instance"] 25 | }, 26 | "Source": { 27 | "Owner": "AWS", 28 | "SourceIdentifier": "INSTANCES_IN_VPC" 29 | } 30 | } 31 | } 32 | }, 33 | "Parameters": { 34 | "ConfigRuleName": { 35 | "Type": "String", 36 | "Default": "ec2-instances-in-vpc", 37 | "Description": "The name that you assign to the AWS Config rule.", 38 | "MinLength": "1", 39 | "ConstraintDescription": "This parameter is required." 40 | }, 41 | "vpcId": { 42 | "Type": "String", 43 | "Default": "", 44 | "Description": "VPC ID that contains these EC2 instances." 45 | } 46 | }, 47 | "Metadata": { 48 | "AWS::CloudFormation::Interface": { 49 | "ParameterGroups": [ 50 | { 51 | "Label": { 52 | "default": "Required" 53 | }, 54 | "Parameters": [] 55 | }, 56 | { 57 | "Label": { 58 | "default": "Optional" 59 | }, 60 | "Parameters": ["vpcId"] 61 | } 62 | ] 63 | } 64 | }, 65 | "Conditions": { 66 | "vpcId": { 67 | "Fn::Not": [ 68 | { 69 | "Fn::Equals": [ 70 | "", 71 | { 72 | "Ref": "vpcId" 73 | } 74 | ] 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /auto_remediate_setup/data/config_rules/encrypted-volumes.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "AWSConfigRule": { 4 | "Type": "AWS::Config::ConfigRule", 5 | "Properties": { 6 | "ConfigRuleName": { 7 | "Ref": "ConfigRuleName" 8 | }, 9 | "Description": "Checks whether EBS volumes that are in an attached state are encrypted.", 10 | "InputParameters": { 11 | "kmsId": { 12 | "Fn::If": [ 13 | "kmsId", 14 | { 15 | "Ref": "kmsId" 16 | }, 17 | { 18 | "Ref": "AWS::NoValue" 19 | } 20 | ] 21 | } 22 | }, 23 | "Scope": { 24 | "ComplianceResourceTypes": ["AWS::EC2::Volume"] 25 | }, 26 | "Source": { 27 | "Owner": "AWS", 28 | "SourceIdentifier": "ENCRYPTED_VOLUMES" 29 | } 30 | } 31 | } 32 | }, 33 | "Parameters": { 34 | "ConfigRuleName": { 35 | "Type": "String", 36 | "Default": "encrypted-volumes", 37 | "Description": "The name that you assign to the AWS Config rule.", 38 | "MinLength": "1", 39 | "ConstraintDescription": "This parameter is required." 40 | }, 41 | "kmsId": { 42 | "Type": "String", 43 | "Default": "", 44 | "Description": "ID or ARN of the KMS key that is used to encrypt the volume." 45 | } 46 | }, 47 | "Metadata": { 48 | "AWS::CloudFormation::Interface": { 49 | "ParameterGroups": [ 50 | { 51 | "Label": { 52 | "default": "Required" 53 | }, 54 | "Parameters": [] 55 | }, 56 | { 57 | "Label": { 58 | "default": "Optional" 59 | }, 60 | "Parameters": ["kmsId"] 61 | } 62 | ] 63 | } 64 | }, 65 | "Conditions": { 66 | "kmsId": { 67 | "Fn::Not": [ 68 | { 69 | "Fn::Equals": [ 70 | "", 71 | { 72 | "Ref": "kmsId" 73 | } 74 | ] 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /auto_remediate_setup/data/config_rules/guardduty-enabled-centralized.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "AWSConfigRule": { 4 | "Type": "AWS::Config::ConfigRule", 5 | "Properties": { 6 | "ConfigRuleName": { 7 | "Ref": "ConfigRuleName" 8 | }, 9 | "Description": "Checks whether GuardDuty is enabled. You can optionally verify that the results are centralized in a specific AWS Account.", 10 | "InputParameters": { 11 | "CentralMonitoringAccount": { 12 | "Fn::If": [ 13 | "CentralMonitoringAccount", 14 | { 15 | "Ref": "CentralMonitoringAccount" 16 | }, 17 | { 18 | "Ref": "AWS::NoValue" 19 | } 20 | ] 21 | } 22 | }, 23 | "Scope": {}, 24 | "Source": { 25 | "Owner": "AWS", 26 | "SourceIdentifier": "GUARDDUTY_ENABLED_CENTRALIZED" 27 | }, 28 | "MaximumExecutionFrequency": { 29 | "Ref": "MaximumExecutionFrequency" 30 | } 31 | } 32 | } 33 | }, 34 | "Parameters": { 35 | "ConfigRuleName": { 36 | "Type": "String", 37 | "Default": "guardduty-enabled-centralized", 38 | "Description": "The name that you assign to the AWS Config rule.", 39 | "MinLength": "1", 40 | "ConstraintDescription": "This parameter is required." 41 | }, 42 | "MaximumExecutionFrequency": { 43 | "Type": "String", 44 | "Default": "TwentyFour_Hours", 45 | "Description": "The frequency that you want AWS Config to run evaluations for the rule.", 46 | "MinLength": "1", 47 | "ConstraintDescription": "This parameter is required.", 48 | "AllowedValues": [ 49 | "One_Hour", 50 | "Three_Hours", 51 | "Six_Hours", 52 | "Twelve_Hours", 53 | "TwentyFour_Hours" 54 | ] 55 | }, 56 | "CentralMonitoringAccount": { 57 | "Type": "String", 58 | "Default": "", 59 | "Description": "Specify the AWS Account (12-digit) where the GuardDuty results should be centralized." 60 | } 61 | }, 62 | "Metadata": { 63 | "AWS::CloudFormation::Interface": { 64 | "ParameterGroups": [ 65 | { 66 | "Label": { 67 | "default": "Required" 68 | }, 69 | "Parameters": [] 70 | }, 71 | { 72 | "Label": { 73 | "default": "Optional" 74 | }, 75 | "Parameters": ["CentralMonitoringAccount"] 76 | } 77 | ] 78 | } 79 | }, 80 | "Conditions": { 81 | "CentralMonitoringAccount": { 82 | "Fn::Not": [ 83 | { 84 | "Fn::Equals": [ 85 | "", 86 | { 87 | "Ref": "CentralMonitoringAccount" 88 | } 89 | ] 90 | } 91 | ] 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /auto_remediate_setup/data/config_rules/lambda-function-public-access-prohibited.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "AWSConfigRule": { 4 | "Type": "AWS::Config::ConfigRule", 5 | "Properties": { 6 | "ConfigRuleName": { 7 | "Ref": "ConfigRuleName" 8 | }, 9 | "Description": "Checks whether the Lambda function policy prohibits public access.", 10 | "InputParameters": {}, 11 | "Scope": { 12 | "ComplianceResourceTypes": ["AWS::Lambda::Function"] 13 | }, 14 | "Source": { 15 | "Owner": "AWS", 16 | "SourceIdentifier": "LAMBDA_FUNCTION_PUBLIC_ACCESS_PROHIBITED" 17 | } 18 | } 19 | } 20 | }, 21 | "Parameters": { 22 | "ConfigRuleName": { 23 | "Type": "String", 24 | "Default": "lambda-function-public-access-prohibited", 25 | "Description": "The name that you assign to the AWS Config rule.", 26 | "MinLength": "1", 27 | "ConstraintDescription": "This parameter is required." 28 | } 29 | }, 30 | "Metadata": { 31 | "AWS::CloudFormation::Interface": { 32 | "ParameterGroups": [ 33 | { 34 | "Label": { 35 | "default": "Required" 36 | }, 37 | "Parameters": [] 38 | }, 39 | { 40 | "Label": { 41 | "default": "Optional" 42 | }, 43 | "Parameters": [] 44 | } 45 | ] 46 | } 47 | }, 48 | "Conditions": {} 49 | } 50 | -------------------------------------------------------------------------------- /auto_remediate_setup/data/config_rules/rds-instance-public-access-check.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "AWSConfigRule": { 4 | "Type": "AWS::Config::ConfigRule", 5 | "Properties": { 6 | "ConfigRuleName": { 7 | "Ref": "ConfigRuleName" 8 | }, 9 | "Description": "Checks whether the Amazon Relational Database Service (RDS) instances are not publicly accessible. The rule is non-compliant if the publiclyAccessible field is true in the instance configuration item.", 10 | "InputParameters": {}, 11 | "Scope": { 12 | "ComplianceResourceTypes": ["AWS::RDS::DBInstance"] 13 | }, 14 | "Source": { 15 | "Owner": "AWS", 16 | "SourceIdentifier": "RDS_INSTANCE_PUBLIC_ACCESS_CHECK" 17 | } 18 | } 19 | } 20 | }, 21 | "Parameters": { 22 | "ConfigRuleName": { 23 | "Type": "String", 24 | "Default": "rds-instance-public-access-check", 25 | "Description": "The name that you assign to the AWS Config rule.", 26 | "MinLength": "1", 27 | "ConstraintDescription": "This parameter is required." 28 | } 29 | }, 30 | "Metadata": { 31 | "AWS::CloudFormation::Interface": { 32 | "ParameterGroups": [ 33 | { 34 | "Label": { 35 | "default": "Required" 36 | }, 37 | "Parameters": [] 38 | }, 39 | { 40 | "Label": { 41 | "default": "Optional" 42 | }, 43 | "Parameters": [] 44 | } 45 | ] 46 | } 47 | }, 48 | "Conditions": {} 49 | } 50 | -------------------------------------------------------------------------------- /auto_remediate_setup/data/config_rules/rds-multi-az-support.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "AWSConfigRule": { 4 | "Type": "AWS::Config::ConfigRule", 5 | "Properties": { 6 | "ConfigRuleName": { 7 | "Ref": "ConfigRuleName" 8 | }, 9 | "Description": "Checks whether high availability is enabled for your RDS DB instances.", 10 | "InputParameters": {}, 11 | "Scope": { 12 | "ComplianceResourceTypes": ["AWS::RDS::DBInstance"] 13 | }, 14 | "Source": { 15 | "Owner": "AWS", 16 | "SourceIdentifier": "RDS_MULTI_AZ_SUPPORT" 17 | } 18 | } 19 | } 20 | }, 21 | "Parameters": { 22 | "ConfigRuleName": { 23 | "Type": "String", 24 | "Default": "rds-multi-az-support", 25 | "Description": "The name that you assign to the AWS Config rule.", 26 | "MinLength": "1", 27 | "ConstraintDescription": "This parameter is required." 28 | } 29 | }, 30 | "Metadata": { 31 | "AWS::CloudFormation::Interface": { 32 | "ParameterGroups": [ 33 | { 34 | "Label": { 35 | "default": "Required" 36 | }, 37 | "Parameters": [] 38 | }, 39 | { 40 | "Label": { 41 | "default": "Optional" 42 | }, 43 | "Parameters": [] 44 | } 45 | ] 46 | } 47 | }, 48 | "Conditions": {} 49 | } 50 | -------------------------------------------------------------------------------- /auto_remediate_setup/data/config_rules/rds-snapshots-public-prohibited.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "AWSConfigRule": { 4 | "Type": "AWS::Config::ConfigRule", 5 | "Properties": { 6 | "ConfigRuleName": { 7 | "Ref": "ConfigRuleName" 8 | }, 9 | "Description": "Checks if Amazon Relational Database Service (Amazon RDS) snapshots are public. The rule is non-compliant if any existing and new Amazon RDS snapshots are public.", 10 | "InputParameters": {}, 11 | "Scope": { 12 | "ComplianceResourceTypes": ["AWS::RDS::DBSnapshot"] 13 | }, 14 | "Source": { 15 | "Owner": "AWS", 16 | "SourceIdentifier": "RDS_SNAPSHOTS_PUBLIC_PROHIBITED" 17 | } 18 | } 19 | } 20 | }, 21 | "Parameters": { 22 | "ConfigRuleName": { 23 | "Type": "String", 24 | "Default": "rds-snapshots-public-prohibited", 25 | "Description": "The name that you assign to the AWS Config rule.", 26 | "MinLength": "1", 27 | "ConstraintDescription": "This parameter is required." 28 | } 29 | }, 30 | "Metadata": { 31 | "AWS::CloudFormation::Interface": { 32 | "ParameterGroups": [ 33 | { 34 | "Label": { 35 | "default": "Required" 36 | }, 37 | "Parameters": [] 38 | }, 39 | { 40 | "Label": { 41 | "default": "Optional" 42 | }, 43 | "Parameters": [] 44 | } 45 | ] 46 | } 47 | }, 48 | "Conditions": {} 49 | } 50 | -------------------------------------------------------------------------------- /auto_remediate_setup/data/config_rules/rds-storage-encrypted.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "AWSConfigRule": { 4 | "Type": "AWS::Config::ConfigRule", 5 | "Properties": { 6 | "ConfigRuleName": { 7 | "Ref": "ConfigRuleName" 8 | }, 9 | "Description": "Checks whether storage encryption is enabled for your RDS DB instances.", 10 | "InputParameters": { 11 | "kmsKeyId": { 12 | "Fn::If": [ 13 | "kmsKeyId", 14 | { 15 | "Ref": "kmsKeyId" 16 | }, 17 | { 18 | "Ref": "AWS::NoValue" 19 | } 20 | ] 21 | } 22 | }, 23 | "Scope": { 24 | "ComplianceResourceTypes": ["AWS::RDS::DBInstance"] 25 | }, 26 | "Source": { 27 | "Owner": "AWS", 28 | "SourceIdentifier": "RDS_STORAGE_ENCRYPTED" 29 | } 30 | } 31 | } 32 | }, 33 | "Parameters": { 34 | "ConfigRuleName": { 35 | "Type": "String", 36 | "Default": "rds-storage-encrypted", 37 | "Description": "The name that you assign to the AWS Config rule.", 38 | "MinLength": "1", 39 | "ConstraintDescription": "This parameter is required." 40 | }, 41 | "kmsKeyId": { 42 | "Type": "String", 43 | "Default": "" 44 | } 45 | }, 46 | "Metadata": { 47 | "AWS::CloudFormation::Interface": { 48 | "ParameterGroups": [ 49 | { 50 | "Label": { 51 | "default": "Required" 52 | }, 53 | "Parameters": [] 54 | }, 55 | { 56 | "Label": { 57 | "default": "Optional" 58 | }, 59 | "Parameters": ["kmsKeyId"] 60 | } 61 | ] 62 | } 63 | }, 64 | "Conditions": { 65 | "kmsKeyId": { 66 | "Fn::Not": [ 67 | { 68 | "Fn::Equals": [ 69 | "", 70 | { 71 | "Ref": "kmsKeyId" 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /auto_remediate_setup/data/config_rules/s3-bucket-server-side-encryption-enabled.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "AWSConfigRule": { 4 | "Type": "AWS::Config::ConfigRule", 5 | "Properties": { 6 | "ConfigRuleName": { 7 | "Ref": "ConfigRuleName" 8 | }, 9 | "Description": "Checks that your Amazon S3 bucket either has S3 default encryption enabled or that the S3 bucket policy explicitly denies put-object requests without server side encryption.", 10 | "InputParameters": {}, 11 | "Scope": { 12 | "ComplianceResourceTypes": ["AWS::S3::Bucket"] 13 | }, 14 | "Source": { 15 | "Owner": "AWS", 16 | "SourceIdentifier": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED" 17 | } 18 | } 19 | } 20 | }, 21 | "Parameters": { 22 | "ConfigRuleName": { 23 | "Type": "String", 24 | "Default": "s3-bucket-server-side-encryption-enabled", 25 | "Description": "The name that you assign to the AWS Config rule.", 26 | "MinLength": "1", 27 | "ConstraintDescription": "This parameter is required." 28 | } 29 | }, 30 | "Metadata": { 31 | "AWS::CloudFormation::Interface": { 32 | "ParameterGroups": [ 33 | { 34 | "Label": { 35 | "default": "Required" 36 | }, 37 | "Parameters": [] 38 | }, 39 | { 40 | "Label": { 41 | "default": "Optional" 42 | }, 43 | "Parameters": [] 44 | } 45 | ] 46 | } 47 | }, 48 | "Conditions": {} 49 | } 50 | -------------------------------------------------------------------------------- /auto_remediate_setup/data/config_rules/s3-bucket-ssl-requests-only.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "AWSConfigRule": { 4 | "Type": "AWS::Config::ConfigRule", 5 | "Properties": { 6 | "ConfigRuleName": { 7 | "Ref": "ConfigRuleName" 8 | }, 9 | "Description": "Checks whether S3 buckets have policies that require requests to use Secure Socket Layer (SSL).", 10 | "InputParameters": {}, 11 | "Scope": { 12 | "ComplianceResourceTypes": ["AWS::S3::Bucket"] 13 | }, 14 | "Source": { 15 | "Owner": "AWS", 16 | "SourceIdentifier": "S3_BUCKET_SSL_REQUESTS_ONLY" 17 | } 18 | } 19 | } 20 | }, 21 | "Parameters": { 22 | "ConfigRuleName": { 23 | "Type": "String", 24 | "Default": "s3-bucket-ssl-requests-only", 25 | "Description": "The name that you assign to the AWS Config rule.", 26 | "MinLength": "1", 27 | "ConstraintDescription": "This parameter is required." 28 | } 29 | }, 30 | "Metadata": { 31 | "AWS::CloudFormation::Interface": { 32 | "ParameterGroups": [ 33 | { 34 | "Label": { 35 | "default": "Required" 36 | }, 37 | "Parameters": [] 38 | }, 39 | { 40 | "Label": { 41 | "default": "Optional" 42 | }, 43 | "Parameters": [] 44 | } 45 | ] 46 | } 47 | }, 48 | "Conditions": {} 49 | } 50 | -------------------------------------------------------------------------------- /auto_remediate_setup/data/custom_rules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlevit/aws-auto-remediate/949adb55809a122278a55c6503e19c9032b378ca/auto_remediate_setup/data/custom_rules/__init__.py -------------------------------------------------------------------------------- /auto_remediate_setup/lambda_handler.py: -------------------------------------------------------------------------------- 1 | import fnmatch 2 | import json 3 | import logging 4 | import os 5 | import sys 6 | 7 | import boto3 8 | from dynamodb_json import json_util as dynamodb_json 9 | 10 | 11 | class Setup: 12 | def __init__(self, logging): 13 | # parameters 14 | self.logging = logging 15 | 16 | self._client_cloudformation = None 17 | self._client_dynamodb = None 18 | self._client_sts = None 19 | 20 | @property 21 | def client_sts(self): 22 | if not self._client_sts: 23 | self._client_sts = boto3.client("sts") 24 | return self._client_sts 25 | 26 | @property 27 | def region(self): 28 | if self.client_sts.meta.region_name != "aws-global": 29 | return self.client_sts.meta.region_name 30 | else: 31 | return "us-east-1" 32 | 33 | @property 34 | def client_cloudformation(self): 35 | if not self._client_cloudformation: 36 | self._client_cloudformation = boto3.client("cloudformation", self.region) 37 | return self._client_cloudformation 38 | 39 | @property 40 | def client_dynamodb(self): 41 | if not self._client_dynamodb: 42 | self._client_dynamodb = boto3.client("dynamodb", self.region) 43 | return self._client_dynamodb 44 | 45 | def create_stacks(self, stack_sub_dir, settings): 46 | """Parse a directory and and deploy all the AWS Config Rules it contains 47 | 48 | Arguments: 49 | stack_sub_dir {string} -- Sub-directory that houses AWS Config Rules 50 | settings {dictionary} -- Dictionary of settings 51 | """ 52 | existing_stacks = self.get_current_stacks() 53 | path = f"auto_remediate_setup/data/{stack_sub_dir}" 54 | 55 | for file in os.listdir(path): 56 | if fnmatch.fnmatch(file, "*.json"): 57 | stack_name = file.replace(".json", "") 58 | template_body = None 59 | 60 | with open(os.path.join(path, file)) as stack: 61 | template_body = str(stack.read()) 62 | 63 | if stack_name not in existing_stacks: 64 | if ( 65 | settings.get("rules", {}) 66 | .get(stack_name, {}) 67 | .get("deploy", True) 68 | ): 69 | try: 70 | self.client_cloudformation.create_stack( 71 | StackName=stack_name, 72 | TemplateBody=template_body, 73 | OnFailure="DELETE", 74 | EnableTerminationProtection=True, 75 | ) 76 | 77 | self.logging.info( 78 | f"Creating AWS Config Rule '{stack_name}'." 79 | ) 80 | except: 81 | self.logging.error( 82 | f"Could not create AWS Config Rule '{stack_name}'." 83 | ) 84 | self.logging.error(sys.exc_info()[1]) 85 | continue 86 | else: 87 | self.logging.info( 88 | f"AWS Config Rule '{stack_name}' deployement was skipped due to user preferences." 89 | ) 90 | else: 91 | if ( 92 | not settings.get("rules", {}) 93 | .get(stack_name, {}) 94 | .get("deploy", True) 95 | ): 96 | self.client_cloudformation.update_termination_protection( 97 | EnableTerminationProtection=False, StackName=stack_name 98 | ) 99 | self.client_cloudformation.delete_stack(StackName=stack_name) 100 | self.logging.info( 101 | f"AWS Config Rule '{stack_name}' was deleted." 102 | ) 103 | else: 104 | self.logging.debug( 105 | f"AWS Config Rule '{stack_name}' already exists." 106 | ) 107 | 108 | def get_current_stacks(self): 109 | """Retrieve a list of all CloudFormation Stacks currently deployed your AWS accont and region 110 | 111 | Returns: 112 | list -- List of currently deployed AWS Config Rules 113 | """ 114 | try: 115 | resources = self.client_cloudformation.list_stacks().get("StackSummaries") 116 | except: 117 | self.logging.error(sys.exc_info()[1]) 118 | return None 119 | 120 | existing_stacks = [] 121 | for resource in resources: 122 | if resource.get("StackStatus") not in ("DELETE_COMPLETE"): 123 | existing_stacks.append(resource.get("StackName")) 124 | 125 | return existing_stacks 126 | 127 | def get_settings(self): 128 | """Return the DynamoDB aws-auto-remediate-settings table in a Python dict format 129 | 130 | Returns: 131 | dict -- aws-auto-remediate-settings table 132 | """ 133 | settings = {} 134 | try: 135 | for record in self.client_dynamodb.scan( 136 | TableName=os.environ["SETTINGSTABLE"] 137 | )["Items"]: 138 | record_json = dynamodb_json.loads(record, True) 139 | 140 | if "key" in record_json and "value" in record_json: 141 | settings[record_json.get("key")] = record_json.get("value") 142 | except: 143 | self.logging.error( 144 | f"Could not read DynamoDB table '{os.environ['SETTINGSTABLE']}'." 145 | ) 146 | self.logging.error(sys.exc_info()[1]) 147 | 148 | return settings 149 | 150 | def setup_dynamodb(self): 151 | """Inserts all the default settings into a DynamoDB table. 152 | """ 153 | try: 154 | settings_data = open( 155 | "auto_remediate_setup/data/auto-remediate-settings.json" 156 | ) 157 | settings_json = json.loads(settings_data.read()) 158 | 159 | update_settings = False 160 | 161 | # get current settings version 162 | current_version = self.client_dynamodb.get_item( 163 | TableName=os.environ["SETTINGSTABLE"], 164 | Key={"key": {"S": "version"}}, 165 | ConsistentRead=True, 166 | ) 167 | 168 | # get new settings version 169 | new_version = float(settings_json[0].get("value", {}).get("N", 0.0)) 170 | 171 | # check if settings exist and if they're older than current settings 172 | if "Item" in current_version: 173 | current_version = float( 174 | current_version.get("Item").get("value").get("N") 175 | ) 176 | if current_version < new_version: 177 | update_settings = True 178 | self.logging.info( 179 | f"Existing settings with version {str(current_version)} are being updated to version " 180 | f"{str(new_version)} in DynamoDB Table '{os.environ['SETTINGSTABLE']}'." 181 | ) 182 | else: 183 | self.logging.debug( 184 | f"Existing settings are at the lastest version {str(current_version)} in DynamoDB Table " 185 | f"'{os.environ['SETTINGSTABLE']}'." 186 | ) 187 | else: 188 | update_settings = True 189 | self.logging.info( 190 | f"Settings are being inserted into DynamoDB Table " 191 | f"'{os.environ['SETTINGSTABLE']}' for the first time." 192 | ) 193 | 194 | if update_settings: 195 | for setting in settings_json: 196 | try: 197 | self.client_dynamodb.put_item( 198 | TableName=os.environ["SETTINGSTABLE"], Item=setting 199 | ) 200 | except: 201 | self.logging.error(sys.exc_info()[1]) 202 | continue 203 | 204 | settings_data.close() 205 | except: 206 | self.logging.error(sys.exc_info()[1]) 207 | 208 | 209 | def lambda_handler(event, context): 210 | loggger = logging.getLogger() 211 | 212 | if loggger.handlers: 213 | for handler in loggger.handlers: 214 | loggger.removeHandler(handler) 215 | 216 | # change logging levels for boto and others 217 | logging.getLogger("boto3").setLevel(logging.ERROR) 218 | logging.getLogger("botocore").setLevel(logging.ERROR) 219 | logging.getLogger("urllib3").setLevel(logging.ERROR) 220 | 221 | # set logging format 222 | logging.basicConfig( 223 | format="[%(levelname)s] %(message)s (%(filename)s, %(funcName)s(), line %(lineno)d)", 224 | level=os.environ.get("LOGLEVEL", "WARNING").upper(), 225 | ) 226 | 227 | # instantiate class 228 | setup = Setup(logging) 229 | 230 | # run functions 231 | setup.setup_dynamodb() 232 | 233 | settings = setup.get_settings() 234 | 235 | setup.create_stacks("config_rules", settings) 236 | setup.create_stacks("custom_rules", settings) 237 | -------------------------------------------------------------------------------- /auto_remediate_setup/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlevit/aws-auto-remediate/949adb55809a122278a55c6503e19c9032b378ca/auto_remediate_setup/test/__init__.py -------------------------------------------------------------------------------- /auto_remediate_setup/test/data/auto-remediate-settings-deploy.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": { 4 | "S": "version" 5 | }, 6 | "value": { 7 | "N": "1.0" 8 | } 9 | }, 10 | { 11 | "key": { 12 | "S": "rules" 13 | }, 14 | "value": { 15 | "M": { 16 | "cloudtrail-enabled": { 17 | "M": { 18 | "remediate": { 19 | "BOOL": true 20 | }, 21 | "deploy": { 22 | "BOOL": true 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /auto_remediate_setup/test/data/auto-remediate-settings-remove.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": { 4 | "S": "version" 5 | }, 6 | "value": { 7 | "N": "2.0" 8 | } 9 | }, 10 | { 11 | "key": { 12 | "S": "rules" 13 | }, 14 | "value": { 15 | "M": { 16 | "cloudtrail-enabled": { 17 | "M": { 18 | "remediate": { 19 | "BOOL": true 20 | }, 21 | "deploy": { 22 | "BOOL": false 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /auto_remediate_setup/test/data/mock_rules/cloudtrail-enabled.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "AWSConfigRule": { 4 | "Type": "AWS::Config::ConfigRule", 5 | "Properties": { 6 | "ConfigRuleName": { 7 | "Ref": "ConfigRuleName" 8 | }, 9 | "Description": "Checks whether AWS CloudTrail is enabled in your AWS account.", 10 | "InputParameters": { 11 | "s3BucketName": { 12 | "Fn::If": [ 13 | "s3BucketName", 14 | { 15 | "Ref": "s3BucketName" 16 | }, 17 | { 18 | "Ref": "AWS::NoValue" 19 | } 20 | ] 21 | }, 22 | "snsTopicArn": { 23 | "Fn::If": [ 24 | "snsTopicArn", 25 | { 26 | "Ref": "snsTopicArn" 27 | }, 28 | { 29 | "Ref": "AWS::NoValue" 30 | } 31 | ] 32 | }, 33 | "cloudWatchLogsLogGroupArn": { 34 | "Fn::If": [ 35 | "cloudWatchLogsLogGroupArn", 36 | { 37 | "Ref": "cloudWatchLogsLogGroupArn" 38 | }, 39 | { 40 | "Ref": "AWS::NoValue" 41 | } 42 | ] 43 | } 44 | }, 45 | "Scope": {}, 46 | "Source": { 47 | "Owner": "AWS", 48 | "SourceIdentifier": "CLOUD_TRAIL_ENABLED" 49 | }, 50 | "MaximumExecutionFrequency": { 51 | "Ref": "MaximumExecutionFrequency" 52 | } 53 | } 54 | } 55 | }, 56 | "Parameters": { 57 | "ConfigRuleName": { 58 | "Type": "String", 59 | "Default": "cloudtrail-enabled", 60 | "Description": "The name that you assign to the AWS Config rule.", 61 | "MinLength": "1", 62 | "ConstraintDescription": "This parameter is required." 63 | }, 64 | "MaximumExecutionFrequency": { 65 | "Type": "String", 66 | "Default": "TwentyFour_Hours", 67 | "Description": "The frequency that you want AWS Config to run evaluations for the rule.", 68 | "MinLength": "1", 69 | "ConstraintDescription": "This parameter is required.", 70 | "AllowedValues": [ 71 | "One_Hour", 72 | "Three_Hours", 73 | "Six_Hours", 74 | "Twelve_Hours", 75 | "TwentyFour_Hours" 76 | ] 77 | }, 78 | "s3BucketName": { 79 | "Type": "String", 80 | "Default": "", 81 | "Description": "Name of S3 bucket for CloudTrail to deliver log files to." 82 | }, 83 | "snsTopicArn": { 84 | "Type": "String", 85 | "Default": "", 86 | "Description": "SNS topic ARN for CloudTrail to use for notifications." 87 | }, 88 | "cloudWatchLogsLogGroupArn": { 89 | "Type": "String", 90 | "Default": "", 91 | "Description": "CloudWatch log group ARN for CloudTrail to send data to." 92 | } 93 | }, 94 | "Metadata": { 95 | "AWS::CloudFormation::Interface": { 96 | "ParameterGroups": [ 97 | { 98 | "Label": { 99 | "default": "Required" 100 | }, 101 | "Parameters": [] 102 | }, 103 | { 104 | "Label": { 105 | "default": "Optional" 106 | }, 107 | "Parameters": [ 108 | "s3BucketName", 109 | "snsTopicArn", 110 | "cloudWatchLogsLogGroupArn" 111 | ] 112 | } 113 | ] 114 | } 115 | }, 116 | "Conditions": { 117 | "s3BucketName": { 118 | "Fn::Not": [ 119 | { 120 | "Fn::Equals": [ 121 | "", 122 | { 123 | "Ref": "s3BucketName" 124 | } 125 | ] 126 | } 127 | ] 128 | }, 129 | "snsTopicArn": { 130 | "Fn::Not": [ 131 | { 132 | "Fn::Equals": [ 133 | "", 134 | { 135 | "Ref": "snsTopicArn" 136 | } 137 | ] 138 | } 139 | ] 140 | }, 141 | "cloudWatchLogsLogGroupArn": { 142 | "Fn::Not": [ 143 | { 144 | "Fn::Equals": [ 145 | "", 146 | { 147 | "Ref": "cloudWatchLogsLogGroupArn" 148 | } 149 | ] 150 | } 151 | ] 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /auto_remediate_setup/test/test_setup.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import logging 4 | import os 5 | import shutil 6 | 7 | import moto 8 | import py 9 | import pytest 10 | 11 | from .. import lambda_handler 12 | 13 | 14 | class TestCreateStacks: 15 | @pytest.fixture 16 | def setup(self): 17 | with moto.mock_cloudformation(), moto.mock_dynamodb2(), moto.mock_sts(): 18 | setup = lambda_handler.Setup(logging) 19 | yield setup 20 | 21 | def test_create_stacks_deployment(self, setup): 22 | # make mock_rules directory 23 | os.mkdir("auto_remediate_setup/data/mock_rules") 24 | 25 | # move mock CloudFormation Stacks 26 | shutil.copyfile( 27 | "auto_remediate_setup/test/data/mock_rules/cloudtrail-enabled.json", 28 | "auto_remediate_setup/data/mock_rules/cloudtrail-enabled.json", 29 | ) 30 | 31 | # backup settings 32 | shutil.move( 33 | "auto_remediate_setup/data/auto-remediate-settings.json", 34 | "auto_remediate_setup/data/auto-remediate-settings-backup.json", 35 | ) 36 | 37 | # move mock settings 38 | shutil.copyfile( 39 | "auto_remediate_setup/test/data/auto-remediate-settings-deploy.json", 40 | "auto_remediate_setup/data/auto-remediate-settings.json", 41 | ) 42 | 43 | # create table 44 | setup.client_dynamodb.create_table( 45 | TableName="settings-table", 46 | KeySchema=[{"AttributeName": "key", "KeyType": "HASH"}], 47 | AttributeDefinitions=[{"AttributeName": "key", "AttributeType": "S"}], 48 | ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, 49 | ) 50 | 51 | # insert settings to DynamoDB 52 | os.environ["SETTINGSTABLE"] = "settings-table" 53 | setup.setup_dynamodb() 54 | 55 | # test create_stacks function 56 | setup.create_stacks("mock_rules", setup.get_settings()) 57 | 58 | # validate Stack created 59 | response = setup.client_cloudformation.list_stacks() 60 | assert response["StackSummaries"][0]["StackName"] == "cloudtrail-enabled" 61 | 62 | # restore settings 63 | shutil.move( 64 | "auto_remediate_setup/data/auto-remediate-settings-backup.json", 65 | "auto_remediate_setup/data/auto-remediate-settings.json", 66 | ) 67 | 68 | # delete mock_rules directory 69 | shutil.rmtree("auto_remediate_setup/data/mock_rules") 70 | 71 | # def test_create_stacks_removal(self, setup): 72 | # # make mock_rules directory 73 | # os.mkdir("auto_remediate_setup/data/mock_rules") 74 | 75 | # # move mock CloudFormation Stacks 76 | # shutil.copyfile( 77 | # "auto_remediate_setup/test/data/mock_rules/cloudtrail-enabled.json", 78 | # "auto_remediate_setup/data/mock_rules/cloudtrail-enabled.json", 79 | # ) 80 | 81 | # # backup settings 82 | # shutil.move( 83 | # "auto_remediate_setup/data/auto-remediate-settings.json", 84 | # "auto_remediate_setup/data/auto-remediate-settings-backup.json", 85 | # ) 86 | 87 | # # move mock deploy settings 88 | # shutil.copyfile( 89 | # "auto_remediate_setup/test/data/auto-remediate-settings-deploy.json", 90 | # "auto_remediate_setup/data/auto-remediate-settings.json", 91 | # ) 92 | 93 | # # create table 94 | # setup.client_dynamodb.create_table( 95 | # TableName="settings-table", 96 | # KeySchema=[{"AttributeName": "key", "KeyType": "HASH"}], 97 | # AttributeDefinitions=[{"AttributeName": "key", "AttributeType": "S"}], 98 | # ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, 99 | # ) 100 | 101 | # # insert settings to DynamoDB 102 | # os.environ["SETTINGSTABLE"] = "settings-table" 103 | # setup.setup_dynamodb() 104 | 105 | # # test create_stacks function 106 | # setup.create_stacks("mock_rules", setup.get_settings()) 107 | 108 | # # validate Stack created 109 | # response = setup.client_cloudformation.list_stacks() 110 | # assert response["StackSummaries"][0]["StackName"] == "cloudtrail-enabled" 111 | 112 | # # move mock remove settings 113 | # shutil.copyfile( 114 | # "auto_remediate_setup/test/data/auto-remediate-settings-remove.json", 115 | # "auto_remediate_setup/data/auto-remediate-settings.json", 116 | # ) 117 | 118 | # # insert settings to DynamoDB 119 | # setup.setup_dynamodb() 120 | 121 | # # test create_stacks function 122 | # setup.create_stacks("mock_rules", setup.get_settings()) 123 | 124 | # # validate Stack created 125 | # response = setup.client_cloudformation.list_stacks() 126 | 127 | # # restore settings 128 | # shutil.move( 129 | # "auto_remediate_setup/data/auto-remediate-settings-backup.json", 130 | # "auto_remediate_setup/data/auto-remediate-settings.json", 131 | # ) 132 | 133 | # # delete mock_rules directory 134 | # shutil.rmtree("auto_remediate_setup/data/mock_rules") 135 | 136 | 137 | class TestGetSettings: 138 | @pytest.fixture 139 | def setup(self): 140 | with moto.mock_cloudformation(), moto.mock_dynamodb2(), moto.mock_sts(): 141 | setup = lambda_handler.Setup(logging) 142 | yield setup 143 | 144 | def test_get_settings(self, setup): 145 | """Tests retrieval of settings from DynamoDB 146 | 147 | Arguments: 148 | setup {class} -- Instance of Setup class 149 | """ 150 | os.environ["SETTINGSTABLE"] = "settings-table" 151 | 152 | # create table 153 | setup.client_dynamodb.create_table( 154 | TableName="settings-table", 155 | KeySchema=[{"AttributeName": "key", "KeyType": "HASH"}], 156 | AttributeDefinitions=[{"AttributeName": "key", "AttributeType": "S"}], 157 | ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, 158 | ) 159 | 160 | # populate table 161 | setup.client_dynamodb.put_item( 162 | TableName="settings-table", 163 | Item={"key": {"S": "version"}, "value": {"N": "1.0"}}, 164 | ) 165 | 166 | # test get_settings function 167 | settings = setup.get_settings() 168 | 169 | # validate test 170 | assert settings["version"] == 1.0 171 | 172 | def test_invalid_table_schema(self, setup): 173 | """Tests retrieval of settings from DynamoDB with the wrong schema 174 | 175 | Arguments: 176 | setup {class} -- Instance of Setup class 177 | """ 178 | os.environ["SETTINGSTABLE"] = "settings-table" 179 | 180 | setup.client_dynamodb.create_table( 181 | TableName="settings-table", 182 | KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], 183 | AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], 184 | ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, 185 | ) 186 | 187 | setup.client_dynamodb.put_item( 188 | TableName="settings-table", Item={"id": {"S": "123"}} 189 | ) 190 | 191 | # test get_settings function 192 | assert setup.get_settings() == {} 193 | 194 | def test_no_table_name(self, setup): 195 | """Tests retrieval of settings from DynamoDB with no table name 196 | 197 | Arguments: 198 | setup {class} -- Instance of Setup class 199 | """ 200 | assert setup.get_settings() == {} 201 | 202 | 203 | class TestGetCurrentStacks: 204 | @pytest.fixture 205 | def setup(self): 206 | with moto.mock_cloudformation(), moto.mock_dynamodb2(), moto.mock_sts(): 207 | setup = lambda_handler.Setup(logging) 208 | yield setup 209 | 210 | def test_get_current_stacks(self, setup): 211 | """Tests retrieval of CloudFormation Stacks 212 | 213 | Arguments: 214 | setup {[type]} -- [description] 215 | """ 216 | setup.client_cloudformation.create_stack( 217 | StackName="sample_sqs", 218 | TemplateBody='{"Resources":{"SQSQueue":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"test_queue"}}}}', 219 | ) 220 | 221 | # test get_current_stacks function 222 | response = setup.get_current_stacks() 223 | 224 | # assert stack created 225 | assert response[0] == "sample_sqs" 226 | 227 | def test_no_stacks(self, setup): 228 | """Tests retrieval of CloudFormation Stacks with no Stacks 229 | 230 | Arguments: 231 | setup {[type]} -- [description] 232 | """ 233 | assert setup.get_current_stacks() == [] 234 | 235 | 236 | class TestSetupDynamoDb: 237 | @pytest.fixture 238 | def setup(self): 239 | with moto.mock_dynamodb2(), moto.mock_sts(): 240 | setup = lambda_handler.Setup(logging) 241 | yield setup 242 | 243 | def test_setup_dynamodb(self, setup): 244 | os.environ["SETTINGSTABLE"] = "settings-table" 245 | 246 | # create table 247 | setup.client_dynamodb.create_table( 248 | TableName="settings-table", 249 | KeySchema=[{"AttributeName": "key", "KeyType": "HASH"}], 250 | AttributeDefinitions=[{"AttributeName": "key", "AttributeType": "S"}], 251 | ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, 252 | ) 253 | 254 | setup.setup_dynamodb() 255 | 256 | assert len(setup.client_dynamodb.scan(TableName="settings-table")["Items"]) > 0 257 | 258 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auto-remediate", 3 | "description": "", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "serverless": "^1.43.0", 8 | "serverless-s3-remover": "^0.6.0" 9 | }, 10 | "devDependencies": { 11 | "serverless-iam-roles-per-function": "^2.0.1", 12 | "serverless-python-requirements": "^4.3.0" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com:servian/aws-auto-remediate.git" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3>=1.9.149 2 | botocore>=1.12.149 3 | dynamodb-json>=1.3 -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: auto-remediate 2 | 3 | custom: 4 | log_level: INFO # DEBUG for dev | INFO for prod 5 | region: ${opt:region, "ap-southeast-2"} # AWS deployment region 6 | pythonRequirements: 7 | noDeploy: 8 | [ 9 | "docutils", 10 | "jmespath", 11 | "python-dateutil", 12 | "s3transfer", 13 | "six", 14 | "pip", 15 | "setuptools", 16 | ] 17 | slim: true 18 | useStaticCache: true 19 | useDownloadCache: true 20 | 21 | provider: 22 | name: aws 23 | runtime: python3.7 24 | stage: ${opt:stage, "prod"} # Override via CLI "--stage dev" 25 | region: ${self:custom.region} 26 | profile: ${opt:profile, ""} # Override via CLI "--aws-profile saml" 27 | 28 | package: 29 | individually: true 30 | exclude: 31 | - node_modules/** 32 | 33 | functions: 34 | AutoRemediate: 35 | handler: auto_remediate.lambda_handler.lambda_handler 36 | name: ${self:service}-${self:provider.stage} 37 | description: Auto Remediate instantly remediates common security issues 38 | memorySize: 128 39 | timeout: 60 40 | package: 41 | exclude: 42 | - auto_remediate/test/** 43 | - auto_remediate_dlq/** 44 | - auto_remediate_setup/** 45 | environment: 46 | DEADLETTERQUEUE: 47 | Ref: SQSDeadLetterQueue 48 | LOGLEVEL: ${self:custom.log_level} 49 | LOGTOPIC: 50 | Ref: SNSLogTopic 51 | MISSINGREMEDIATIONTOPIC: 52 | Ref: SNSMissingRemediationTopic 53 | PYTHONPATH: "/var/task/auto_remediate:/var/task/dynamodb_json:/var/runtime" 54 | RETRYCOUNT: 3 55 | SETTINGSTABLE: 56 | Ref: DynamoDBSettingsTable 57 | iamRoleStatements: 58 | - Effect: Allow 59 | Action: 60 | - cloudtrail:UpdateTrail 61 | - dynamodb:GetItem 62 | - dynamodb:Scan 63 | - ec2:CreateFlowLogs 64 | - ec2:DescribeSecurityGroups 65 | - ec2:RevokeSecurityGroupEgress 66 | - ec2:RevokeSecurityGroupIngress 67 | - ec2:RevokeSecurityGroupIngress 68 | - iam:CreatePolicyVersion 69 | - iam:CreateRole 70 | - iam:DeleteAccessKey 71 | - iam:DeleteLoginProfile 72 | - iam:DeleteRole 73 | - iam:DeleteRolePolicy 74 | - iam:DetachUserPolicy 75 | - iam:GetAccessKeyLastUsed 76 | - iam:GetLoginProfile 77 | - iam:GetPolicy 78 | - iam:GetPolicyVersion 79 | - iam:GetRole 80 | - iam:ListAccessKeys 81 | - iam:ListAttachedUserPolicies 82 | - iam:ListPolicies 83 | - iam:ListUsers 84 | - iam:PassRole 85 | - iam:PutRolePolicy 86 | - iam:UpdateAccountPasswordPolicy 87 | - kms:CreateAlias 88 | - kms:CreateKey 89 | - kms:DeleteAlias 90 | - kms:EnableKeyRotation 91 | - kms:ScheduleKeyDeletion 92 | - logs:CreateLogGroup 93 | - logs:DeleteLogGroup 94 | - logs:DescribeLogGroups 95 | - rds:DescribeDBInstances 96 | - rds:ModifyDBInstance 97 | - s3:CreateBucket 98 | - s3:GetBucketPolicy 99 | - s3:GetBucketPublicAccessBlock 100 | - s3:PutBucketAcl 101 | - s3:PutBucketLogging 102 | - s3:PutBucketPolicy 103 | - s3:PutBucketPublicAccessBlock 104 | - s3:PutEncryptionConfiguration 105 | - sns:Publish 106 | - sqs:GetQueueUrl 107 | - sqs:SendMessage 108 | - sts:GetCallerIdentity 109 | Resource: "*" 110 | events: 111 | - sqs: 112 | arn: 113 | Fn::GetAtt: 114 | - SQSConfigComplianceQueue 115 | - Arn 116 | batchSize: 1 117 | AutoRemediateDLQ: 118 | handler: auto_remediate_dlq.lambda_handler.lambda_handler 119 | name: ${self:service}-dlq-${self:provider.stage} 120 | description: Auto Remediate DLQ retries failed remediation attempts 121 | memorySize: 128 122 | timeout: 60 123 | package: 124 | exclude: 125 | - auto_remediate_dlq/test/** 126 | - auto_remediate/** 127 | - auto_remediate_setup/** 128 | environment: 129 | COMPLIANCEQUEUE: 130 | Ref: SQSConfigComplianceQueue 131 | DEADLETTERQUEUE: 132 | Ref: SQSDeadLetterQueue 133 | LOGLEVEL: ${self:custom.log_level} 134 | PYTHONPATH: "/var/task/auto_remediate_dlq:/var/runtime" 135 | iamRoleStatements: 136 | - Effect: Allow 137 | Action: 138 | - sqs:DeleteMessage 139 | - sqs:GetQueueUrl 140 | - sqs:ReceiveMessage 141 | - sqs:SendMessage 142 | Resource: "*" 143 | events: 144 | - schedule: 145 | rate: rate(30 minutes) 146 | enabled: true 147 | AutoRemediateSetup: 148 | handler: auto_remediate_setup.lambda_handler.lambda_handler 149 | name: ${self:service}-setup-${self:provider.stage} 150 | description: Auto Remediate Setup creates CloudFormation Stacks for AWS Config 151 | memorySize: 128 152 | timeout: 60 153 | package: 154 | exclude: 155 | - auto_remediate_setup/test/** 156 | - auto_remediate/** 157 | - auto_remediate_dlq/** 158 | environment: 159 | LOGLEVEL: ${self:custom.log_level} 160 | PYTHONPATH: "/var/task/auto_remediate_setup:/var/task/dynamodb_json:/var/runtime" 161 | SETTINGSTABLE: 162 | Ref: DynamoDBSettingsTable 163 | iamRoleStatements: 164 | - Effect: Allow 165 | Action: 166 | - dynamodb:GetItem 167 | - dynamodb:Scan 168 | - cloudformation:CreateStack 169 | - cloudformation:DeleteStack 170 | - cloudformation:ListStacks 171 | - cloudformation:UpdateTerminationProtection 172 | - config:DeleteConfigRule 173 | - config:PutConfigRule 174 | - dynamodb:BatchWriteItem 175 | - dynamodb:DeleteTable 176 | - dynamodb:DescribeTable 177 | - dynamodb:GetItem 178 | - dynamodb:ListTables 179 | - dynamodb:PutItem 180 | - dynamodb:Scan 181 | Resource: "*" 182 | 183 | resources: 184 | Resources: 185 | CloudWatchComplianceEvent: 186 | Type: AWS::Events::Rule 187 | Properties: 188 | Name: ${self:service}-config-compliance-${self:provider.stage} 189 | EventPattern: 190 | source: 191 | - aws.config 192 | detail-type: 193 | - Config Rules Compliance Change 194 | State: ENABLED 195 | Targets: 196 | - Arn: 197 | Fn::GetAtt: 198 | - SQSConfigComplianceQueue 199 | - Arn 200 | Id: 201 | Fn::GetAtt: 202 | - SQSConfigComplianceQueue 203 | - QueueName 204 | DependsOn: SQSConfigComplianceQueue 205 | DynamoDBSettingsTable: 206 | Type: AWS::DynamoDB::Table 207 | Properties: 208 | TableName: ${self:service}-settings-${self:provider.stage} 209 | AttributeDefinitions: 210 | - AttributeName: key 211 | AttributeType: S 212 | KeySchema: 213 | - AttributeName: key 214 | KeyType: HASH 215 | ProvisionedThroughput: 216 | ReadCapacityUnits: 1 217 | WriteCapacityUnits: 1 218 | SNSLogTopic: 219 | Type: AWS::SNS::Topic 220 | Properties: 221 | TopicName: ${self:service}-log-${self:provider.stage} 222 | SNSMissingRemediationTopic: 223 | Type: AWS::SNS::Topic 224 | Properties: 225 | TopicName: ${self:service}-missing-remediation-${self:provider.stage} 226 | SQSConfigComplianceQueue: 227 | Type: AWS::SQS::Queue 228 | Properties: 229 | QueueName: ${self:service}-config-compliance-${self:provider.stage} 230 | VisibilityTimeout: 60 231 | SQSConfigComplianceQueuePolicy: 232 | Type: AWS::SQS::QueuePolicy 233 | Properties: 234 | PolicyDocument: 235 | Version: "2012-10-17" 236 | Id: SQSConfigComplianceQueuePolicy 237 | Statement: 238 | - Effect: Allow 239 | Principal: 240 | Service: 241 | - events.amazonaws.com 242 | - sqs.amazonaws.com 243 | Action: sqs:SendMessage 244 | Resource: 245 | Fn::GetAtt: 246 | - SQSConfigComplianceQueue 247 | - Arn 248 | Queues: 249 | - Ref: SQSConfigComplianceQueue 250 | DependsOn: 251 | - CloudWatchComplianceEvent 252 | - SQSConfigComplianceQueue 253 | SQSDeadLetterQueue: 254 | Type: AWS::SQS::Queue 255 | Properties: 256 | QueueName: ${self:service}-dlq-${self:provider.stage} 257 | 258 | plugins: 259 | - serverless-iam-roles-per-function 260 | - serverless-python-requirements 261 | --------------------------------------------------------------------------------