├── src ├── requirements.txt └── quarantine │ ├── __init__.py │ ├── plugins │ ├── __init__.py │ ├── 03_termination_protection.py │ ├── 04_shutdown_behavior.py │ ├── 09_detach_from_asg.py │ ├── 10_deregister_instance.py │ ├── abstract_plugin.py │ ├── 01_console_screenshot.py │ ├── 02_capture_metadata.py │ ├── 06_tag_instance.py │ ├── 07_snapshot_volumes.py │ ├── 05_preserve_volumes.py │ ├── 08_command_output.py │ └── 11_isolate_instance.py │ ├── resources │ ├── __init__.py │ ├── sns.py │ ├── s3.py │ ├── autoscaling.py │ ├── elbv2.py │ ├── elb.py │ ├── ssm.py │ └── ec2.py │ ├── constants.py │ ├── schemas.py │ ├── utils.py │ └── lambda_handler.py ├── CODEOWNERS ├── .cfnlintrc ├── .gitignore ├── requirements-dev.txt ├── doc ├── architecture.png └── architecture.drawio ├── pyproject.toml ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── .editorconfig ├── Makefile ├── .github └── dependabot.yml ├── LICENSE ├── CONTRIBUTING.md ├── events ├── guardduty_ec2_event.json ├── guardduty_iam_event.json └── guardduty_s3_event.json ├── README.md └── template.yml /src/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jplock 2 | -------------------------------------------------------------------------------- /.cfnlintrc: -------------------------------------------------------------------------------- 1 | templates: 2 | - template.yml 3 | include_checks: 4 | - I 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .venv 3 | .vscode 4 | .aws-sam 5 | samconfig.toml 6 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | aws-lambda-powertools[validation]==2.43.1 2 | black==24.8.0 3 | pre-commit==3.8.0 4 | -------------------------------------------------------------------------------- /doc/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-guardduty-automated-response-sample/HEAD/doc/architecture.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | target-version = ['py312'] 4 | include = '\.pyi?$' 5 | extend-exclude = ''' 6 | ( 7 | /( 8 | \.venv 9 | | \.aws-sam 10 | )/ 11 | ) 12 | ''' 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/aws-cloudformation/cfn-lint 3 | rev: v0.72.2 4 | hooks: 5 | - id: cfn-lint-rc 6 | - repo: https://github.com/psf/black 7 | rev: 22.10.0 8 | hooks: 9 | - id: black 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 4 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 5 | opensource-codeofconduct@amazon.com with any additional questions or comments. 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | max_line_length = 120 11 | 12 | [*.json] 13 | indent_size = 2 14 | quote_type = double 15 | 16 | [*.yml] 17 | quote_type = double 18 | 19 | [*.py] 20 | quote_type = double 21 | indent_size = 4 22 | 23 | [Makefile] 24 | indent_style = tab -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: setup build deploy format clean 2 | 3 | setup: 4 | python3 -m venv .venv 5 | .venv/bin/python3 -m pip install -U pip wheel 6 | .venv/bin/python3 -m pip install -r requirements-dev.txt 7 | .venv/bin/python3 -m pip install -r src/requirements.txt 8 | .venv/bin/pre-commit install 9 | 10 | build: 11 | sam build 12 | 13 | deploy: 14 | sam deploy 15 | 16 | clean: 17 | sam delete 18 | 19 | format: 20 | .venv/bin/black . 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | open-pull-requests-limit: 1 8 | commit-message: 9 | prefix: chore 10 | include: scope 11 | groups: 12 | pip-dependencies: 13 | applies-to: version-updates 14 | update-types: [minor, patch] 15 | patterns: 16 | - "*" 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "weekly" 21 | open-pull-requests-limit: 1 22 | commit-message: 23 | prefix: chore 24 | include: scope 25 | groups: 26 | github-action-dependencies: 27 | applies-to: version-updates 28 | update-types: [minor, patch] 29 | patterns: 30 | - "*" 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /src/quarantine/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | -------------------------------------------------------------------------------- /src/quarantine/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | -------------------------------------------------------------------------------- /src/quarantine/resources/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from .autoscaling import AutoScaling 23 | from .ec2 import EC2 24 | from .elb import ELB 25 | from .elbv2 import ELBv2 26 | from .s3 import S3 27 | from .sns import SNS 28 | from .ssm import SSM 29 | 30 | __all__ = ["AutoScaling", "EC2", "ELB", "ELBv2", "S3", "SNS", "SSM"] 31 | -------------------------------------------------------------------------------- /src/quarantine/constants.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from botocore.config import Config 23 | 24 | __all__ = [ 25 | "BOTO3_CONFIG", 26 | "SSM_COMMANDS", 27 | "SSM_DRAIN_TIME_SECS" 28 | ] 29 | 30 | BOTO3_CONFIG = Config( 31 | retries={ 32 | "max_attempts": 10, 33 | "mode": "standard", 34 | } 35 | ) 36 | 37 | # Commands to execute on EC2 instances for information gathering 38 | SSM_COMMANDS = ["uname -a", "whoami", "netstat -ap", "lsof"] 39 | 40 | # Amount of time to wait after executing an SSM command for the output to be uploaded to S3 41 | SSM_DRAIN_TIME_SECS = 10 42 | -------------------------------------------------------------------------------- /src/quarantine/schemas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | INPUT = { 23 | "$schema": "http://json-schema.org/draft-07/schema", 24 | "type": "object", 25 | "properties": { 26 | "resource": { 27 | "type": "object", 28 | "properties": { 29 | "resourceType": {"type": "string"}, 30 | "instanceDetails": { 31 | "type": "object", 32 | "properties": {"instanceId": {"type": "string"}}, 33 | "required": ["instanceId"], 34 | }, 35 | }, 36 | "required": ["resourceType", "instanceDetails"], 37 | } 38 | }, 39 | "required": ["resource"], 40 | } 41 | -------------------------------------------------------------------------------- /src/quarantine/plugins/03_termination_protection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import Optional 23 | 24 | from aws_lambda_powertools import Logger 25 | 26 | from quarantine.plugins.abstract_plugin import AbstractPlugin 27 | 28 | logger = Logger(child=True) 29 | 30 | 31 | class TerminationProtection(AbstractPlugin): 32 | """ 33 | Enable termination protection on an EC2 instance 34 | """ 35 | 36 | def execute(self) -> Optional[str]: 37 | try: 38 | self.ec2.enable_termination_protection(self.instance_id) 39 | message = f"Enabled termination protection on {self.instance_id}" 40 | except Exception: 41 | message = f"Failed to enable termination protection on {self.instance_id}" 42 | logger.exception(message) 43 | 44 | return message 45 | -------------------------------------------------------------------------------- /src/quarantine/plugins/04_shutdown_behavior.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import Optional 23 | 24 | from aws_lambda_powertools import Logger 25 | 26 | from quarantine.plugins.abstract_plugin import AbstractPlugin 27 | 28 | logger = Logger(child=True) 29 | 30 | 31 | class ShutdownBehavior(AbstractPlugin): 32 | """ 33 | Set shutdown behavior to 'stop' (instead of 'terminate') 34 | """ 35 | 36 | def execute(self) -> Optional[str]: 37 | try: 38 | self.ec2.shutdown_behavior_stop(self.instance_id) 39 | message = f"Shutdown behavior set to 'stop' on {self.instance_id}" 40 | except Exception: 41 | message = f"Failed to modify shutdown behavior on {self.instance_id} to 'stop'" 42 | logger.exception(message) 43 | 44 | return message 45 | -------------------------------------------------------------------------------- /src/quarantine/plugins/09_detach_from_asg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import Optional 23 | 24 | from aws_lambda_powertools import Logger 25 | 26 | from quarantine.plugins.abstract_plugin import AbstractPlugin 27 | 28 | logger = Logger(child=True) 29 | 30 | 31 | class DetachFromASG(AbstractPlugin): 32 | """ 33 | Detach the instance from any autoscaling groups 34 | """ 35 | 36 | def execute(self) -> Optional[str]: 37 | try: 38 | self.autoscaling.detach_instance(self.instance_id) 39 | message = f"Detached instance {self.instance_id} from any autoscaling groups" 40 | except Exception: 41 | message = f"Unable to detach instance {self.instance_id} from autoscaling groups" 42 | logger.exception(message) 43 | 44 | return message 45 | -------------------------------------------------------------------------------- /src/quarantine/plugins/10_deregister_instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import Optional 23 | 24 | from aws_lambda_powertools import Logger 25 | 26 | from quarantine.plugins.abstract_plugin import AbstractPlugin 27 | 28 | logger = Logger(child=True) 29 | 30 | 31 | class DeregisterInstance(AbstractPlugin): 32 | """ 33 | Deregister the instance from any classic ELBs and ALB/NLB target groups 34 | """ 35 | 36 | def execute(self) -> Optional[str]: 37 | try: 38 | self.elb.deregister_instance(self.instance_id) # classic ELB 39 | self.elbv2.deregister_target(self.instance_id) # ALB/NLB 40 | message = f"Deregistered instance {self.instance_id} from all load balancers and target groups" 41 | except Exception: 42 | message = f"Unable to deregister instance {self.instance_id} from load balancers or target groups" 43 | logger.exception(message) 44 | 45 | return message 46 | -------------------------------------------------------------------------------- /src/quarantine/plugins/abstract_plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from abc import ABC, abstractmethod 23 | from typing import Optional 24 | 25 | import boto3 26 | 27 | from quarantine.resources import AutoScaling, EC2, ELB, ELBv2, S3, SSM 28 | 29 | 30 | class AbstractPlugin(ABC): 31 | def __init__(self, session: boto3.Session, instance_id: str, finding_id: str) -> None: 32 | self.s3 = S3(session) 33 | self.ec2 = EC2(session) 34 | self.autoscaling = AutoScaling(session) 35 | self.ssm = SSM(session) 36 | self.elb = ELB(session) 37 | self.elbv2 = ELBv2(session) 38 | 39 | self.instance_id = instance_id 40 | self.finding_id = finding_id 41 | 42 | @abstractmethod 43 | def execute(self) -> Optional[str]: 44 | """ 45 | Plugins must implement this method. 46 | 47 | If a plugin returns a string, it will be published to the SNS topic. 48 | """ 49 | raise NotImplementedError 50 | -------------------------------------------------------------------------------- /src/quarantine/plugins/01_console_screenshot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | import base64 23 | from typing import Optional 24 | 25 | from aws_lambda_powertools import Logger 26 | 27 | from quarantine.plugins.abstract_plugin import AbstractPlugin 28 | 29 | logger = Logger(child=True) 30 | 31 | 32 | class ConsoleScreenshot(AbstractPlugin): 33 | """ 34 | Get a screenshot of the EC2 console and upload to S3 35 | """ 36 | 37 | def execute(self) -> Optional[str]: 38 | try: 39 | image_data = self.ec2.get_console_screenshot(self.instance_id) 40 | key = f"console_screenshot_{self.instance_id}.jpg" 41 | screenshot = base64.b64decode(image_data) 42 | self.s3.put_object(self.instance_id, key, screenshot) 43 | message = f"Successfully captured console screen shot: {key}" 44 | except Exception: 45 | message = f"Unable to get screenshot from instance {self.instance_id}" 46 | logger.exception(message) 47 | 48 | return message 49 | -------------------------------------------------------------------------------- /src/quarantine/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | import datetime 23 | import json 24 | from typing import Any 25 | 26 | from aws_lambda_powertools.shared.json_encoder import Encoder 27 | 28 | __all__ = ["json_dumps", "get_prefix", "now"] 29 | 30 | 31 | class DateTimeEncoder(Encoder): 32 | def default(self, obj): 33 | if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)): 34 | return obj.isoformat() 35 | elif isinstance(obj, datetime.timedelta): 36 | return (datetime.datetime.min + obj).time().isoformat() 37 | return super().default(obj) 38 | 39 | 40 | def json_dumps(obj: Any) -> str: 41 | return json.dumps(obj, indent=None, sort_keys=True, separators=(",", ":"), cls=DateTimeEncoder) 42 | 43 | 44 | def get_prefix(instance_id: str) -> str: 45 | # now = datetime.datetime.now(tz=datetime.timezone.utc).strftime( 46 | # "year=%Y/month=%m/day=%d/hour=%H" 47 | # ) 48 | # return f"{now}/{instance_id}" 49 | return instance_id 50 | 51 | 52 | def now() -> str: 53 | """ 54 | Return a ISO8601 timestamp 55 | """ 56 | return ( 57 | datetime.datetime.now(tz=datetime.timezone.utc) 58 | .replace(microsecond=0) 59 | .isoformat() 60 | .replace("+00:00", "Z") 61 | ) 62 | -------------------------------------------------------------------------------- /src/quarantine/resources/sns.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | import os 23 | 24 | from aws_lambda_powertools import Logger 25 | import boto3 26 | import botocore 27 | 28 | from quarantine.utils import json_dumps 29 | from quarantine.constants import BOTO3_CONFIG 30 | 31 | TOPIC_ARN = os.environ["NOTIFICATION_TOPIC_ARN"] 32 | 33 | logger = Logger(child=True) 34 | 35 | __all__ = ["SNS"] 36 | 37 | 38 | class SNS: 39 | def __init__(self, session: boto3.Session) -> None: 40 | self.client = session.client("sns", config=BOTO3_CONFIG) 41 | 42 | def publish(self, instance_id: str, message: str) -> None: 43 | params = { 44 | "TopicArn": TOPIC_ARN, 45 | "Message": json_dumps({"default": message, "instance_id": instance_id}), 46 | "MessageStructure": "json", 47 | "MessageAttributes": {"InstanceId": {"DataType": "String", "StringValue": instance_id}}, 48 | } 49 | logger.debug(params) 50 | 51 | logger.debug(f"Publishing message to topic {TOPIC_ARN}") 52 | try: 53 | self.client.publish(**params) 54 | logger.debug(f"Published message to topic {TOPIC_ARN}") 55 | except botocore.exceptions.ClientError: 56 | logger.exception(f"Failed to publish message to topic {TOPIC_ARN}") 57 | -------------------------------------------------------------------------------- /src/quarantine/plugins/02_capture_metadata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import Optional 23 | import tempfile 24 | 25 | from aws_lambda_powertools import Logger 26 | 27 | from quarantine.plugins.abstract_plugin import AbstractPlugin 28 | from quarantine.utils import json_dumps 29 | 30 | logger = Logger(child=True) 31 | 32 | 33 | class CaptureMetadata(AbstractPlugin): 34 | """ 35 | Collect EC2 instance metadata upload to S3 36 | """ 37 | 38 | def execute(self) -> Optional[str]: 39 | key = f"metadata_file_{self.instance_id}.json" 40 | 41 | try: 42 | # Capture instance metadata 43 | instance_data = self.ec2.describe_instances(self.instance_id) 44 | with tempfile.TemporaryFile() as fp: 45 | fp.write(json_dumps(instance_data).encode("utf-8")) 46 | fp.seek(0) 47 | 48 | self.s3.put_object(self.instance_id, key, fp) 49 | 50 | logger.info(f"Captured instance metadata for instance {self.instance_id}") 51 | message = f"Successfully captured instance metadata: {key}" 52 | except Exception: 53 | message = f"Unable to capture instance metadata on instance {self.instance_id}" 54 | logger.exception(message) 55 | 56 | return message 57 | -------------------------------------------------------------------------------- /src/quarantine/plugins/06_tag_instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import Optional 23 | 24 | from aws_lambda_powertools import Logger 25 | 26 | from quarantine.plugins.abstract_plugin import AbstractPlugin 27 | from quarantine.utils import now 28 | 29 | logger = Logger(child=True) 30 | 31 | 32 | class TagInstance(AbstractPlugin): 33 | """ 34 | Tag the instance 35 | """ 36 | 37 | def execute(self) -> Optional[str]: 38 | tags = [ 39 | { 40 | "Key": "SOC-Status", 41 | "Value": "quarantined", 42 | }, 43 | { 44 | "Key": "SOC-ContainedAt", 45 | "Value": now(), 46 | }, 47 | { 48 | "Key": "SOC-FindingId", 49 | "Value": self.finding_id, 50 | }, 51 | { 52 | "Key": "SOC-FindingSource", 53 | "Value": "GuardDuty", 54 | }, 55 | ] 56 | 57 | try: 58 | self.ec2.create_tags(self.instance_id, tags) 59 | message = f"Added incident tags to instance {self.instance_id}" 60 | except Exception: 61 | message = f"Unable to add tags to instance {self.instance_id}" 62 | logger.exception(message) 63 | 64 | return message 65 | -------------------------------------------------------------------------------- /src/quarantine/resources/s3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | import os 23 | from typing import Union 24 | 25 | from aws_lambda_powertools import Logger 26 | import boto3 27 | import botocore 28 | 29 | from quarantine.utils import get_prefix 30 | from quarantine.constants import BOTO3_CONFIG 31 | 32 | BUCKET_NAME = os.environ["ARTIFACT_BUCKET"] 33 | AWS_ACCOUNT_ID = os.environ["AWS_ACCOUNT_ID"] 34 | 35 | logger = Logger(child=True) 36 | 37 | __all__ = ["S3"] 38 | 39 | 40 | class S3: 41 | def __init__(self, session: boto3.Session) -> None: 42 | self.client = session.client("s3", config=BOTO3_CONFIG) 43 | 44 | def put_object(self, instance_id: str, key: str, body: Union[bytes, str]) -> None: 45 | prefix = get_prefix(instance_id) 46 | 47 | params = { 48 | "ACL": "bucket-owner-full-control", 49 | "Bucket": BUCKET_NAME, 50 | "Key": f"{prefix}/{key}", 51 | "Metadata": {"instance_id": instance_id}, 52 | "ExpectedBucketOwner": AWS_ACCOUNT_ID, 53 | } 54 | logger.debug(params) 55 | params["Body"] = body # the body is separate so its not logged 56 | 57 | logger.debug(f"Uploading s3://{BUCKET_NAME}/{prefix}/{key}") 58 | try: 59 | self.client.put_object(**params) 60 | logger.debug(f"Uploaded s3://{BUCKET_NAME}/{prefix}/{key}") 61 | except botocore.exceptions.ClientError: 62 | logger.exception(f"Failed to upload s3://{BUCKET_NAME}/{prefix}/{key}") 63 | -------------------------------------------------------------------------------- /src/quarantine/plugins/07_snapshot_volumes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import Optional 23 | 24 | from aws_lambda_powertools import Logger 25 | 26 | from quarantine.plugins.abstract_plugin import AbstractPlugin 27 | 28 | logger = Logger(child=True) 29 | 30 | 31 | class SnapshotVolumes(AbstractPlugin): 32 | """ 33 | Take snapshots of all attached EBS volumes 34 | """ 35 | 36 | def execute(self) -> Optional[str]: 37 | try: 38 | instance_data = self.ec2.describe_instances(self.instance_id) 39 | 40 | block_device_mappings = instance_data.get("BlockDeviceMappings", []) 41 | if not block_device_mappings: 42 | logger.debug(f"No EBS volumes found on instance {self.instance_id}, skipping volume snapshot") 43 | return 44 | 45 | volume_ids = [] 46 | 47 | for block_device in block_device_mappings: 48 | volume_id = block_device.get("Ebs", {}).get("VolumeId") 49 | if volume_id: 50 | volume_ids.append(volume_id) 51 | logger.debug(f"Found EBS volume(s) to snapshot: {volume_ids}") 52 | 53 | # Triggering snapshots 54 | for volume_id in volume_ids: 55 | # TODO: this method can be throttled 56 | self.ec2.create_snapshot(self.instance_id, volume_id) 57 | 58 | message = f"Snapshotted EBS volumes {volume_ids} on instance {self.instance_id}" 59 | except Exception: 60 | message = f"Unable to snapshot EBS volumes on instance {self.instance_id}" 61 | logger.exception(message) 62 | 63 | return message 64 | -------------------------------------------------------------------------------- /src/quarantine/plugins/05_preserve_volumes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import Optional 23 | 24 | from aws_lambda_powertools import Logger 25 | 26 | from quarantine.plugins.abstract_plugin import AbstractPlugin 27 | 28 | logger = Logger(child=True) 29 | 30 | 31 | class PreserveVolumes(AbstractPlugin): 32 | """ 33 | Enable volume termination protection on all attached volumes 34 | """ 35 | 36 | def execute(self) -> Optional[str]: 37 | try: 38 | instance_data = self.ec2.describe_instances(self.instance_id) 39 | 40 | block_device_mappings = instance_data.get("BlockDeviceMappings", []) 41 | if not block_device_mappings: 42 | logger.debug(f"No EBS volumes found on instance {self.instance_id}, skipping volume termination protection") 43 | return 44 | 45 | device_names = [block_device["DeviceName"] for block_device in block_device_mappings] 46 | if not device_names: 47 | logger.debug(f"No EBS device names found on instance {self.instance_id}, skipping volume termination protection") 48 | return 49 | 50 | logger.debug(f"Found EBS block devices: {device_names}") 51 | 52 | # Enable termination protection on volumes 53 | self.ec2.enable_volume_termination_protection(self.instance_id, device_names) 54 | 55 | message = f"Enabled volume termination protection on attached volumes {device_names} on instance {self.instance_id}" 56 | except Exception: 57 | message = f"Unable to enable volume termination protection on instance {self.instance_id}" 58 | logger.exception(message) 59 | 60 | return message 61 | -------------------------------------------------------------------------------- /src/quarantine/resources/autoscaling.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from aws_lambda_powertools import Logger 23 | import boto3 24 | import botocore 25 | 26 | from quarantine.constants import BOTO3_CONFIG 27 | 28 | logger = Logger(child=True) 29 | 30 | __all__ = ["AutoScaling"] 31 | 32 | 33 | class AutoScaling: 34 | def __init__(self, session: boto3.Session) -> None: 35 | self.client = session.client("autoscaling", config=BOTO3_CONFIG) 36 | 37 | def detach_instance(self, instance_id: str) -> None: 38 | """ 39 | Detach an instance from any autoscaling groups 40 | """ 41 | 42 | logger.info(f"Checking if instance {instance_id} is attached to autoscaling groups") 43 | try: 44 | response = self.client.describe_auto_scaling_instances(InstanceIds=[instance_id]) 45 | logger.debug("Described auto scaling instances") 46 | except botocore.exceptions.ClientError: 47 | logger.exception("Failed to describe auto scaling instances") 48 | raise 49 | 50 | instances = response.get("AutoScalingInstances", []) 51 | if not instances: 52 | logger.info(f"Instance {instance_id} not attached to any auto scaling groups") 53 | return 54 | 55 | for instance in instances: 56 | asg_name = instance["AutoScalingGroupName"] 57 | logger.info(f"Detaching {instance_id} from {asg_name}") 58 | try: 59 | self.client.detach_instances( 60 | InstanceIds=[instance_id], 61 | AutoScalingGroupName=asg_name, 62 | ShouldDecrementDesiredCapacity=False, 63 | ) 64 | logger.info(f"Detached {instance_id} from {asg_name}") 65 | except botocore.exceptions.ClientError: 66 | logger.exception(f"Failed to detach {instance_id} from {asg_name}") 67 | -------------------------------------------------------------------------------- /doc/architecture.drawio: -------------------------------------------------------------------------------- 1 | 7Vxtd6OqFv41+dgsQaPpx7zOmbvaWT2Tc87c+ZRFlSScqmQpaZv59RcUEwXydqtNOk2nsypbBNzs52GzAVv2IHr9kqDl4p4GOGxBK3ht2cMWhLDjdvgfIVnnkttbLxfMExLkIrAVTMgvLIWWlK5IgNNKRkZpyMiyKvRpHGOfVWQoSehLNduMhtVal2iONcHER6Eu/UECtpBSYFnbG39gMl/IqrsdeSNCRWYpSBcooC8lkT1q2YOEUpZfRa8DHArlFXrJnxvvuLtpWIJjdswDzp1npT/H3eSn25nTezQE/7m5kaU8o3AlX7j3Y8IFg5CuAtluti6UsaQkZplCO33+y+sbWK0OvzMQqTbsKAI17VUFQE+JMqoCNe1VBUAtHij1A7WBJYGWqhRvKfVbpQbyX7tPVywkMR5sTM/iwnmCAsK7ZEBDmnBZTGOuvf6CRSFPAX75siAMT5bIF1p94bDhshmNmTR+AIu0VLwolZs3Q7yuRKaznsDJ6BnnHZLnCUO0TMnj5qkE+6skJc/4O07zwoWUG+JSXEevc4HZNnpJnfY8oatl1vyvvC7j3Sm/nPrCMKYoZKIgltAnXLxoC9r831gYX39GwlBRwDNOGOG46oVkLspnVFSHZCrEs6xErhUSz++y1NC2pCZMVQQoXeBAvpKOhcKwea34tSSS2PiCaYRZsuZZ5N2bTgFpyVQFkF9KsLcKWlqUIN8pEI4k18w3hW/hyC8kIk9AJ3Q1eGqYxAHnK5mUquadFQcb3dCELeicxii8o0LnmQ3+ixlbS4NDK0arFrpTnSldJT7exye25GiUzDHb92pyXBDN39s9CQ4R4yZcZfY3aPpPNJj/FS1mExffePDXzdiZ3N/AHTw4YXjJ/4xXsc8IjVNhDzR5moWcya/seGVHhR1Tbi7TWWEs042pGLhyMITQdRrlyk0VBq7UiNGA991caStceatz5YY/y1TpOE1RZUHMZQRH6BfvLOhmg9Vjwq/m4urLCiXBcMXWGoLTJ8z8RdmIVDgbIW2CtRHaOrwr2TLAGWpQhSaZpwuBnq3AqC40yUyEpD4NDE8D5enddLBrcFdpgt8bdxzPuS3dGxIOYIGzDDeJsLsKlgQAPGCDsQl9s+xHhUaBuzv0iMMHmhJZ/CNljEYHgeljwTpVOjtEXShd5uqYkVfRDjP/JDgfeXP24cyVGnlIGHYgDLsej8jpVlFuOzrMeZfoKL9tCuSd0/yhresz2krP6h5ZR7pHwDmXe7S3OUewazbo9vlcnjf8yq8fiF/HzrjbP41f+wNgd9xPw69YmPZjbtr1MKytMSzQGbZrYNhuU0A/ccbZEMNylSbr/8rns8RPkeA4kMnha/nmcF1OPeCEcG1sZgNvoGtHp2vj9NG9KLaG9u8bNrAuStPAO6zpj6DWs9nvPjSVlDomccDnulz413pp8Cuy4aeIz8Oq+lTCFxNyf4ES1g6wT9JsBDJGHXaw+UkTZegoBG/rLrRrmCh7NcyTjboFZ+KGLaNvSfxnhcPNjG40zINo2GdWB8k8n2Wcy/gNrvbX3r2xjzI3qqpmzVVSPaqIBEE+RougF9pGw6ru+HCf/cu1Nvlwa7PCVe6UPca3Ey1W2ynCwhIu0jaP1rws+kG8SxmFlUJvoFctgc5mKbcItec2DXxDZ+rra/WPD/iVsBKyeGoDLH69xZVIrEsJ1U/aBVFweRDN1fgGiO4yFFCla1hEHIoy8pbJxxqwF30dYmL/Jtg/MFJy8AMbVLQP3ob+5uHdfQ901z8Vagqqe3zQ+qEKbNWzel+o3mp9PxqYXakPB9XuIaRC1/WUIfWCoLp3KlTqrz9XKEExI9zB1aKYdyh6DJDWndcAZkV2WQFMrzuynNMCmEOrM+Bz+M8SwAxzq64ndgnA+WKXRtrSJ04DGi0TGpGUK06DeH/lP0muuUL8Y0DctXq27Z0Gceh5AHyeNYrUrilypS5NgFt4Xnjra79ZXMT6TkPDAM47GVp/p7wr9gL8TRYpY2PqotjY6hoMTma+OFtT90qZQqa5/dHMIW3CthwPtB33KPOCXttqysJOXfyqd92klllZEa84GEE5b4xTx/J3/MzBwmVig76G5hSnab79cu/02hTF1/T8xoC+q5iuqwf0N4cDKruEG4vod9/FSLdRRlgOMxa3zGHGYy27gUieXY0kd7oKa+RQ0sID/0dJDQcazruaWwsrFS0+yErOOVmpaGVJ0f2Q+k/ipVePIfF1XkK+z5np8ljJ5Kw1xkrmhXpNl/88DDRFXbfMf+ot889L3zQx445fFzS7Nb7X63v9ruqP72S448MiXhWI0LRlFhqA2CASDTvjuQ5niFvYNShyDYp8rqDIG/drKsOsZUD3e27XNLgs8tzaOmU4SnWA36MYzQ9FRa4IrxXhV3VeFGGObcfpwtMI85PtdE9z+phGki1qYc+OQp6O07bKP8cFAJvjUj0oVZxxsSYkWpqizN8od6R4N2a9y7Ph5Jn413MvH4oNrudeDrJBnNbkPylLxifE/Zs78aK7UA8JeUZMBKPT1WNsmA5dwxaXELYA5wpbpKI4wtbTbeZJRgRFwSorAMcb9Xs6j4zcMRx364tqbOqpO6pxC9vKxuxiBa8EW9vzilxl5ILi4wC1Q1dfrsu24Flf45Sh+DoOf6xx+Lp969A47NNouWLYCOn9Y9vRQAeupyDdsL75ruOz7pVPJP22xDfjBAGrMDeEjTUDGQ7tjlDEji5Xu1Ex5M3TjXUEVPvBcHAMQGgkXLspwjUcZxmFKGWmpbFvmInP32R0zFEzQ4f4uIGtOM4Qer2e1v0ffisOzpU+jXMdT8lGww1apOtklF8ySe/M1KAfvv02+sEFN/x/ZXO3cI2Exj8Hc6hHTh2dOCyzp1Z8b67+2Iq+N1fT/WWdSDdtWbiso9PmZusMfeGKPv7oP3wnTfPk9qus+S6e7bdt7dH/AA== -------------------------------------------------------------------------------- /src/quarantine/resources/elbv2.py: -------------------------------------------------------------------------------- 1 | """ 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | """ 18 | 19 | from aws_lambda_powertools import Logger 20 | import boto3 21 | import botocore 22 | 23 | from quarantine.constants import BOTO3_CONFIG 24 | 25 | logger = Logger(child=True) 26 | 27 | __all__ = ["ELBv2"] 28 | 29 | 30 | class ELBv2: 31 | def __init__(self, session: boto3.Session) -> None: 32 | self.client = session.client("elbv2", config=BOTO3_CONFIG) 33 | 34 | def deregister_target(self, instance_id: str) -> None: 35 | """ 36 | Deregister instance from any target groups 37 | """ 38 | 39 | logger.info(f"Checking if instance {instance_id} is registered with any target groups") 40 | try: 41 | response = self.client.describe_target_groups() 42 | logger.debug("Described target groups") 43 | except botocore.exceptions.ClientError: 44 | logger.exception("Failed to describe target groups") 45 | raise 46 | 47 | for tg in response.get("TargetGroups", []): 48 | target_group_arn = tg["TargetGroupArn"] 49 | found_instance = False 50 | 51 | instances = self.client.describe_target_health(TargetGroupArn=target_group_arn) 52 | for target in instances.get("TargetHealthDescriptions", []): 53 | if instance_id == target["Target"]["Id"]: 54 | logger.info(f"Found {instance_id} registered to target group {target_group_arn}") 55 | found_instance = True 56 | break 57 | 58 | if found_instance: 59 | params = { 60 | "TargetGroupArn": target_group_arn, 61 | "Targets": [ 62 | { 63 | "Id": instance_id 64 | } 65 | ] 66 | } 67 | try: 68 | self.client.deregister_targets(**params) 69 | logger.info(f"Deregistered instance {instance_id} from target group {target_group_arn}") 70 | except botocore.exceptions.ClientError: 71 | logger.exception(f"Failed to deregister instance {instance_id} from target group {target_group_arn}") 72 | -------------------------------------------------------------------------------- /src/quarantine/resources/elb.py: -------------------------------------------------------------------------------- 1 | """ 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | """ 18 | 19 | from aws_lambda_powertools import Logger 20 | import boto3 21 | import botocore 22 | 23 | from quarantine.constants import BOTO3_CONFIG 24 | 25 | logger = Logger(child=True) 26 | 27 | __all__ = ["ELB"] 28 | 29 | 30 | class ELB: 31 | def __init__(self, session: boto3.Session) -> None: 32 | self.client = session.client("elb", config=BOTO3_CONFIG) 33 | 34 | def deregister_instance(self, instance_id: str) -> None: 35 | """ 36 | Deregister instance from any classic ELBs 37 | """ 38 | 39 | logger.info(f"Checking if instance {instance_id} is registered with any classic ELBs") 40 | try: 41 | response = self.client.describe_load_balancers() 42 | logger.debug("Described load balancers") 43 | except botocore.exceptions.ClientError: 44 | logger.exception("Failed to describe load balancers") 45 | raise 46 | 47 | for lb in response.get("LoadBalancerDescriptions", []): 48 | load_balancer_name = lb["LoadBalancerName"] 49 | found_instance = False 50 | 51 | instances = self.client.describe_instance_health(LoadBalancerName=load_balancer_name) 52 | for instance in instances.get("InstanceStates", []): 53 | if instance_id == instance["InstanceId"]: 54 | logger.info(f"Found {instance_id} registered to ELB {load_balancer_name}") 55 | found_instance = True 56 | break 57 | 58 | if found_instance: 59 | params = { 60 | "LoadBalancerName": load_balancer_name, 61 | "Instances": [ 62 | { 63 | "InstanceId": instance_id 64 | } 65 | ] 66 | } 67 | try: 68 | self.client.deregister_instances_from_load_balancer(**params) 69 | logger.info(f"Deregistered instance {instance_id} from ELB {load_balancer_name}") 70 | except botocore.exceptions.ClientError: 71 | logger.exception(f"Failed to deregister instance {instance_id} from ELB {load_balancer_name}") 72 | -------------------------------------------------------------------------------- /src/quarantine/lambda_handler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | import inspect 23 | import importlib 24 | import pkgutil 25 | from typing import Dict, Any 26 | 27 | from aws_lambda_powertools import Logger 28 | from aws_lambda_powertools.utilities.typing import LambdaContext 29 | from aws_lambda_powertools.utilities.validation import validator 30 | import boto3 31 | 32 | import quarantine.plugins 33 | from quarantine.plugins.abstract_plugin import AbstractPlugin 34 | from quarantine.resources import SNS 35 | from quarantine.schemas import INPUT 36 | 37 | logger = Logger() 38 | 39 | 40 | def iter_namespace(ns_pkg): 41 | # https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/#using-namespace-packages 42 | return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + ".") 43 | 44 | 45 | discovered_plugins = { 46 | name: importlib.import_module(name) 47 | for _, name, ispkg in iter_namespace(quarantine.plugins) 48 | if not ispkg 49 | } 50 | 51 | logger.debug(f"Discovered plugins: {discovered_plugins}") 52 | 53 | 54 | @validator(inbound_schema=INPUT) 55 | @logger.inject_lambda_context(log_event=True) 56 | def handler(event: Dict[str, Any], context: LambdaContext) -> None: 57 | 58 | finding_id = event.get("id") 59 | instance_id = event.get("resource", {}).get("instanceDetails", {}).get("instanceId") 60 | if not instance_id: 61 | raise Exception("instanceId not found in request") 62 | 63 | logger.append_keys(instance_id=instance_id) 64 | 65 | session = boto3._get_default_session() 66 | 67 | plugins = [] 68 | for _, plugin_module in discovered_plugins.items(): 69 | clsmembers = inspect.getmembers(plugin_module, inspect.isclass) 70 | for (_, c) in clsmembers: 71 | if issubclass(c, AbstractPlugin) and (c is not AbstractPlugin): 72 | plugins.append(c(session, instance_id, finding_id)) 73 | 74 | logger.info(f"Loaded plugins: {plugins}") 75 | 76 | sns = SNS(session) 77 | 78 | for plugin in plugins: 79 | message = plugin.execute() 80 | if message is not None: 81 | sns.publish(instance_id, message) 82 | 83 | message = f"Instance {instance_id} successfully quarantined" 84 | sns.publish(instance_id, message) 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /src/quarantine/plugins/08_command_output.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | import os 23 | import time 24 | from typing import Optional 25 | 26 | from aws_lambda_powertools import Logger 27 | 28 | from quarantine.plugins.abstract_plugin import AbstractPlugin 29 | from quarantine.constants import SSM_COMMANDS 30 | 31 | logger = Logger(child=True) 32 | EC2_INSTANCE_PROFILE_ARN = os.getenv("EC2_INSTANCE_PROFILE_ARN") 33 | 34 | 35 | class CommandOutput(AbstractPlugin): 36 | """ 37 | Run commands from SSM and upload results to S3 38 | """ 39 | 40 | def execute(self) -> Optional[str]: 41 | if not SSM_COMMANDS: 42 | logger.debug(f"No commands to execute on {self.instance_id}, skipping") 43 | return 44 | 45 | is_ssm_managed = len(self.ssm.describe_instance_information(self.instance_id)) > 0 46 | 47 | try: 48 | # remove any existing EC2 instance profiles 49 | self.ec2.remove_ec2_instance_profile(self.instance_id) 50 | except Exception: 51 | logger.exception(f"Unable to remove EC2 instance profile from {self.instance_id}") 52 | return 53 | 54 | if not is_ssm_managed: 55 | message = ( 56 | f"Instance {self.instance_id} was not managed by SSM, skipping command capture" 57 | ) 58 | logger.debug(message) 59 | return message 60 | 61 | if not EC2_INSTANCE_PROFILE_ARN: 62 | logger.warning("EC2_INSTANCE_PROFILE_ARN is not defined, unable to issue commands") 63 | return 64 | 65 | try: 66 | # attach limited EC2 instance profile 67 | self.ec2.attach_ec2_instance_profile(self.instance_id, EC2_INSTANCE_PROFILE_ARN) 68 | 69 | # wait 5 seconds for the instance profile to stabilize 70 | time.sleep(5) 71 | 72 | self.ssm.send_commands(self.instance_id, SSM_COMMANDS) 73 | 74 | # remove the limited EC2 instance profiles 75 | self.ec2.remove_ec2_instance_profile(self.instance_id) 76 | 77 | message = f"Captured output from {self.instance_id} for commands: {SSM_COMMANDS}" 78 | except Exception: 79 | message = ( 80 | f"Unable to capture output from {self.instance_id} for commands: {SSM_COMMANDS}" 81 | ) 82 | logger.exception(message) 83 | 84 | return message 85 | -------------------------------------------------------------------------------- /events/guardduty_ec2_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "2.0", 3 | "accountId": "123456789012", 4 | "region": "us-east-1", 5 | "partition": "aws", 6 | "id": "16afba5c5c43e07c9e3e5e2e544e95df", 7 | "arn": "arn:aws:guardduty:us-east-1:123456789012:detector/123456789012/finding/16afba5c5c43e07c9e3e5e2e544e95df", 8 | "type": "Canary:EC2/Stateless.IntegTest", 9 | "resource": { 10 | "resourceType": "Instance", 11 | "instanceDetails": { 12 | "instanceId": "i-05746eb48123455e0", 13 | "instanceType": "t2.micro", 14 | "launchTime": 1492735675000, 15 | "productCodes": [], 16 | "networkInterfaces": [ 17 | { 18 | "ipv6Addresses": [], 19 | "privateDnsName": "ip-0-0-0-0.us-east-1.compute.internal", 20 | "privateIpAddress": "0.0.0.0", 21 | "privateIpAddresses": [ 22 | { 23 | "privateDnsName": "ip-0-0-0-0.us-east-1.compute.internal", 24 | "privateIpAddress": "0.0.0.0" 25 | } 26 | ], 27 | "subnetId": "subnet-d58b7123", 28 | "vpcId": "vpc-34865123", 29 | "securityGroups": [ 30 | { 31 | "groupName": "launch-wizard-1", 32 | "groupId": "sg-9918a123" 33 | } 34 | ], 35 | "publicDnsName": "ec2-11-111-111-1.us-east-1.compute.amazonaws.com", 36 | "publicIp": "11.111.111.1" 37 | } 38 | ], 39 | "tags": [ 40 | { 41 | "key": "Name", 42 | "value": "ssh-22-open" 43 | } 44 | ], 45 | "instanceState": "running", 46 | "availabilityZone": "us-east-1b", 47 | "imageId": "ami-4836a123", 48 | "imageDescription": "Amazon Linux AMI 2017.03.0.20170417 x86_64 HVM GP2" 49 | } 50 | }, 51 | "service": { 52 | "serviceName": "guardduty", 53 | "detectorId": "3caf4e0aaa46ce4ccbcef949a8785353", 54 | "action": { 55 | "actionType": "NETWORK_CONNECTION", 56 | "networkConnectionAction": { 57 | "connectionDirection": "OUTBOUND", 58 | "remoteIpDetails": { 59 | "ipAddressV4": "0.0.0.0", 60 | "organization": { 61 | "asn": -1, 62 | "isp": "GeneratedFindingISP", 63 | "org": "GeneratedFindingORG" 64 | }, 65 | "country": { 66 | "countryName": "United States" 67 | }, 68 | "city": { 69 | "cityName": "GeneratedFindingCityName" 70 | }, 71 | "geoLocation": { 72 | "lat": 0, 73 | "lon": 0 74 | } 75 | }, 76 | "remotePortDetails": { 77 | "port": 22, 78 | "portName": "SSH" 79 | }, 80 | "localPortDetails": { 81 | "port": 2000, 82 | "portName": "Unknown" 83 | }, 84 | "protocol": "TCP", 85 | "blocked": false 86 | } 87 | }, 88 | "resourceRole": "TARGET", 89 | "additionalInfo": { 90 | "unusualProtocol": "UDP", 91 | "threatListName": "GeneratedFindingCustomerListName", 92 | "unusual": 22 93 | }, 94 | "eventFirstSeen": "2017-10-31T23:16:23Z", 95 | "eventLastSeen": "2017-10-31T23:16:23Z", 96 | "archived": false, 97 | "count": 1 98 | }, 99 | "severity": 5, 100 | "createdAt": "2017-10-31T23:16:23.824Z", 101 | "updatedAt": "2017-10-31T23:16:23.824Z", 102 | "title": "Canary:EC2/Stateless.IntegTest", 103 | "description": "Canary:EC2/Stateless.IntegTest" 104 | } 105 | -------------------------------------------------------------------------------- /src/quarantine/plugins/11_isolate_instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import Optional 23 | 24 | from aws_lambda_powertools import Logger 25 | 26 | from quarantine.plugins.abstract_plugin import AbstractPlugin 27 | from quarantine.utils import now 28 | 29 | logger = Logger(child=True) 30 | 31 | 32 | class IsolateInstance(AbstractPlugin): 33 | """ 34 | Isolate the EC2 instance by moving any attached network interfaces into new security groups 35 | """ 36 | 37 | def execute(self) -> Optional[str]: 38 | try: 39 | network_interfaces = self.ec2.describe_network_interfaces(self.instance_id) 40 | if not network_interfaces: 41 | logger.info(f"No network interfaces found on instance {self.instance_id}") 42 | return 43 | 44 | vpc_ids = {network_interface["VpcId"] for network_interface in network_interfaces} 45 | logger.info( 46 | f"Found {len(network_interfaces)} network interface(s) in {len(vpc_ids)} VPCs" 47 | ) 48 | 49 | tags = [ 50 | { 51 | "Key": "Name", 52 | "Value": f"quarantine-{self.instance_id}", 53 | }, 54 | { 55 | "Key": "SOC-InstanceId", 56 | "Value": self.instance_id, 57 | }, 58 | { 59 | "Key": "SOC-Status", 60 | "Value": "quarantined", 61 | }, 62 | { 63 | "Key": "SOC-ContainedAt", 64 | "Value": now(), 65 | }, 66 | { 67 | "Key": "SOC-FindingId", 68 | "Value": self.finding_id, 69 | }, 70 | { 71 | "Key": "SOC-FindingSource", 72 | "Value": "GuardDuty", 73 | }, 74 | ] 75 | 76 | vpc_map = {} 77 | for vpc_id in vpc_ids: 78 | 79 | existing_sg = self.ec2.describe_security_groups(self.instance_id, vpc_id) 80 | if existing_sg: 81 | vpc_map[vpc_id] = existing_sg[0]["GroupId"] 82 | else: 83 | vpc_map[vpc_id] = self.ec2.create_security_group(self.instance_id, vpc_id, tags) 84 | 85 | for network_interface in network_interfaces: 86 | group_id = vpc_map.get(network_interface["VpcId"]) 87 | if group_id: 88 | self.ec2.update_security_groups( 89 | network_interface["NetworkInterfaceId"], group_id 90 | ) 91 | 92 | message = f"Isolated instance {self.instance_id} into restricted security groups" 93 | except Exception: 94 | message = f"Unable to isolate instance {self.instance_id}" 95 | logger.exception(message) 96 | 97 | return message 98 | -------------------------------------------------------------------------------- /src/quarantine/resources/ssm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | import os 23 | import time 24 | from typing import List, Dict, Any 25 | 26 | from aws_lambda_powertools import Logger 27 | import boto3 28 | import botocore 29 | 30 | from quarantine.constants import SSM_DRAIN_TIME_SECS, BOTO3_CONFIG 31 | from quarantine.utils import get_prefix 32 | 33 | BUCKET_NAME = os.environ["ARTIFACT_BUCKET"] 34 | NOTIFICATION_TOPIC_ARN = os.environ["NOTIFICATION_TOPIC_ARN"] 35 | SSM_ROLE_ARN = os.environ["SSM_ROLE_ARN"] 36 | logger = Logger(child=True) 37 | 38 | __all__ = ["SSM"] 39 | 40 | 41 | class SSM: 42 | def __init__(self, session: boto3.Session) -> None: 43 | self.client = session.client("ssm", config=BOTO3_CONFIG) 44 | 45 | def describe_instance_information(self, instance_id: str) -> List[Dict[str, Any]]: 46 | """ 47 | Describing instances managed by SSM 48 | """ 49 | 50 | logger.info(f"Checking if instance {instance_id} managed by SSM") 51 | try: 52 | response = self.client.describe_instance_information( 53 | Filters=[{"Key": "InstanceIds", "Values": [instance_id]}] 54 | ) 55 | logger.debug(f"Described SSM instance information for {instance_id}") 56 | except botocore.exceptions.ClientError: 57 | logger.exception(f"Failed to describe SSM instance information for {instance_id}") 58 | raise 59 | 60 | return response.get("InstanceInformationList", []) 61 | 62 | def send_commands(self, instance_id: str, commands: List[str]) -> None: 63 | """ 64 | Send commands through SSM to an instance 65 | """ 66 | 67 | prefix = get_prefix(instance_id) 68 | 69 | params = { 70 | "InstanceIds": [instance_id], 71 | "DocumentName": "AWS-RunShellScript", 72 | "TimeoutSeconds": 240, 73 | "Parameters": { 74 | "commands": commands, 75 | "executionTimeout": ["3600"], 76 | "workingDirectory": ["/tmp"], 77 | }, 78 | "OutputS3BucketName": BUCKET_NAME, 79 | "OutputS3KeyPrefix": f"{prefix}/ssm-output-file", 80 | "ServiceRoleArn": SSM_ROLE_ARN, 81 | "NotificationConfig": { 82 | "NotificationArn": NOTIFICATION_TOPIC_ARN, 83 | "NotificationEvents": [ 84 | "Success", 85 | "TimedOut", 86 | "Cancelled", 87 | "Failed", 88 | ], 89 | "NotificationType": "Invocation", 90 | }, 91 | } 92 | 93 | logger.info(f"Sending SSM commands to {instance_id}: {commands}") 94 | try: 95 | response = self.client.send_command(**params) 96 | logger.debug(f"Sent SSM commands to {instance_id}") 97 | except botocore.exceptions.ClientError: 98 | logger.exception(f"Failed to send SSM commands to {instance_id}") 99 | raise 100 | 101 | command_id = response["Command"]["CommandId"] 102 | 103 | waiter = self.client.get_waiter("command_executed") 104 | try: 105 | # wait 60 seconds for all commands to execute 106 | waiter.wait( 107 | CommandId=command_id, 108 | InstanceId=instance_id, 109 | WaiterConfig={"Delay": 3, "MaxAttempts": 20}, 110 | ) 111 | logger.info(f"Waiting {SSM_DRAIN_TIME_SECS} seconds for SSM to complete uploads") 112 | time.sleep(SSM_DRAIN_TIME_SECS) 113 | except Exception: 114 | logger.exception("SSM commands were queued, but failed to execute before timeout") 115 | -------------------------------------------------------------------------------- /events/guardduty_iam_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "2.0", 3 | "accountId": "123456789012", 4 | "region": "us-east-1", 5 | "partition": "aws", 6 | "id": "30bf285e52a1e405084ff6ec28e06e17", 7 | "arn": "arn:aws:guardduty:us-east-1:123456789012:detector/123456789012/finding/30bf285e52a1e405084ff6ec28e06e17", 8 | "type": "Recon:IAMUser/MaliciousIPCaller.Custom", 9 | "resource": { 10 | "resourceType": "AccessKey", 11 | "accessKeyDetails": { 12 | "accessKeyId": "GeneratedFindingAccessKeyId", 13 | "principalId": "GeneratedFindingPrincipalId", 14 | "userType": "IAMUser", 15 | "userName": "GeneratedFindingUserName" 16 | }, 17 | "instanceDetails": { 18 | "instanceId": "i-99999999", 19 | "instanceType": "m3.xlarge", 20 | "outpostArn": "arn:aws:outposts:us-west-2:123456789000:outpost/op-0fbc006e9abbc73c3", 21 | "launchTime": "2016-08-02T02:05:06.000Z", 22 | "platform": null, 23 | "productCodes": [ 24 | { 25 | "productCodeId": "GeneratedFindingProductCodeId", 26 | "productCodeType": "GeneratedFindingProductCodeType" 27 | } 28 | ], 29 | "iamInstanceProfile": { 30 | "arn": "arn:aws:iam::755484374681:example/instance/profile", 31 | "id": "GeneratedFindingInstanceProfileId" 32 | }, 33 | "networkInterfaces": [ 34 | { 35 | "ipv6Addresses": [], 36 | "networkInterfaceId": "eni-bfcffe88", 37 | "privateDnsName": "GeneratedFindingPrivateDnsName", 38 | "privateIpAddress": "10.0.0.1", 39 | "privateIpAddresses": [ 40 | { 41 | "privateDnsName": "GeneratedFindingPrivateName", 42 | "privateIpAddress": "10.0.0.1" 43 | } 44 | ], 45 | "subnetId": "GeneratedFindingSubnetId", 46 | "vpcId": "GeneratedFindingVPCId", 47 | "securityGroups": [ 48 | { 49 | "groupName": "GeneratedFindingSecurityGroupName", 50 | "groupId": "GeneratedFindingSecurityId" 51 | } 52 | ], 53 | "publicDnsName": "GeneratedFindingPublicDNSName", 54 | "publicIp": "198.51.100.0" 55 | } 56 | ], 57 | "tags": [ 58 | { 59 | "key": "GeneratedFindingInstaceTag1", 60 | "value": "GeneratedFindingInstaceValue1" 61 | }, 62 | { 63 | "key": "GeneratedFindingInstaceTag2", 64 | "value": "GeneratedFindingInstaceTagValue2" 65 | }, 66 | { 67 | "key": "GeneratedFindingInstaceTag3", 68 | "value": "GeneratedFindingInstaceTagValue3" 69 | }, 70 | { 71 | "key": "GeneratedFindingInstaceTag4", 72 | "value": "GeneratedFindingInstaceTagValue4" 73 | }, 74 | { 75 | "key": "GeneratedFindingInstaceTag5", 76 | "value": "GeneratedFindingInstaceTagValue5" 77 | }, 78 | { 79 | "key": "GeneratedFindingInstaceTag6", 80 | "value": "GeneratedFindingInstaceTagValue6" 81 | }, 82 | { 83 | "key": "GeneratedFindingInstaceTag7", 84 | "value": "GeneratedFindingInstaceTagValue7" 85 | }, 86 | { 87 | "key": "GeneratedFindingInstaceTag8", 88 | "value": "GeneratedFindingInstaceTagValue8" 89 | }, 90 | { 91 | "key": "GeneratedFindingInstaceTag9", 92 | "value": "GeneratedFindingInstaceTagValue9" 93 | } 94 | ], 95 | "instanceState": "running", 96 | "availabilityZone": "GeneratedFindingInstaceAvailabilityZone", 97 | "imageId": "ami-99999999", 98 | "imageDescription": "GeneratedFindingInstaceImageDescription" 99 | } 100 | }, 101 | "service": { 102 | "serviceName": "guardduty", 103 | "detectorId": "48bf285e13736c6c722945805b3e224a", 104 | "action": { 105 | "actionType": "AWS_API_CALL", 106 | "awsApiCallAction": { 107 | "api": "GeneratedFindingAPIName", 108 | "serviceName": "GeneratedFindingAPIServiceName", 109 | "callerType": "Remote IP", 110 | "errorCode": "AccessDenied", 111 | "remoteIpDetails": { 112 | "ipAddressV4": "198.51.100.0", 113 | "organization": { 114 | "asn": "-1", 115 | "asnOrg": "GeneratedFindingASNOrg", 116 | "isp": "GeneratedFindingISP", 117 | "org": "GeneratedFindingORG" 118 | }, 119 | "country": { 120 | "countryName": "GeneratedFindingCountryName" 121 | }, 122 | "city": { 123 | "cityName": "GeneratedFindingCityName" 124 | }, 125 | "geoLocation": { 126 | "lat": 0, 127 | "lon": 0 128 | } 129 | }, 130 | "affectedResources": {} 131 | } 132 | }, 133 | "resourceRole": "TARGET", 134 | "additionalInfo": { 135 | "apiCalls": [ 136 | { 137 | "name": "GeneratedFindingAPIName1", 138 | "count": 2, 139 | "firstSeen": 1513721039, 140 | "lastSeen": 1513721040 141 | }, 142 | { 143 | "name": "GeneratedFindingAPIName2", 144 | "count": 1, 145 | "firstSeen": 1513721040, 146 | "lastSeen": 1513721040 147 | } 148 | ], 149 | "sample": true 150 | }, 151 | "evidence": { 152 | "threatIntelligenceDetails": [ 153 | { 154 | "threatListName": "GeneratedFindingThreatListName", 155 | "threatNames": [ 156 | "GeneratedFindingThreatName" 157 | ] 158 | } 159 | ] 160 | }, 161 | "eventFirstSeen": "2022-01-13T00:00:47.000Z", 162 | "eventLastSeen": "2022-01-13T00:00:47.000Z", 163 | "archived": true, 164 | "count": 1 165 | }, 166 | "severity": 5, 167 | "createdAt": "2022-01-13T00:00:47.427Z", 168 | "updatedAt": "2022-01-13T00:00:47.427Z", 169 | "title": "Reconnaissance API GeneratedFindingAPIName was invoked from an IP address on a custom threat list.", 170 | "description": "APIs commonly used in reconnaissance attacks, was invoked from an IP address on the custom threat list. Unauthorized actors perform such activity to gather information and discover resources like databases, S3 buckets etc., in order to further tailor the attack." 171 | } 172 | -------------------------------------------------------------------------------- /events/guardduty_s3_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "2.0", 3 | "accountId": "123456789012", 4 | "region": "us-east-1", 5 | "partition": "aws", 6 | "id": "2cbf285e52a6e74ff96f4cb3cf93296d", 7 | "arn": "arn:aws:guardduty:us-east-1:123456789012:detector/123456789012/finding/2cbf285e52a6e74ff96f4cb3cf93296d", 8 | "type": "PenTest:S3/ParrotLinux", 9 | "resource": { 10 | "resourceType": "S3Bucket", 11 | "accessKeyDetails": { 12 | "accessKeyId": "GeneratedFindingAccessKeyId", 13 | "principalId": "GeneratedFindingPrincipalId", 14 | "userType": "IAMUser", 15 | "userName": "GeneratedFindingUserName" 16 | }, 17 | "s3BucketDetails": [ 18 | { 19 | "arn": "arn:aws:s3:::bucketName", 20 | "name": "bucketName", 21 | "type": "Destination", 22 | "createdAt": 1513612691.551, 23 | "owner": { 24 | "id": "CanonicalId of Owner" 25 | }, 26 | "tags": [ 27 | { 28 | "key": "foo", 29 | "value": "bar" 30 | } 31 | ], 32 | "defaultServerSideEncryption": { 33 | "encryptionType": "SSEAlgorithm", 34 | "kmsMasterKeyArn": "arn:aws:kms:region:123456789012:key/key-id" 35 | }, 36 | "publicAccess": { 37 | "permissionConfiguration": { 38 | "bucketLevelPermissions": { 39 | "accessControlList": { 40 | "allowsPublicReadAccess": false, 41 | "allowsPublicWriteAccess": false 42 | }, 43 | "bucketPolicy": { 44 | "allowsPublicReadAccess": false, 45 | "allowsPublicWriteAccess": false 46 | }, 47 | "blockPublicAccess": { 48 | "ignorePublicAcls": false, 49 | "restrictPublicBuckets": false, 50 | "blockPublicAcls": false, 51 | "blockPublicPolicy": false 52 | } 53 | }, 54 | "accountLevelPermissions": { 55 | "blockPublicAccess": { 56 | "ignorePublicAcls": false, 57 | "restrictPublicBuckets": false, 58 | "blockPublicAcls": false, 59 | "blockPublicPolicy": false 60 | } 61 | } 62 | }, 63 | "effectivePermission": "NOT_PUBLIC" 64 | } 65 | } 66 | ], 67 | "instanceDetails": { 68 | "instanceId": "i-99999999", 69 | "instanceType": "m3.xlarge", 70 | "outpostArn": "arn:aws:outposts:us-west-2:123456789000:outpost/op-0fbc006e9abbc73c3", 71 | "launchTime": "2016-08-02T02:05:06.000Z", 72 | "platform": null, 73 | "productCodes": [ 74 | { 75 | "productCodeId": "GeneratedFindingProductCodeId", 76 | "productCodeType": "GeneratedFindingProductCodeType" 77 | } 78 | ], 79 | "iamInstanceProfile": { 80 | "arn": "arn:aws:iam::755484374681:example/instance/profile", 81 | "id": "GeneratedFindingInstanceProfileId" 82 | }, 83 | "networkInterfaces": [ 84 | { 85 | "ipv6Addresses": [], 86 | "networkInterfaceId": "eni-bfcffe88", 87 | "privateDnsName": "GeneratedFindingPrivateDnsName", 88 | "privateIpAddress": "10.0.0.1", 89 | "privateIpAddresses": [ 90 | { 91 | "privateDnsName": "GeneratedFindingPrivateName", 92 | "privateIpAddress": "10.0.0.1" 93 | } 94 | ], 95 | "subnetId": "GeneratedFindingSubnetId", 96 | "vpcId": "GeneratedFindingVPCId", 97 | "securityGroups": [ 98 | { 99 | "groupName": "GeneratedFindingSecurityGroupName", 100 | "groupId": "GeneratedFindingSecurityId" 101 | } 102 | ], 103 | "publicDnsName": "GeneratedFindingPublicDNSName", 104 | "publicIp": "198.51.100.0" 105 | } 106 | ], 107 | "tags": [ 108 | { 109 | "key": "GeneratedFindingInstaceTag1", 110 | "value": "GeneratedFindingInstaceValue1" 111 | }, 112 | { 113 | "key": "GeneratedFindingInstaceTag2", 114 | "value": "GeneratedFindingInstaceTagValue2" 115 | }, 116 | { 117 | "key": "GeneratedFindingInstaceTag3", 118 | "value": "GeneratedFindingInstaceTagValue3" 119 | }, 120 | { 121 | "key": "GeneratedFindingInstaceTag4", 122 | "value": "GeneratedFindingInstaceTagValue4" 123 | }, 124 | { 125 | "key": "GeneratedFindingInstaceTag5", 126 | "value": "GeneratedFindingInstaceTagValue5" 127 | }, 128 | { 129 | "key": "GeneratedFindingInstaceTag6", 130 | "value": "GeneratedFindingInstaceTagValue6" 131 | }, 132 | { 133 | "key": "GeneratedFindingInstaceTag7", 134 | "value": "GeneratedFindingInstaceTagValue7" 135 | }, 136 | { 137 | "key": "GeneratedFindingInstaceTag8", 138 | "value": "GeneratedFindingInstaceTagValue8" 139 | }, 140 | { 141 | "key": "GeneratedFindingInstaceTag9", 142 | "value": "GeneratedFindingInstaceTagValue9" 143 | } 144 | ], 145 | "instanceState": "running", 146 | "availabilityZone": "GeneratedFindingInstaceAvailabilityZone", 147 | "imageId": "ami-99999999", 148 | "imageDescription": "GeneratedFindingInstaceImageDescription" 149 | } 150 | }, 151 | "service": { 152 | "serviceName": "guardduty", 153 | "detectorId": "48bf285e13736c6c722945805b3e224a", 154 | "action": { 155 | "actionType": "AWS_API_CALL", 156 | "awsApiCallAction": { 157 | "api": "GeneratedFindingAPIName", 158 | "serviceName": "GeneratedFindingAPIServiceName", 159 | "callerType": "Remote IP", 160 | "errorCode": "AccessDenied", 161 | "remoteIpDetails": { 162 | "ipAddressV4": "198.51.100.0", 163 | "organization": { 164 | "asn": "-1", 165 | "asnOrg": "GeneratedFindingASNOrg", 166 | "isp": "GeneratedFindingISP", 167 | "org": "GeneratedFindingORG" 168 | }, 169 | "country": { 170 | "countryName": "GeneratedFindingCountryName" 171 | }, 172 | "city": { 173 | "cityName": "GeneratedFindingCityName" 174 | }, 175 | "geoLocation": { 176 | "lat": 0, 177 | "lon": 0 178 | } 179 | }, 180 | "affectedResources": { 181 | "AWS::S3::Bucket": "GeneratedFindingS3Bucket" 182 | } 183 | } 184 | }, 185 | "resourceRole": "TARGET", 186 | "additionalInfo": { 187 | "unusual": { 188 | "hoursOfDay": [ 189 | 1513609200000 190 | ], 191 | "userNames": [ 192 | "GeneratedFindingUserName" 193 | ] 194 | }, 195 | "sample": true 196 | }, 197 | "eventFirstSeen": "2022-01-13T00:00:47.000Z", 198 | "eventLastSeen": "2022-01-13T00:00:47.000Z", 199 | "archived": true, 200 | "count": 1 201 | }, 202 | "severity": 5, 203 | "createdAt": "2022-01-13T00:00:47.437Z", 204 | "updatedAt": "2022-01-13T00:00:47.437Z", 205 | "title": "API GeneratedFindingAPIName was invoked from a remote host potentially running Parrot Security Linux.", 206 | "description": "API GeneratedFindingAPIName was used to access S3 Bucket GeneratedFindingS3Bucket from a remote host with IP address 198.51.100.0 that is potentially running the Parrot Security Linux penetration testing tool." 207 | } 208 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automated GuardDuty Security Response 2 | 3 | ## 🚨🚨🚨 DISCLAIMER 🚨🚨🚨 4 | 5 | This project, when deployed in an AWS account, will break your application if [Amazon GuardDuty](https://aws.amazon.com/guardduty/) detects activity related to running EC2 instances, IAM credentials or S3 buckets. This is by design. Using Amazon GuardDuty, this project will monitor for malicious activity occuring in your account and automatically respond by doing the following: 6 | 7 | - If Amazon GuardDuty detects malicious activity on publicly readable S3 buckets, this project will [block public access to S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html) 8 | - If Amazon GuardDuty detects malicious activity on IAM principles, this project will [revoke any active sessions](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_revoke-sessions.html). 9 | - If Amazon GuardDuty detects malicious activity on EC2 instances, this project will isolate and quarantine the instance (blocking all traffic to the instance) 10 | 11 | Amazon GuardDuty is a regional service, so this project will only monitor resources in the AWS region in which it is deployed. 12 | 13 | This project is intended to reduce the blast radius caused by a security event by isolating and quarantining instances as soon as they are detected. No resources are destroyed, so if the event is deemed a false positive, service can be restored. 14 | 15 | ### Table of contents 16 | 17 | 1. [Introduction](#introduction) 18 | 2. [Architecture](#architecture) 19 | 3. [Prerequisites](#prerequisites) 20 | 4. [Tools and services](#tools-and-services) 21 | 5. [Usage](#usage) 22 | 6. [Clean up](#clean-up) 23 | 7. [Reference](#reference) 24 | 8. [Contributing](#contributing) 25 | 9. [License](#license) 26 | 27 | ## Introduction 28 | 29 | This project will set up an automated response workflow for [Amazon GuardDuty](https://aws.amazon.com/guardduty/) findings. Currently, [EC2 finding types](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-ec2.html), a subset of [S3 finding types](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-s3.html) and [IAM finding types](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-iam.html) targeting an `IAMUser` or `AssumedRole` are supported. 30 | 31 | #### EC2 Finding Types 32 | 33 | When an EC2 finding is detected, [AWS Step Functions](https://aws.amazon.com/step-functions/) is used to execute an [AWS Lambda](https://aws.amazon.com/lambda/) function to gather information and quarantine the EC2 instance: 34 | 35 | 1. Grabs a screenshot from the instance and uploads it to S3 36 | 2. Captures metadata about the instance and uploads it to S3 37 | 3. Enables termination protection on the instance 38 | 4. Ensure Instance Shutdown Behavior is set to “Stop” 39 | 5. Disable the “DeleteOnTermination” setting for All Attached Volumes 40 | 6. Tag the instance 41 | 7. Creates a snapshot of any attached EBS volumes 42 | 8. Acquire Instance memory (write directly to S3, if possible) [*NOTE*: Not yet supported] 43 | 9. Removes any existing IAM instance profiles 44 | 10. Attaches a new IAM instance profile with [AWS Systems Manager Session Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html) (SSM) access 45 | 11. Execute data gathering commands on the instance and upload results to S3 via SSM 46 | 12. Detach the instance from EC2 autoscaling groups (if applicable) 47 | 13. Deregister Instance from Load Balancers (if applicable) 48 | 14. For each [Elastic Network Interface](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html) (ENI), create a new isolated [security group](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html) in the ENI's VPC and update the existing ENI's to use new security groups 49 | 50 | #### S3 Finding Types 51 | 52 | When an S3 finding is detected, if the effective permissions of the bucket are `PUBLIC` (we are assuming that all buckets should be private in this environment), [AWS Step Functions](https://aws.amazon.com/step-functions/) will call the S3 [PutPublicAccessBlock](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutPublicAccessBlock.html) API to make the bucket private. 53 | 54 | #### IAM Finding Types 55 | 56 | When an IAM finding is detected, if the identity type is `IAMUser`, Step Functions attaches a policy named `AWSRevokeOlderSessions` to the IAM user to revoke any active sessions. If the identity type is `AssumedRole`, Step Functions attaches a policy named [AWSRevokeOlderSessions](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_revoke-sessions.html) to the IAM role to revoke any active sessions. 57 | 58 | ## Architecture 59 | 60 | ![architecture](doc/architecture.png) 61 | 62 | ## Prerequisites 63 | 64 | - [Python 3](https://www.python.org/downloads/), installed 65 | - [AWS Command Line Interface (AWS CLI)](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) version 2, installed 66 | - [AWS Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-getting-started.html), installed 67 | - [Docker Desktop](https://www.docker.com/products/docker-desktop), installed 68 | 69 | ## Tools and services 70 | 71 | - [AWS Lambda](https://aws.amazon.com/lambda/) - AWS Lambda is a serverless compute service that lets you run code without provisioning or managing servers, creating workload-aware cluster scaling logic, maintaining event integrations, or managing runtimes. 72 | - [Amazon GuardDuty](https://aws.amazon.com/guardduty/) - Amazon GuardDuty is a threat detection service that continuously monitors your AWS accounts and workloads for malicious activity and delivers detailed security findings for visibility and remediation. 73 | - [AWS Step Functions](https://aws.amazon.com/step-functions/) - AWS Step Functions is a low-code, visual workflow service that developers use to build distributed applications, automate IT and business processes, and build data and machine learning pipelines using AWS services. 74 | - [Amazon EventBridge](https://aws.amazon.com/eventbridge/) - Amazon EventBridge is a serverless event bus that makes it easier to build event-driven applications at scale using events generated from your applications, integrated Software-as-a-Service (SaaS) applications, and AWS services. 75 | - [AWS Systems Manager Session Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html) - Session Manager provides secure and auditable node management without the need to open inbound ports, maintain bastion hosts, or manage SSH keys. 76 | 77 | ## Usage 78 | 79 | #### Parameters 80 | 81 | | Parameter | Type | Default | Description | 82 | | ---------- | :----: | :----------------------------------------: | ------------------------------- | 83 | | GitHubOrg | String | aws-samples | Source code GitHub organization | 84 | | GitHubRepo | String | amazon-guardduty-automated-response-sample | Source code GitHub repository | 85 | 86 | #### Installation 87 | 88 | The CloudFormation stack must be deployed in the same AWS account and region where a GuardDuty detector has been configured and your EC2 instances are running. 89 | 90 | ``` 91 | git clone https://github.com/aws-samples/amazon-guardduty-automated-response-sample 92 | cd amazon-guardduty-automated-response-sample 93 | sam build 94 | sam deploy \ 95 | --guided \ 96 | --tags "GITHUB_ORG=aws-samples GITHUB_REPO=amazon-guardduty-automated-response-sample" 97 | ``` 98 | 99 | ## Clean up 100 | 101 | Deleting the CloudFormation Stack will remove the Lambda functions, state machine and EventBridge rules. 102 | 103 | ``` 104 | sam delete 105 | ``` 106 | 107 | ## Reference 108 | 109 | This solution is inspired by these references: 110 | 111 | - [Startup Security: Techniques to Stay Secure while Building Quickly](https://catalog.workshops.aws/startup-security-stay-secure-while-building-quickly/en-US) (workshop) 112 | - [AWS Security Hub Automated Response and Remediation](https://aws.amazon.com/solutions/implementations/aws-security-hub-automated-response-and-remediation/) (AWS SHARR) 113 | - [How to automate incident response in the AWS Cloud for EC2 instances](https://aws.amazon.com/blogs/security/how-to-automate-incident-response-in-aws-cloud-for-ec2-instances/) 114 | - [Automated security orchestrator with AWS Step Functions](https://github.com/aws-samples/automating-a-security-incident-with-step-functions) 115 | - [Auto Cloud Digital Forensics Incident Response (DFIR)](https://github.com/aws-samples/auto-cloud-digital-forensics-incident-response) 116 | - [AWS Incident Response Playbook Samples](https://github.com/aws-samples/aws-incident-response-playbooks) 117 | 118 | ## Contributing 119 | 120 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 121 | 122 | ## License 123 | 124 | This library is licensed under the MIT-0 License. See the [LICENSE](LICENSE) file. 125 | -------------------------------------------------------------------------------- /src/quarantine/resources/ec2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | import time 23 | import os 24 | from typing import Dict, Any, List, Optional 25 | 26 | from aws_lambda_powertools import Logger 27 | import boto3 28 | import botocore 29 | 30 | from quarantine.constants import BOTO3_CONFIG 31 | 32 | EC2_INSTANCE_PROFILE_ARN = os.environ["EC2_INSTANCE_PROFILE_ARN"] 33 | 34 | logger = Logger(child=True) 35 | 36 | __all__ = ["EC2"] 37 | 38 | 39 | class EC2: 40 | def __init__(self, session: boto3.Session) -> None: 41 | self.client = session.client("ec2", config=BOTO3_CONFIG) 42 | 43 | def get_console_screenshot(self, instance_id: str) -> str: 44 | """ 45 | Get console screenshot 46 | """ 47 | 48 | logger.info(f"Getting EC2 console screenshot from {instance_id}") 49 | try: 50 | response = self.client.get_console_screenshot(InstanceId=instance_id, WakeUp=True) 51 | logger.debug(f"Got EC2 console screenshot from {instance_id}") 52 | except botocore.exceptions.ClientError: 53 | logger.exception(f"Failed to get EC2 console screenshot from {instance_id}") 54 | raise 55 | 56 | return response["ImageData"] 57 | 58 | def describe_instances(self, instance_id: str) -> Dict[str, Any]: 59 | """ 60 | Describe instances 61 | """ 62 | 63 | logger.info(f"Describing instance {instance_id}") 64 | try: 65 | response = self.client.describe_instances(InstanceIds=[instance_id]) 66 | logger.debug(f"Described instance {instance_id}") 67 | except botocore.exceptions.ClientError: 68 | logger.exception(f"Failed to describe instance {instance_id}") 69 | raise 70 | 71 | return response["Reservations"][0]["Instances"][0] 72 | 73 | def describe_security_groups(self, instance_id: str, vpc_id: str) -> Dict[str, Any]: 74 | """ 75 | Describe security groups 76 | """ 77 | 78 | params = { 79 | "Filters": [ 80 | {"Name": "tag:SOC-Status", "Values": ["quarantined"]}, 81 | {"Name": "tag:SOC-InstanceId", "Values": [instance_id]}, 82 | {"Name": "vpc-id", "Values": [vpc_id]}, 83 | ] 84 | } 85 | 86 | logger.info(f"Describing security groups in VPC {vpc_id} for instance {instance_id}") 87 | try: 88 | response = self.client.describe_security_groups(**params) 89 | logger.debug(f"Described security groups in VPC {vpc_id} for instance {instance_id}") 90 | except botocore.exceptions.ClientError: 91 | logger.exception( 92 | f"Failed to describe security groups in {vpc_id} for instance {instance_id}" 93 | ) 94 | raise 95 | 96 | return response.get("SecurityGroups", []) 97 | 98 | def create_snapshot(self, instance_id: str, volume_id: str) -> None: 99 | """ 100 | Create an EBS snapshot 101 | """ 102 | 103 | description = f"Security Response automated copy of {volume_id} for instance {instance_id}" 104 | 105 | logger.info(f"Creating snapshot of volume {volume_id}") 106 | try: 107 | self.client.create_snapshot(VolumeId=volume_id, Description=description) 108 | logger.debug(f"Created snapshot of volume {volume_id}") 109 | except botocore.exceptions.ClientError: 110 | logger.exception(f"Failed to create snapshot of volume {volume_id}") 111 | 112 | def enable_termination_protection(self, instance_id: str) -> None: 113 | """ 114 | Enable instance termination protection 115 | """ 116 | 117 | logger.info(f"Enabling termination protection on {instance_id}") 118 | try: 119 | self.client.modify_instance_attribute( 120 | InstanceId=instance_id, DisableApiTermination={"Value": True} 121 | ) 122 | self.client.modify_instance_attribute( 123 | InstanceId=instance_id, DisableApiStop={"Value": True} 124 | ) 125 | logger.debug(f"Enabled termination protection on {instance_id}") 126 | except botocore.exceptions.ClientError: 127 | logger.exception(f"Failed to enable termination protection on {instance_id}") 128 | raise 129 | 130 | def enable_volume_termination_protection( 131 | self, instance_id: str, block_device_names: Optional[List[str]] = None 132 | ) -> None: 133 | """ 134 | Enable EBS volume termination protection 135 | """ 136 | 137 | if not block_device_names: 138 | return 139 | 140 | block_device_mappings = [ 141 | {"DeviceName": device_name, "Ebs": {"DeleteOnTermination": False}} 142 | for device_name in block_device_names 143 | ] 144 | 145 | logger.info(f"Enabling volume termination protection on {instance_id}") 146 | try: 147 | self.client.modify_instance_attribute( 148 | InstanceId=instance_id, BlockDeviceMappings=block_device_mappings 149 | ) 150 | logger.debug(f"Enabled volume termination protection on {instance_id}") 151 | except botocore.exceptions.ClientError: 152 | logger.exception(f"Failed to enable volume termination protection on {instance_id}") 153 | raise 154 | 155 | def shutdown_behavior_stop(self, instance_id: str) -> None: 156 | """ 157 | Set instance shutdown behavior to stop 158 | """ 159 | 160 | logger.info(f"Modifying instance shutdown behavior to 'stop' on {instance_id}") 161 | try: 162 | self.client.modify_instance_attribute( 163 | InstanceId=instance_id, 164 | InstanceInitiatedShutdownBehavior={"Value": "stop"}, 165 | ) 166 | logger.debug(f"Modified instance shutdown behavior to 'stop' on {instance_id}") 167 | except botocore.exceptions.ClientError: 168 | logger.exception( 169 | f"Failed to modify instance shutdown behavior to 'stop' on {instance_id}" 170 | ) 171 | raise 172 | 173 | def remove_ec2_instance_profile(self, instance_id: str) -> None: 174 | """ 175 | Remove any EC2 instance profile attached to an instance 176 | """ 177 | 178 | params = { 179 | "Filters": [ 180 | {"Name": "instance-id", "Values": [instance_id]}, 181 | {"Name": "state", "Values": ["associating", "associated"]}, 182 | ] 183 | } 184 | 185 | logger.debug("Describing IAM instance profile associations on {instance_id}") 186 | try: 187 | response = self.client.describe_iam_instance_profile_associations(**params) 188 | logger.debug(f"Described IAM instance profile associations on {instance_id}") 189 | except botocore.exceptions.ClientError: 190 | logger.exception( 191 | f"Failed to describe IAM instance profile associations on {instance_id}" 192 | ) 193 | raise 194 | 195 | associations = response.get("IamInstanceProfileAssociations", []) 196 | if not associations: 197 | logger.debug(f"No IAM instance profiles attached to {instance_id}") 198 | return 199 | 200 | for association in associations: 201 | profile_arn = association["IamInstanceProfile"]["Arn"] 202 | logger.info(f"Disassociating IAM instance profile {profile_arn} from {instance_id}") 203 | try: 204 | self.client.disassociate_iam_instance_profile( 205 | AssociationId=association["AssociationId"] 206 | ) 207 | logger.debug(f"Disassociated IAM instance profile {profile_arn} from {instance_id}") 208 | except botocore.exceptions.ClientError: 209 | logger.exception( 210 | f"Failed to disassociate IAM instance profile {profile_arn} from {instance_id}" 211 | ) 212 | 213 | def attach_ec2_instance_profile(self, instance_id: str, profile_arn: str) -> None: 214 | """ 215 | Attach an IAM Instance Profile to an EC2 instance 216 | """ 217 | logger.info(f"Associating IAM instance profile {profile_arn} to {instance_id}") 218 | try: 219 | self.client.associate_iam_instance_profile( 220 | IamInstanceProfile={"Arn": profile_arn}, 221 | InstanceId=instance_id, 222 | ) 223 | logger.debug(f"Associated IAM instance profile {profile_arn} to {instance_id}") 224 | except botocore.exceptions.ClientError: 225 | logger.exception( 226 | f"Failed to associated IAM instance profile {profile_arn} to {instance_id}" 227 | ) 228 | 229 | def create_security_group(self, instance_id: str, vpc_id: str, tags=None) -> str: 230 | """ 231 | Create a security group with no access 232 | """ 233 | 234 | now = int(time.time()) 235 | 236 | params = { 237 | "GroupName": f"quarantine-{instance_id}-{now}", 238 | "Description": f"Quarantine group for {instance_id}", 239 | "VpcId": vpc_id, 240 | } 241 | 242 | if tags: 243 | params["TagSpecifications"] = [ 244 | { 245 | "ResourceType": "security-group", 246 | "Tags": tags, 247 | } 248 | ] 249 | 250 | logger.info(f"Creating new isolation security group for {instance_id}") 251 | try: 252 | response = self.client.create_security_group(**params) 253 | logger.debug(f"Created isolation security group for {instance_id}") 254 | except botocore.exceptions.ClientError: 255 | logger.exception(f"Failed to create isolation security group for {instance_id}") 256 | raise 257 | 258 | group_id = response["GroupId"] 259 | self.client.revoke_security_group_egress( 260 | GroupId=group_id, 261 | IpPermissions=[ 262 | { 263 | "IpProtocol": "-1", 264 | "IpRanges": [ 265 | {"CidrIp": "0.0.0.0/0"}, 266 | ], 267 | }, 268 | ], 269 | ) 270 | return group_id 271 | 272 | def describe_network_interfaces(self, instance_id: str) -> List[Dict[str, Any]]: 273 | """ 274 | Update the security groups for an instance 275 | """ 276 | 277 | logger.info(f"Describing network interfaces on {instance_id}") 278 | try: 279 | response = self.client.describe_network_interfaces( 280 | Filters=[ 281 | { 282 | "Name": "attachment.instance-id", 283 | "Values": [instance_id], 284 | }, 285 | { 286 | "Name": "attachment.status", 287 | "Values": ["attaching", "attached"], 288 | }, 289 | ] 290 | ) 291 | logger.debug(f"Described network interfaces on {instance_id}") 292 | except botocore.exceptions.ClientError: 293 | logger.exception(f"Failed to describe network interfaces on {instance_id}") 294 | 295 | return response.get("NetworkInterfaces", []) 296 | 297 | def update_security_groups(self, network_interface_id: str, group_id: str) -> None: 298 | """ 299 | Update the security groups on a network interface 300 | """ 301 | 302 | logger.info(f"Updating security groups on {network_interface_id} to {group_id}") 303 | try: 304 | self.client.modify_network_interface_attribute( 305 | NetworkInterfaceId=network_interface_id, Groups=[group_id] 306 | ) 307 | logger.debug(f"Updated security groups on {network_interface_id} to {group_id}") 308 | except botocore.exceptions.ClientError: 309 | logger.exception(f"Failed to update security groups on {network_interface_id}") 310 | 311 | def create_tags(self, instance_id: str, tags=None) -> None: 312 | """ 313 | Create new tags on an EC2 instance 314 | """ 315 | 316 | if not tags: 317 | return 318 | 319 | params = { 320 | "Resources": [instance_id], 321 | "Tags": tags, 322 | } 323 | 324 | logger.info(f"Creating tags on {instance_id}") 325 | try: 326 | self.client.create_tags(**params) 327 | logger.debug(f"Created tags on {instance_id}") 328 | except botocore.exceptions.ClientError: 329 | logger.exception(f"Failed to create tags on {instance_id}") 330 | -------------------------------------------------------------------------------- /template.yml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | --- 5 | AWSTemplateFormatVersion: "2010-09-09" 6 | Transform: "AWS::Serverless-2016-10-31" 7 | Description: Automated GuardDuty Security Response 8 | 9 | Parameters: 10 | GitHubOrg: 11 | Type: String 12 | Description: Source Code GitHub Organization 13 | Default: "aws-samples" 14 | GitHubRepo: 15 | Type: String 16 | Description: Source Code GitHub Repository 17 | Default: "amazon-guardduty-automated-response-sample" 18 | 19 | Globals: 20 | Function: 21 | CodeUri: src/ 22 | Environment: 23 | Variables: 24 | POWERTOOLS_METRICS_NAMESPACE: SecurityOperations 25 | LOG_LEVEL: INFO 26 | Layers: 27 | - !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:57" 28 | MemorySize: 256 # megabytes 29 | Runtime: python3.12 30 | Tags: 31 | GITHUB_ORG: !Ref GitHubOrg 32 | GITHUB_REPO: !Ref GitHubRepo 33 | Timeout: 120 # seconds 34 | 35 | Resources: 36 | EncryptionKey: 37 | Type: "AWS::KMS::Key" 38 | UpdateReplacePolicy: Delete 39 | DeletionPolicy: Delete 40 | Properties: 41 | Description: Encryption key for security operations resources 42 | EnableKeyRotation: true 43 | KeyPolicy: 44 | Version: "2012-10-17" 45 | Statement: 46 | - Sid: "Enable IAM policies" 47 | Effect: Allow 48 | Principal: 49 | AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" 50 | Action: "kms:*" 51 | Resource: "*" 52 | - Sid: "Allow use of the key" 53 | Effect: Allow 54 | Principal: 55 | AWS: 56 | - !GetAtt QuarantineFunctionRole.Arn 57 | - !GetAtt QuarantineInstanceRole.Arn 58 | - !GetAtt StateMachineRole.Arn 59 | Action: 60 | - "kms:Encrypt" 61 | - "kms:Decrypt" 62 | - "kms:ReEncrypt*" 63 | - "kms:GenerateDataKey*" 64 | - "kms:DescribeKey" 65 | Resource: "*" 66 | - Sid: "Allow use of the key by CloudWatch Logs" 67 | Effect: Allow 68 | Principal: 69 | Service: !Sub "logs.${AWS::Region}.${AWS::URLSuffix}" 70 | Action: 71 | - "kms:Encrypt*" 72 | - "kms:Decrypt*" 73 | - "kms:ReEncrypt*" 74 | - "kms:GenerateDataKey*" 75 | - "kms:Describe*" 76 | Resource: "*" 77 | Condition: 78 | ArnLike: 79 | "kms:EncryptionContext:aws:logs:arn": !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*" 80 | Tags: 81 | - Key: GITHUB_ORG 82 | Value: !Ref GitHubOrg 83 | - Key: GITHUB_REPO 84 | Value: !Ref GitHubRepo 85 | - Key: "aws-cloudformation:stack-name" 86 | Value: !Ref "AWS::StackName" 87 | - Key: "aws-cloudformation:stack-id" 88 | Value: !Ref "AWS::StackId" 89 | - Key: "aws-cloudformation:logical-id" 90 | Value: EncryptionKey 91 | 92 | EncryptionAlias: 93 | Type: "AWS::KMS::Alias" 94 | Properties: 95 | AliasName: "alias/sec_ops" 96 | TargetKeyId: !Ref EncryptionKey 97 | 98 | NotificationTopic: 99 | Type: "AWS::SNS::Topic" 100 | Properties: 101 | DisplayName: Security Operations Notifications 102 | KmsMasterKeyId: !Ref EncryptionKey 103 | Tags: 104 | - Key: GITHUB_ORG 105 | Value: !Ref GitHubOrg 106 | - Key: GITHUB_REPO 107 | Value: !Ref GitHubRepo 108 | 109 | ArtifactBucket: 110 | Type: "AWS::S3::Bucket" 111 | Metadata: 112 | cfn_nag: 113 | rules_to_suppress: 114 | - id: W35 115 | reason: "Ignoring access logs" 116 | UpdateReplacePolicy: Retain 117 | DeletionPolicy: Retain 118 | Properties: 119 | BucketEncryption: 120 | ServerSideEncryptionConfiguration: 121 | - BucketKeyEnabled: true 122 | ServerSideEncryptionByDefault: 123 | KMSMasterKeyID: !Ref EncryptionKey 124 | SSEAlgorithm: "aws:kms" 125 | LifecycleConfiguration: 126 | Rules: 127 | - Id: TransitionRule 128 | NoncurrentVersionTransitions: 129 | - StorageClass: INTELLIGENT_TIERING 130 | TransitionInDays: 0 131 | Status: Enabled 132 | Transitions: 133 | - TransitionInDays: 0 134 | StorageClass: INTELLIGENT_TIERING 135 | OwnershipControls: 136 | Rules: 137 | - ObjectOwnership: BucketOwnerEnforced 138 | PublicAccessBlockConfiguration: 139 | BlockPublicAcls: true 140 | BlockPublicPolicy: true 141 | IgnorePublicAcls: true 142 | RestrictPublicBuckets: true 143 | Tags: 144 | - Key: GITHUB_ORG 145 | Value: !Ref GitHubOrg 146 | - Key: GITHUB_REPO 147 | Value: !Ref GitHubRepo 148 | VersioningConfiguration: 149 | Status: Enabled 150 | 151 | ArtifactBucketPolicy: 152 | Type: "AWS::S3::BucketPolicy" 153 | Properties: 154 | Bucket: !Ref ArtifactBucket 155 | PolicyDocument: 156 | Statement: 157 | - Sid: DenyInsecureConnections 158 | Effect: Deny 159 | Principal: "*" 160 | Action: "s3:*" 161 | Resource: 162 | - !GetAtt ArtifactBucket.Arn 163 | - !Sub "${ArtifactBucket.Arn}/*" 164 | Condition: 165 | Bool: 166 | "aws:SecureTransport": false 167 | 168 | QuarantineFunctionLogGroup: 169 | Type: "AWS::Logs::LogGroup" 170 | UpdateReplacePolicy: Delete 171 | DeletionPolicy: Delete 172 | Properties: 173 | KmsKeyId: !GetAtt EncryptionKey.Arn 174 | LogGroupName: !Sub "/aws/lambda/${QuarantineFunction}" 175 | RetentionInDays: 3 176 | Tags: 177 | - Key: GITHUB_ORG 178 | Value: !Ref GitHubOrg 179 | - Key: GITHUB_REPO 180 | Value: !Ref GitHubRepo 181 | - Key: "aws-cloudformation:stack-name" 182 | Value: !Ref "AWS::StackName" 183 | - Key: "aws-cloudformation:stack-id" 184 | Value: !Ref "AWS::StackId" 185 | - Key: "aws-cloudformation:logical-id" 186 | Value: QuarantineFunctionLogGroup 187 | 188 | QuarantineInstanceRole: 189 | Type: "AWS::IAM::Role" 190 | Properties: 191 | AssumeRolePolicyDocument: 192 | Version: "2012-10-17" 193 | Statement: 194 | Effect: Allow 195 | Principal: 196 | Service: !Sub "ec2.${AWS::URLSuffix}" 197 | Action: "sts:AssumeRole" 198 | Condition: 199 | StringEquals: 200 | "aws:SourceAccount": !Ref "AWS::AccountId" 201 | Description: !Sub "DO NOT DELETE - Used by EC2. Created by CloudFormation ${AWS::StackId}" 202 | ManagedPolicyArns: 203 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore" 204 | Tags: 205 | - Key: "aws-cloudformation:stack-name" 206 | Value: !Ref "AWS::StackName" 207 | - Key: "aws-cloudformation:stack-id" 208 | Value: !Ref "AWS::StackId" 209 | - Key: "aws-cloudformation:logical-id" 210 | Value: QuarantineInstanceRole 211 | - Key: GITHUB_ORG 212 | Value: !Ref GitHubOrg 213 | - Key: GITHUB_REPO 214 | Value: !Ref GitHubRepo 215 | 216 | QuarantineInstancePolicy: 217 | Type: "AWS::IAM::Policy" 218 | Properties: 219 | PolicyName: QuarantineInstancePolicy 220 | PolicyDocument: 221 | Version: "2012-10-17" 222 | Statement: 223 | - Effect: Allow 224 | Action: "s3:PutObject" 225 | Resource: !Sub "${ArtifactBucket.Arn}/*" 226 | Roles: 227 | - !Ref QuarantineInstanceRole 228 | 229 | QuarantineInstanceRoleProfile: 230 | Type: "AWS::IAM::InstanceProfile" 231 | Properties: 232 | Roles: 233 | - Ref: QuarantineInstanceRole 234 | 235 | QuarantineFunctionRole: 236 | Type: "AWS::IAM::Role" 237 | Properties: 238 | AssumeRolePolicyDocument: 239 | Version: "2012-10-17" 240 | Statement: 241 | Effect: Allow 242 | Principal: 243 | Service: !Sub "lambda.${AWS::URLSuffix}" 244 | Action: "sts:AssumeRole" 245 | Description: !Sub "DO NOT DELETE - Used by Lambda. Created by CloudFormation ${AWS::StackId}" 246 | Tags: 247 | - Key: "aws-cloudformation:stack-name" 248 | Value: !Ref "AWS::StackName" 249 | - Key: "aws-cloudformation:stack-id" 250 | Value: !Ref "AWS::StackId" 251 | - Key: "aws-cloudformation:logical-id" 252 | Value: QuarantineFunctionRole 253 | - Key: GITHUB_ORG 254 | Value: !Ref GitHubOrg 255 | - Key: GITHUB_REPO 256 | Value: !Ref GitHubRepo 257 | 258 | QuarantineFunctionPolicy: 259 | Type: "AWS::IAM::Policy" 260 | Metadata: 261 | cfn_nag: 262 | rules_to_suppress: 263 | - id: W12 264 | reason: "Needs permission to modify any resource" 265 | - id: W76 266 | reason: "Needs broad permissions for quarantining" 267 | Properties: 268 | PolicyName: QuarantineFunctionPolicy 269 | PolicyDocument: 270 | Version: "2012-10-17" 271 | Statement: 272 | - Effect: Allow 273 | Action: "iam:PassRole" 274 | Resource: 275 | - !GetAtt QuarantineInstanceRole.Arn 276 | - !GetAtt SSMPublishRole.Arn 277 | - Effect: Allow 278 | Action: "s3:PutObject" 279 | Resource: !Sub "${ArtifactBucket.Arn}/*" 280 | Condition: 281 | ArnEquals: 282 | "lambda:SourceFunctionArn": !GetAtt QuarantineFunction.Arn 283 | - Effect: Allow 284 | Action: 285 | - "logs:CreateLogStream" 286 | - "logs:PutLogEvents" 287 | Resource: !GetAtt QuarantineFunctionLogGroup.Arn 288 | - Effect: Allow 289 | Action: "sns:Publish" 290 | Resource: !Ref NotificationTopic 291 | - Effect: Allow 292 | Action: 293 | - "autoscaling:DescribeAutoScalingInstances" 294 | - "ec2:DescribeInstances" 295 | - "ec2:GetConsoleScreenshot" 296 | - "ec2:DescribeIamInstanceProfileAssociations" 297 | - "ec2:DescribeNetworkInterfaces" 298 | - "ec2:DescribeSecurityGroups" 299 | - "ec2:CreateSecurityGroup" 300 | - "ec2:CreateSnapshot" 301 | - "ec2:CreateTags" 302 | - "ec2:ModifyInstanceAttribute" 303 | - "ec2:ModifyNetworkInterfaceAttribute" 304 | - "ec2:RevokeSecurityGroupEgress" 305 | - "elasticloadbalancing:DescribeLoadBalancers" 306 | - "elasticloadbalancing:DescribeInstanceHealth" 307 | - "elasticloadbalancing:DescribeTargetGroups" 308 | - "elasticloadbalancing:DescribeTargetHealth" 309 | - "ssm:DescribeInstanceInformation" 310 | - "ssm:ListCommands" 311 | - "ssm:GetCommandInvocation" 312 | - "ssm:SendCommand" 313 | Resource: "*" 314 | - Effect: Allow 315 | Action: "autoscaling:DetachInstances" 316 | Resource: !Sub "arn:${AWS::Partition}:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*" 317 | - Effect: Allow 318 | Action: "elasticloadbalancing:DeregisterInstancesFromLoadBalancer" 319 | Resource: !Sub "arn:${AWS::Partition}:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:loadbalancer/*" 320 | - Effect: Allow 321 | Action: "elasticloadbalancing:DeregisterTargets" 322 | Resource: !Sub "arn:${AWS::Partition}:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:targetgroup/*" 323 | - Effect: Allow 324 | Action: 325 | - "ec2:AssociateIamInstanceProfile" 326 | - "ec2:DisassociateIamInstanceProfile" 327 | Resource: !Sub "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/*" 328 | Roles: 329 | - !Ref QuarantineFunctionRole 330 | 331 | QuarantineFunction: 332 | Type: "AWS::Serverless::Function" 333 | Metadata: 334 | cfn_nag: 335 | rules_to_suppress: 336 | - id: W58 337 | reason: "Function has permission to write to CloudWatch Logs" 338 | - id: W89 339 | reason: "Function does not need VPC resources" 340 | Properties: 341 | Description: DO NOT DELETE - Security Operations - Quarantine Function 342 | Environment: 343 | Variables: 344 | ARTIFACT_BUCKET: !Ref ArtifactBucket 345 | NOTIFICATION_TOPIC_ARN: !Ref NotificationTopic 346 | EC2_INSTANCE_PROFILE_ARN: !GetAtt QuarantineInstanceRoleProfile.Arn 347 | AWS_ACCOUNT_ID: !Ref "AWS::AccountId" 348 | SSM_ROLE_ARN: !GetAtt SSMPublishRole.Arn 349 | Handler: quarantine.lambda_handler.handler 350 | ReservedConcurrentExecutions: 10 351 | Role: !GetAtt QuarantineFunctionRole.Arn 352 | 353 | SSMPublishRole: 354 | Type: "AWS::IAM::Role" 355 | Properties: 356 | AssumeRolePolicyDocument: 357 | Statement: 358 | - Effect: Allow 359 | Principal: 360 | Service: !Sub "ssm.${AWS::URLSuffix}" 361 | Action: "sts:AssumeRole" 362 | Description: !Sub "DO NOT DELETE - Used by SSM. Created by CloudFormation ${AWS::StackId}" 363 | Policies: 364 | - PolicyName: SSMPublishPolicy 365 | PolicyDocument: 366 | Version: "2012-10-17" 367 | Statement: 368 | - Effect: Allow 369 | Action: "sns:Publish" 370 | Resource: !Ref NotificationTopic 371 | Tags: 372 | - Key: "aws-cloudformation:stack-name" 373 | Value: !Ref "AWS::StackName" 374 | - Key: "aws-cloudformation:stack-id" 375 | Value: !Ref "AWS::StackId" 376 | - Key: "aws-cloudformation:logical-id" 377 | Value: SSMPublishRole 378 | - Key: GITHUB_ORG 379 | Value: !Ref GitHubOrg 380 | - Key: GITHUB_REPO 381 | Value: !Ref GitHubRepo 382 | 383 | EventBridgeRole: 384 | Type: "AWS::IAM::Role" 385 | Properties: 386 | AssumeRolePolicyDocument: 387 | Statement: 388 | - Effect: Allow 389 | Principal: 390 | Service: !Sub "events.${AWS::URLSuffix}" 391 | Action: "sts:AssumeRole" 392 | Condition: 393 | StringEquals: 394 | "aws:SourceAccount": !Ref "AWS::AccountId" 395 | Description: !Sub "DO NOT DELETE - Used by EventBridge. Created by CloudFormation ${AWS::StackId}" 396 | Policies: 397 | - PolicyName: EventBridgePolicy 398 | PolicyDocument: 399 | Version: "2012-10-17" 400 | Statement: 401 | - Effect: Allow 402 | Action: "states:StartExecution" 403 | Resource: !Ref StateMachine 404 | Tags: 405 | - Key: "aws-cloudformation:stack-name" 406 | Value: !Ref "AWS::StackName" 407 | - Key: "aws-cloudformation:stack-id" 408 | Value: !Ref "AWS::StackId" 409 | - Key: "aws-cloudformation:logical-id" 410 | Value: EventBridgeRole 411 | - Key: GITHUB_ORG 412 | Value: !Ref GitHubOrg 413 | - Key: GITHUB_REPO 414 | Value: !Ref GitHubRepo 415 | 416 | GuardDutyRemediationRule: 417 | Type: "AWS::Events::Rule" 418 | Properties: 419 | Description: GuardDuty Remediation Rule 420 | EventPattern: 421 | source: 422 | - aws.guardduty 423 | detail-type: 424 | - GuardDuty Finding 425 | State: ENABLED 426 | Targets: 427 | - Arn: !Ref StateMachine 428 | Id: stepfunction-remediation 429 | InputPath: "$.detail" 430 | RoleArn: !GetAtt EventBridgeRole.Arn 431 | 432 | StateMachineRole: 433 | Type: "AWS::IAM::Role" 434 | Properties: 435 | AssumeRolePolicyDocument: 436 | Statement: 437 | - Effect: Allow 438 | Principal: 439 | Service: !Sub "states.${AWS::URLSuffix}" 440 | Action: "sts:AssumeRole" 441 | Condition: 442 | StringEquals: 443 | "aws:SourceAccount": !Ref "AWS::AccountId" 444 | Description: !Sub "DO NOT DELETE - Used by Step Functions. Created by CloudFormation ${AWS::StackId}" 445 | Tags: 446 | - Key: "aws-cloudformation:stack-name" 447 | Value: !Ref "AWS::StackName" 448 | - Key: "aws-cloudformation:stack-id" 449 | Value: !Ref "AWS::StackId" 450 | - Key: "aws-cloudformation:logical-id" 451 | Value: StateMachineRole 452 | - Key: GITHUB_ORG 453 | Value: !Ref GitHubOrg 454 | - Key: GITHUB_REPO 455 | Value: !Ref GitHubRepo 456 | 457 | StateMachinePolicy: 458 | Type: "AWS::IAM::Policy" 459 | Properties: 460 | PolicyName: StateMachinePolicy 461 | PolicyDocument: 462 | Version: "2012-10-17" 463 | Statement: 464 | - Effect: Allow 465 | Action: "iam:PutRolePolicy" 466 | Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/*" 467 | - Effect: Allow 468 | Action: "iam:PutUserPolicy" 469 | Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:user/*" 470 | - Effect: Allow 471 | Action: "s3:PutBucketPublicAccessBlock" 472 | Resource: !Sub "arn:${AWS::Partition}:s3:::*" 473 | Condition: 474 | StringEquals: 475 | "s3:ResourceAccount": !Ref "AWS::AccountId" 476 | - Effect: Allow 477 | Action: "lambda:InvokeFunction" 478 | Resource: !GetAtt QuarantineFunction.Arn 479 | - Effect: Allow 480 | Action: "sns:Publish" 481 | Resource: !Ref NotificationTopic 482 | Roles: 483 | - !Ref StateMachineRole 484 | 485 | StateMachine: 486 | Type: "AWS::StepFunctions::StateMachine" 487 | Properties: 488 | Definition: 489 | StartAt: FindingType 490 | States: 491 | FindingType: 492 | Type: Choice 493 | Choices: 494 | - Variable: "$.resource.resourceType" 495 | StringEquals: AccessKey 496 | Next: IAMFinding 497 | - Variable: "$.resource.resourceType" 498 | StringEquals: Instance 499 | Next: EC2Finding 500 | - Variable: "$.resource.resourceType" 501 | StringEquals: S3Bucket 502 | Next: S3Finding 503 | Default: UnsupportedFinding 504 | IAMFinding: 505 | Type: Pass 506 | Result: 507 | PolicyDocument: |- 508 | \{ 509 | "Version": "2012-10-17", 510 | "Statement": [ 511 | \{ 512 | "Effect": "Deny", 513 | "Action": "*", 514 | "Resource": "*", 515 | "Condition": \{ 516 | "DateLessThan": \{ 517 | "aws:TokenIssueTime": "{}" 518 | \} 519 | \} 520 | \} 521 | ] 522 | \} 523 | ResultPath: "$.Policy" 524 | Next: IAMFindingType 525 | IAMFindingType: 526 | Type: Choice 527 | Choices: 528 | # https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-user-identity.html#cloudtrail-event-reference-user-identity-fields 529 | - Variable: "$.resource.accessKeyDetails.userType" 530 | StringEquals: AssumedRole 531 | Next: IAMPutRolePolicy 532 | - Variable: "$.resource.accessKeyDetails.userType" 533 | StringEquals: IAMUser 534 | Next: IAMPutUserPolicy 535 | Default: UnsupportedIdentityType 536 | IAMPutRolePolicy: 537 | Type: Task 538 | Parameters: 539 | "PolicyDocument.$": States.Format($.Policy.PolicyDocument, $.service.eventLastSeen) 540 | PolicyName: AWSRevokeOlderSessions 541 | "RoleName.$": "$.resource.accessKeyDetails.userName" 542 | Resource: "arn:aws:states:::aws-sdk:iam:putRolePolicy" 543 | ResultPath: "$.putRolePolicy" 544 | Next: IAMFindingPublish 545 | IAMPutUserPolicy: 546 | Type: Task 547 | Parameters: 548 | "PolicyDocument.$": States.Format($.Policy.PolicyDocument, $.service.eventLastSeen) 549 | PolicyName: AWSRevokeOlderSessions 550 | "UserName.$": "$.resource.accessKeyDetails.userName" 551 | Resource: "arn:aws:states:::aws-sdk:iam:putUserPolicy" 552 | ResultPath: "$.putUserPolicy" 553 | Next: IAMFindingPublish 554 | IAMFindingPublish: 555 | Type: Task 556 | Parameters: 557 | "Message.$": States.Format('Successfully revoked older sessions on {}', $.resource.accessKeyDetails.userName) 558 | TopicArn: !Ref NotificationTopic 559 | Resource: "arn:aws:states:::aws-sdk:sns:publish" 560 | End: true 561 | EC2Finding: 562 | Type: Task 563 | Resource: !GetAtt QuarantineFunction.Arn 564 | Retry: 565 | - ErrorEquals: 566 | - Lambda.TooManyRequestsException 567 | - Lambda.ServiceException 568 | - Lambda.AWSLambdaException 569 | - Lambda.SdkClientException 570 | IntervalSeconds: 2 571 | MaxAttempts: 6 572 | BackoffRate: 2 573 | TimeoutSeconds: 120 574 | End: true 575 | S3Finding: 576 | Type: Choice 577 | Choices: 578 | - Variable: "$.resource.s3BucketDetails[0].publicAccess.effectivePermission" 579 | StringEquals: PUBLIC 580 | Next: S3BlockPublicAccess 581 | Default: BucketNotPublic 582 | S3BlockPublicAccess: 583 | Type: Task 584 | Parameters: 585 | "Bucket.$": "$.resource.s3BucketDetails[0].name" 586 | PublicAccessBlockConfiguration: 587 | BlockPublicAcls: true 588 | IgnorePublicAcls: true 589 | BlockPublicPolicy: true 590 | RestrictPublicBuckets: true 591 | ExpectedBucketOwner: !Ref "AWS::AccountId" 592 | Resource: "arn:aws:states:::aws-sdk:s3:putPublicAccessBlock" 593 | ResultPath: "$.putPublicAccessBlock" 594 | Next: S3FindingPublish 595 | S3FindingPublish: 596 | Type: Task 597 | Parameters: 598 | "Message.$": States.Format('Successfully blocked public access on S3 bucket {}', $.resource.s3BucketDetails[0].name) 599 | TopicArn: !Ref NotificationTopic 600 | Resource: "arn:aws:states:::aws-sdk:sns:publish" 601 | End: true 602 | BucketNotPublic: 603 | Type: Succeed 604 | UnsupportedFinding: 605 | Type: Fail 606 | UnsupportedIdentityType: 607 | Type: Fail 608 | RoleArn: !GetAtt StateMachineRole.Arn 609 | 610 | Outputs: 611 | ArtifactBucket: 612 | Description: Security artifact S3 bucket 613 | Value: !GetAtt ArtifactBucket.RegionalDomainName 614 | NotificationTopic: 615 | Description: Security notifications SNS topic 616 | Value: !Ref NotificationTopic 617 | EncryptionKeyAliasArn: 618 | Description: Encryption key alias ARN 619 | Value: !Sub "arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:${EncryptionAlias}" 620 | --------------------------------------------------------------------------------