├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── aws_ir_plugins ├── README.rst ├── __init__.py ├── _version.py ├── disableaccess_key.py ├── examineracl_host.py ├── gather_host.py ├── isolate_host.py ├── revokests_key.py ├── snapshotdisks_host.py ├── stop_host.py ├── tag_host.py └── templates │ └── deny-sts-before-time.json.j2 ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── test_disableaccess.py ├── test_examineracl.py ├── test_gatherhost.py ├── test_isolatehost.py ├── test_revokests_key.py ├── test_snapshotdisks.py ├── test_stophost.py └── test_taghost.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .idea/ 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.6' 4 | before_install: 5 | - if [[ -z $TRAVIS_TAG ]]; then sed -i "s/['\"]\([^\"]*\)['\"]/\"\1b$TRAVIS_BUILD_NUMBER\"/g" 6 | aws_ir_plugins/_version.py; fi 7 | install: 8 | - pip install -r requirements-dev.txt 9 | - pip install flake8 10 | - python -W ignore::UserWarning:distutils.dist setup.py build 11 | script: 12 | - flake8 plugins 13 | - python setup.py test 14 | deploy: 15 | - provider: pypi 16 | distributions: sdist bdist_wheel 17 | user: threatresponse 18 | password: 19 | secure: qYav6prhKwMUZX7ceFA0scfbDy/mobg1KQS3v507GqlYBhLX9y1LNy2j9tfDLPuEQFpirZ2wR2oIQOt6S1gnY8WGeoC242MlIIUCV+i0Rwi+zY7kEQxIeegkc37oqA0Xd3a+F9DlY0sVzS2PkScDj9CkQlAAUwfxvuZqIA6Eezaj325YTlmHOwc5WPsPW0CqrGpCz5Lu2B63Z+vvsH7/XVuO4K8qcTVaO+uosmY8tgVjwjNA/zsb8xvOsSfwgkphMVTPT94HjtgGLqWUqOkmcUh97dAEczf+0cRHNTJHd12kL2WlU1Zy5kP/GyqKpOwN57inOk408IFobGJKMCSdaKVBtsO+V4hs33IGuKHJdd0LwHyJqTwp7CTRwazxnK9InP7qrvMV1HhaupJrS5VtoOY4xw4MHzUpTM8kCaksZ3MSah/o7kIzTSPBR8Xh+ublPLaoTtTFXoH8d58GyLCU3JFvZdEXTi2ccdoKyQ6tI+J3Zi6Hig9xA/vQGij4aQNnPZiR7xOFKbUiunBhwcZ9EpwCSzw/ln/LmpAvQcsScIRaujzX2yJDWUI+91wR3EfYKGu3PrDIXu2ruICNyBY8wbJG7VOTxgzKVG4NunL/ylJWTs5f1lXh8bmomr7GOwNkjm0MjHV338UKukWlXrN+2M9ngBNK3+4TbWQbt6PqsiI= 20 | on: 21 | tags: true 22 | - provider: pypi 23 | server: https://test.pypi.org/legacy/ 24 | distributions: sdist bdist_wheel 25 | user: threatresponse 26 | password: 27 | secure: hvKSisK/Tq/06padXVmrLsomPu7I1AehElxMtveq3IjtDEd01m7J9rXDEiYxzK5nAunMncz3oJL5OC4z/CSvW18KYLisRLbW0+WiNg1O/sQh69Y8fNnJo6NNb8mnrgpDlljqFcelFgTniCBjGY0HyucOsUes6DJWFM0rQrwXmH+Ys+/uInNKbgzN5pBjMzHNRp2Hu4HKO5PI0yxvWcbKZrw1jJNjMBaiAb55KZ7ckjpOaxVWYUpoRaIyUGZAMMUdYdXD87LwC3+H4puvbtiPY1jzzXmE/sPjd5fPuKs0PbG+n7gtiCUDhX/Rg7mjUsK0cvd4EAGlsDhDWU/agh8oPZGRF610or7krhv21ivHRMx/sH8JxW8eFftNiyJbQi1GtrXah/YHQHI2KYpDHbbphMFhXm3aNRKZfoewP9/gAFipTKk8sAJQn90uWYmYqaph/KapgxXXRwBTGF/trTekYoDk45Fh1pddIkHJluzPluqtX9ONL0EqP1bdWo8YPB5/0xQOIuC/8LgtMzGSIN/RTB5ORTA43My+ksDDxYJo3r4xSXXC4fx1B6tZb5lkKh3hMI/p1moWXG5viWMwViF0PxwBYCOwm2LLWI/R4iq9ST0ZJwXtsYZ/UK51QgBBOMJ713L70o97pLimtMlEAmyv8XCA1A33R9mhBd5zhi62v1A= 28 | skip_cleanup: true 29 | on: 30 | all_branches: true 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ThreatResponse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws_ir-plugins 2 | Core supported plugins for the aws_ir-cli 3 | 4 | These files were formerly part of https://github.com/threatresponse/aws_ir 5 | 6 | In preparation for supporting community plugins and pull requests these have been moved 7 | to this repository. 8 | 9 | ## Usage 10 | 11 | ``` 12 | 13 | import aws_ir_plugins 14 | 15 | # Or 16 | 17 | from aws_ir_plugins import disableaccess_key.py 18 | 19 | ``` 20 | 21 | ## Virutal Env 22 | `virtualenv env -p python3` 23 | 24 | ## Tests 25 | `python setup.py test` 26 | -------------------------------------------------------------------------------- /aws_ir_plugins/README.rst: -------------------------------------------------------------------------------- 1 | aws_ir Plugin Support ( Experimental ) 2 | ======================================= 3 | 4 | All plugins need to take the following: 5 | * Client : A boto3 client object with a connection to appropriate region 6 | * Compromised Resource: A dictionary of information gathered during inventory this also includes case_number and compromise type in the dictionary. 7 | * Dry Run Flag : True / False will eventually propogate from dry_run flag upstream. 8 | -------------------------------------------------------------------------------- /aws_ir_plugins/__init__.py: -------------------------------------------------------------------------------- 1 | from aws_ir_plugins import _version 2 | 3 | __version__ = _version.__version__ 4 | -------------------------------------------------------------------------------- /aws_ir_plugins/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.3' 2 | -------------------------------------------------------------------------------- /aws_ir_plugins/disableaccess_key.py: -------------------------------------------------------------------------------- 1 | import logging 2 | """ Allows the examiner cidr range access to the instance. """ 3 | 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class DisableOwnKeyError(RuntimeError): 9 | """Thrown when a request is made to disable the current key being used.""" 10 | pass 11 | 12 | 13 | class Plugin(object): 14 | def __init__( 15 | self, 16 | boto_session, 17 | compromised_resource, 18 | dry_run 19 | ): 20 | 21 | self.session = boto_session # Requires an IAM Client 22 | self.compromised_resource = compromised_resource 23 | self.compromise_type = compromised_resource['compromise_type'] 24 | self.dry_run = dry_run 25 | 26 | self.access_key_id = self.compromised_resource['access_key_id'] 27 | self.setup() 28 | 29 | def setup(self): 30 | """Method runs the plugin""" 31 | if self.dry_run is not True: 32 | self.client = self._get_client() 33 | self._disable_access_key() 34 | 35 | def validate(self): 36 | """Returns whether this plugin does what it claims to have done""" 37 | try: 38 | response = self.client.get_access_key_last_used( 39 | AccessKeyId=self.access_key_id 40 | ) 41 | 42 | username = response['UserName'] 43 | access_keys = self.client.list_access_keys( 44 | UserName=username 45 | ) 46 | 47 | for key in access_keys['AccessKeyMetadata']: 48 | if \ 49 | (key['AccessKeyId'] == self.access_key_id)\ 50 | and (key['Status'] == 'Inactive'): 51 | return True 52 | 53 | return False 54 | except Exception as e: 55 | logger.info( 56 | "Failed to validate key disable for " 57 | "key {id} due to: {e}.".format( 58 | e=e, id=self.access_key_id 59 | ) 60 | ) 61 | return False 62 | 63 | def _get_client(self): 64 | client = self.session.client( 65 | service_name='iam', 66 | region_name='us-west-2' 67 | ) 68 | return client 69 | 70 | def _search_user_for_key(self): 71 | try: 72 | response = self.client.get_access_key_last_used( 73 | AccessKeyId=self.access_key_id 74 | ) 75 | logger.info( 76 | "A user for key {id} has been " 77 | "found proceeding to disable.".format( 78 | id=self.access_key_id 79 | ) 80 | ) 81 | return response['UserName'] 82 | except Exception as e: 83 | logger.info( 84 | "A user for key {id} could " 85 | "not be located due to: {e}.".format( 86 | e=e, id=self.access_key_id 87 | ) 88 | ) 89 | 90 | def _disable_access_key(self, force_disable_self=False): 91 | """This function first checks to see if the key is already disabled\ 92 | 93 | if not then it goes to disabling 94 | """ 95 | client = self.client 96 | if self.validate is True: 97 | return 98 | else: 99 | try: 100 | client.update_access_key( 101 | UserName=self._search_user_for_key(), 102 | AccessKeyId=self.access_key_id, 103 | Status='Inactive' 104 | ) 105 | logger.info( 106 | "Access key {id} has " 107 | "been disabled.".format(id=self.access_key_id) 108 | ) 109 | except Exception as e: 110 | logger.info( 111 | "Access key {id} could not " 112 | "be disabled due to: {e}.".format( 113 | e=e, id=self.access_key_id 114 | ) 115 | ) 116 | -------------------------------------------------------------------------------- /aws_ir_plugins/examineracl_host.py: -------------------------------------------------------------------------------- 1 | import logging 2 | """ Allows the examiner cidr range access to the instance. """ 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class Plugin(object): 8 | def __init__( 9 | self, 10 | boto_session, 11 | compromised_resource, 12 | dry_run 13 | ): 14 | 15 | self.session = boto_session 16 | self.compromised_resource = compromised_resource 17 | self.compromise_type = compromised_resource['compromise_type'] 18 | self.examiner_cidr_range = compromised_resource['examiner_cidr_range'] 19 | self.dry_run = dry_run 20 | 21 | self.setup() 22 | 23 | def setup(self): 24 | self.client = self._get_client() 25 | sg = self._create_isolation_security_group() 26 | if self.exists is not True: 27 | acl = self._create_network_acl() 28 | self._add_network_acl_entries(acl) 29 | self._add_security_group_rule(sg) 30 | self._add_security_group_to_instance(sg) 31 | 32 | """Conditions that can not be dry_run""" 33 | if self.dry_run is not False: 34 | self._add_security_group_rule(sg) 35 | self._add_security_group_to_instance(sg) 36 | 37 | def validate(self): 38 | """Validate that the instance is in fact isolated""" 39 | if self.sg_name is not None: 40 | return True 41 | else: 42 | return False 43 | 44 | def _get_client(self): 45 | client = self.session.client( 46 | service_name='ec2' 47 | ) 48 | return client 49 | 50 | def _create_isolation_security_group(self): 51 | try: 52 | security_group_result = self.client.create_security_group( 53 | DryRun=self.dry_run, 54 | GroupName=self._generate_security_group_name(), 55 | Description="ThreatResponse Examiner Access Security Group", 56 | VpcId=self.compromised_resource['vpc_id'], 57 | 58 | ) 59 | self.exists = False 60 | 61 | except Exception: 62 | logger.info( 63 | "Security group already exists. Attaching existing SG." 64 | ) 65 | self.exists = True 66 | security_group_result = self.client.describe_security_groups( 67 | DryRun=self.dry_run, 68 | GroupNames=[ 69 | self._generate_security_group_name() 70 | ] 71 | )['SecurityGroups'][0] 72 | return security_group_result['GroupId'] 73 | 74 | def _add_security_group_rule(self, security_group_id): 75 | try: 76 | self.client.authorize_security_group_ingress( 77 | DryRun=self.dry_run, 78 | GroupId=security_group_id, 79 | IpProtocol='tcp', 80 | FromPort=22, 81 | ToPort=22, 82 | CidrIp=self.examiner_cidr_range 83 | ) 84 | 85 | self.client.revoke_security_group_egress( 86 | DryRun=self.dry_run, 87 | GroupId=security_group_id, 88 | IpPermissions=[ 89 | { 90 | 'IpProtocol': '-1', 91 | 'IpRanges': [ 92 | { 93 | 'CidrIp': '0.0.0.0/0' 94 | } 95 | ] 96 | } 97 | ] 98 | ) 99 | 100 | self.client.authorize_security_group_egress( 101 | DryRun=self.dry_run, 102 | GroupId=security_group_id, 103 | IpPermissions=[ 104 | { 105 | 'IpProtocol': 'tcp', 106 | 'FromPort': 32000, 107 | 'ToPort': 65535, 108 | 'IpRanges': [ 109 | { 110 | 'CidrIp': self.examiner_cidr_range 111 | } 112 | ] 113 | } 114 | ] 115 | ) 116 | except Exception as e: 117 | logger.info( 118 | 'There isolation SG could not be added ' 119 | 'to instance {i} due to {e}'.format( 120 | e=e, 121 | i=self.compromised_resource['instance_id'] 122 | ) 123 | ) 124 | 125 | def _generate_security_group_name(self): 126 | sg_name = "examiner-sg-{case_number}-{instance}".format( 127 | case_number=self.compromised_resource['case_number'], 128 | instance=self.compromised_resource['instance_id'] 129 | ) 130 | self.sg_name = sg_name 131 | return sg_name 132 | 133 | def _add_security_group_to_instance(self, group_id): 134 | try: 135 | self.client.modify_instance_attribute( 136 | DryRun=self.dry_run, 137 | InstanceId=self.compromised_resource['instance_id'], 138 | Groups=[ 139 | group_id, 140 | ], 141 | ) 142 | return True 143 | except Exception: 144 | return False 145 | 146 | def _create_network_acl(self): 147 | try: 148 | response = self.client.create_network_acl( 149 | DryRun=self.dry_run, 150 | VpcId=self.compromised_resource['vpc_id'], 151 | ) 152 | return response['NetworkAcl']['NetworkAclId'] 153 | except Exception as e: 154 | logger.info( 155 | 'There was an error {e} ' 156 | 'while creating the nacl'.format(e=e) 157 | ) 158 | 159 | def _add_network_acl_entries(self, acl_id): 160 | try: 161 | self.client.create_network_acl_entry( 162 | DryRun=self.dry_run, 163 | NetworkAclId=acl_id, 164 | RuleNumber=1336, 165 | Protocol='-1', 166 | RuleAction='allow', 167 | Egress=True, 168 | CidrBlock=self.examiner_cidr_range 169 | ) 170 | return True 171 | except Exception as e: 172 | logger.info( 173 | 'There was an error {e} while adding the nacl'.format(e=e) 174 | ) 175 | -------------------------------------------------------------------------------- /aws_ir_plugins/gather_host.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import logging 4 | from datetime import datetime 5 | """Gathers ephemeral data that could be lost on instance termination""" 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class DateTimeEncoder(json.JSONEncoder): 11 | def default(self, o): 12 | if isinstance(o, datetime): 13 | return o.isoformat() 14 | 15 | return super(DateTimeEncoder, self).default(o) 16 | 17 | 18 | class Plugin(object): 19 | """Initializer takes standard plugin constructor 20 | 21 | added api flag for data persistence in AWS_IR api 22 | """ 23 | def __init__( 24 | self, 25 | boto_session, 26 | compromised_resource, 27 | dry_run, 28 | api=False 29 | ): 30 | 31 | self.session = boto_session 32 | self.compromised_resource = compromised_resource 33 | self.compromise_type = compromised_resource['compromise_type'] 34 | self.dry_run = dry_run 35 | self.api = api 36 | 37 | """ 38 | These attrs will only be set during API=True 39 | Added for Readability and AWS_IR data persistence 40 | """ 41 | self.evidence = {} 42 | 43 | self.setup() 44 | 45 | def setup(self): 46 | if self.dry_run is not True: 47 | self.client = self._get_client() 48 | metadata = self._get_aws_instance_metadata() 49 | self._log_aws_instance_metadata(metadata) 50 | console = self._get_aws_instance_console_output() 51 | self._log_aws_instance_console_output(console) 52 | self._log_aws_instance_screenshot() 53 | return True 54 | else: 55 | return False 56 | 57 | def validate(self): 58 | """Can't really validate data gather.""" 59 | return True 60 | 61 | def _get_client(self): 62 | client = self.session.client( 63 | service_name='ec2' 64 | ) 65 | return client 66 | 67 | def _get_aws_instance_metadata(self): 68 | metadata = self.client.describe_instances( 69 | Filters=[ 70 | { 71 | 'Name': 'instance-id', 72 | 'Values': [ 73 | self.compromised_resource['instance_id'] 74 | ] 75 | } 76 | ] 77 | )['Reservations'] 78 | 79 | return metadata 80 | 81 | def _log_aws_instance_metadata(self, data): 82 | if self.api is True: 83 | self.evidence['metadata.json'] = json.dumps(data, cls=DateTimeEncoder) 84 | else: 85 | logfile = ("/tmp/{case_number}-{instance_id}-metadata.log").format( 86 | case_number=self.compromised_resource['case_number'], 87 | instance_id=self.compromised_resource['instance_id'] 88 | ) 89 | with open(logfile, 'w') as w: 90 | w.write(str(data)) 91 | 92 | def _get_aws_instance_console_output(self): 93 | output = self.client.get_console_output( 94 | InstanceId=self.compromised_resource['instance_id'] 95 | ) 96 | return output 97 | 98 | def _log_aws_instance_console_output(self, data): 99 | if self.api is True: 100 | self.evidence['console.json'] = json.dumps(data, cls=DateTimeEncoder) 101 | else: 102 | logfile = ("/tmp/{case_number}-{instance_id}-console.log").format( 103 | case_number=self.compromised_resource['case_number'], 104 | instance_id=self.compromised_resource['instance_id'] 105 | ) 106 | with open(logfile, 'w') as w: 107 | w.write(str(data)) 108 | logger.info( 109 | 'Console logs have been acquired for' 110 | '{i}'.format( 111 | i=self.compromised_resource['instance_id'] 112 | ) 113 | ) 114 | 115 | def _log_aws_instance_screenshot(self): 116 | try: 117 | response = self.client.get_console_screenshot( 118 | InstanceId=self.compromised_resource['instance_id'], 119 | WakeUp=True 120 | ) 121 | if self.api is True: 122 | self.evidence['screenshot.jpg'] = base64.b64decode(response['ImageData']) 123 | else: 124 | logfile = ("/tmp/{case_number}-{instance_id}-screenshot.jpg")\ 125 | .format( 126 | case_number=self.compromised_resource['case_number'], 127 | instance_id=self.compromised_resource['instance_id'] 128 | ) 129 | 130 | fh = open(logfile, "wb") 131 | fh.write(base64.b64decode(response['ImageData'])) 132 | fh.close() 133 | logger.info( 134 | 'Screenshot has been acquired for ' 135 | '{i}'.format( 136 | i=self.compromised_resource['instance_id'] 137 | ) 138 | ) 139 | except Exception as e: 140 | logger.info( 141 | 'There was an error {e} while ' 142 | 'fetching the screenshot for {i}'.format( 143 | e=e, 144 | i=self.compromised_resource['instance_id'] 145 | ) 146 | ) 147 | -------------------------------------------------------------------------------- /aws_ir_plugins/isolate_host.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class Plugin(object): 8 | def __init__( 9 | self, 10 | boto_session, 11 | compromised_resource, 12 | dry_run=False 13 | ): 14 | self.session = boto_session 15 | self.compromised_resource = compromised_resource 16 | self.compromise_type = compromised_resource['compromise_type'] 17 | self.examiner_cidr_range = compromised_resource['examiner_cidr_range'] 18 | self.dry_run = dry_run 19 | 20 | self.setup() 21 | 22 | def setup(self): 23 | self.client = self._get_client() 24 | sg = self._create_isolation_security_group() 25 | if self.exists is not True: 26 | acl = self._create_network_acl() 27 | self._add_network_acl_entries(acl) 28 | self._add_security_group_to_instance(sg) 29 | 30 | """Conditions that can not be dry_run""" 31 | if self.dry_run is False: 32 | self._revoke_egress(sg) 33 | self._add_security_group_to_instance(sg) 34 | 35 | def validate(self): 36 | """Validate that the instance is in fact isolated""" 37 | if self.sg_name is not None: 38 | return True 39 | else: 40 | return False 41 | 42 | def _get_client(self): 43 | client = self.session.client( 44 | service_name='ec2' 45 | ) 46 | return client 47 | 48 | def _create_isolation_security_group(self): 49 | try: 50 | security_group_result = self.client.create_security_group( 51 | DryRun=self.dry_run, 52 | GroupName=self._generate_security_group_name(), 53 | Description="ThreatResponse Isolation Security Group", 54 | VpcId=self.compromised_resource['vpc_id'], 55 | ) 56 | 57 | self.exists = False 58 | 59 | except Exception: 60 | logger.info( 61 | "Security group already exists. Attaching existing SG." 62 | ) 63 | self.exists = True 64 | security_group_result = self.client.describe_security_groups( 65 | DryRun=self.dry_run, 66 | Filters=[{ 67 | 'Name': 'group-name', 68 | 'Values': [ 69 | self._generate_security_group_name(), 70 | ] 71 | }] 72 | )['SecurityGroups'][0] 73 | return security_group_result['GroupId'] 74 | 75 | def _revoke_egress(self, group_id): 76 | try: 77 | result = self.client.revoke_security_group_egress( 78 | DryRun=self.dry_run, 79 | GroupId=group_id, 80 | IpPermissions=[ 81 | { 82 | 'IpProtocol': '-1', 83 | 'FromPort': -1, 84 | 'ToPort': -1, 85 | }, 86 | ] 87 | ) 88 | return result 89 | except Exception as e: 90 | logger.info('There was an error {e} while ' 91 | 'revoking egress from {sg}.'.format(e=e, sg=group_id)) 92 | 93 | def _generate_security_group_name(self): 94 | sg_name = "isolation-sg-{case_number}-{instance}".format( 95 | case_number=self.compromised_resource['case_number'], 96 | instance=self.compromised_resource['instance_id'] 97 | ) 98 | self.sg_name = sg_name 99 | return sg_name 100 | 101 | def _add_security_group_to_instance(self, group_id): 102 | try: 103 | self.client.modify_instance_attribute( 104 | DryRun=self.dry_run, 105 | InstanceId=self.compromised_resource['instance_id'], 106 | Groups=[ 107 | group_id, 108 | ], 109 | ) 110 | return True 111 | except Exception: 112 | return False 113 | 114 | def _create_network_acl(self): 115 | try: 116 | response = self.client.create_network_acl( 117 | DryRun=self.dry_run, 118 | VpcId=self.compromised_resource['vpc_id'], 119 | ) 120 | return response['NetworkAcl']['NetworkAclId'] 121 | except Exception as e: 122 | logger.info( 123 | 'There was an error {e} ' 124 | 'while creating the nacl'.format(e=e) 125 | ) 126 | 127 | def _add_network_acl_entries(self, acl_id): 128 | try: 129 | self.client.create_network_acl_entry( 130 | DryRun=self.dry_run, 131 | NetworkAclId=acl_id, 132 | RuleNumber=1337, 133 | Protocol='-1', 134 | RuleAction='deny', 135 | Egress=True, 136 | CidrBlock="0.0.0.0/0" 137 | ) 138 | return True 139 | except Exception as e: 140 | logger.info( 141 | 'There was an error {e} ' 142 | 'while adding the nacl'.format(e=e) 143 | ) 144 | -------------------------------------------------------------------------------- /aws_ir_plugins/revokests_key.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import fnmatch 3 | import logging 4 | import os 5 | 6 | from jinja2 import Template 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Plugin(object): 13 | def __init__( 14 | self, 15 | boto_session, 16 | compromised_resource, 17 | dry_run 18 | ): 19 | 20 | self.session = boto_session 21 | self.compromised_resource = compromised_resource 22 | self.compromise_type = compromised_resource['compromise_type'] 23 | self.dry_run = dry_run 24 | 25 | self.setup() 26 | 27 | def setup(self): 28 | """Method runs the plugin attaching policies to the user in question""" 29 | self.template = self._generate_inline_policy() 30 | if self.dry_run is not True: 31 | self.client = self._get_client() 32 | username = self._get_username_for_key() 33 | policy_document = self._generate_inline_policy() 34 | self._attach_inline_policy(username, policy_document) 35 | pass 36 | 37 | def validate(self): 38 | """Checks the a policy is actually attached""" 39 | for policy in self._get_policies()['PolicyNames']: 40 | if policy == "threatresponse-temporal-key-revocation": 41 | return True 42 | else: 43 | pass 44 | return False 45 | 46 | def _get_client(self): 47 | client = self.session.client( 48 | service_name='iam', 49 | region_name='us-west-2' 50 | ) 51 | return client 52 | 53 | def _get_policies(self): 54 | """Returns all the policy names for a given user""" 55 | username = self._get_username_for_key() 56 | policies = self.client.list_user_policies( 57 | UserName=username 58 | ) 59 | return policies 60 | 61 | def _get_date(self): 62 | """Returns a date in zulu time""" 63 | now = datetime.datetime.utcnow().isoformat() + 'Z' 64 | return now 65 | 66 | def _get_username_for_key(self): 67 | """Find the user for a given access key""" 68 | response = self.client.get_access_key_last_used( 69 | AccessKeyId=self.compromised_resource['access_key_id'] 70 | ) 71 | username = response['UserName'] 72 | return username 73 | 74 | def _generate_inline_policy(self): 75 | """Renders a policy from a jinja template""" 76 | template_name = self._locate_file('deny-sts-before-time.json.j2') 77 | template_file = open(template_name) 78 | template_contents = template_file.read() 79 | template_file.close() 80 | jinja_template = Template(template_contents) 81 | policy_document = jinja_template.render( 82 | before_date=self._get_date() 83 | ) 84 | return policy_document 85 | 86 | def _attach_inline_policy(self, username, policy_document): 87 | """Attaches the policy to the user""" 88 | response = self.client.put_user_policy( 89 | UserName=username, 90 | PolicyName="threatresponse-temporal-key-revocation", 91 | PolicyDocument=policy_document 92 | ) 93 | logger.info( 94 | 'An inline policy has been attached for' 95 | ' {u} revoking sts tokens.'.format(u=username) 96 | ) 97 | return response 98 | 99 | def _locate_file(self, pattern, root=os.path.dirname('revokests_key.py')): 100 | """Locate all files matching supplied filename pattern in and below 101 | 102 | supplied root directory. 103 | """ 104 | 105 | for path, dirs, files in os.walk(os.path.abspath(root)): 106 | for filename in fnmatch.filter(files, pattern): 107 | return os.path.join(path, filename) 108 | -------------------------------------------------------------------------------- /aws_ir_plugins/snapshotdisks_host.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class Plugin(object): 8 | def __init__( 9 | self, 10 | boto_session, 11 | compromised_resource, 12 | dry_run 13 | ): 14 | 15 | self.session = boto_session 16 | self.compromised_resource = compromised_resource 17 | self.compromise_type = compromised_resource['compromise_type'] 18 | self.dry_run = dry_run 19 | self.setup() 20 | 21 | def setup(self): 22 | self.client = self._get_client() 23 | self.snapshot_volumes() 24 | 25 | def _get_client(self): 26 | client = self.session.client( 27 | service_name='ec2' 28 | ) 29 | return client 30 | 31 | def _get_resource(self): 32 | return self.session.resource('ec2') 33 | 34 | def _create_snapshot(self, volume_id, description): 35 | try: 36 | response = self.client.create_snapshot( 37 | DryRun=self.dry_run, 38 | VolumeId=volume_id, 39 | Description=description 40 | ) 41 | return response 42 | logger.info('A snapshot was taken' 43 | ' for volume {v}.'.format(v=volume_id)) 44 | except Exception as e: 45 | print(e) 46 | logger.info( 47 | 'There was an error taking the ' 48 | 'snapshot for volume {v}.'.format(v=volume_id)) 49 | return None 50 | 51 | def _tag_snapshot(self, snapshot_id): 52 | if snapshot_id is not None: 53 | ec2 = self._get_resource() 54 | snapshot = ec2.Snapshot(snapshot_id) 55 | snapshot.create_tags( 56 | Tags=[ 57 | dict( 58 | Key='cr-case-number', 59 | Value=self.compromised_resource['case_number'] 60 | ) 61 | ] 62 | ) 63 | return True 64 | else: 65 | return False 66 | 67 | def snapshot_volumes(self): 68 | logger.info('Attempting snapshots on compromised resource.') 69 | 70 | for volume_id in self.compromised_resource['volume_ids']: 71 | description = 'Snapshot of {vid} for case {cn}'.format( 72 | vid=volume_id, 73 | cn=self.compromised_resource['case_number'] 74 | ) 75 | 76 | snapshot = self._create_snapshot(volume_id, description) 77 | if snapshot is not None: 78 | snapshot_id = snapshot.get('SnapshotId') 79 | print(self._tag_snapshot(snapshot_id)) 80 | 81 | def validate(self): 82 | return True 83 | -------------------------------------------------------------------------------- /aws_ir_plugins/stop_host.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class Plugin(object): 8 | def __init__( 9 | self, 10 | boto_session, 11 | compromised_resource, 12 | dry_run 13 | ): 14 | 15 | self.session = boto_session 16 | self.compromised_resource = compromised_resource 17 | self.compromise_type = compromised_resource['compromise_type'] 18 | self.dry_run = dry_run 19 | self.setup() 20 | 21 | def setup(self): 22 | self.client = self._get_client() 23 | if self.dry_run is not True: 24 | self.stop_instance() 25 | else: 26 | pass 27 | 28 | def validate(self): 29 | return True 30 | 31 | def _get_client(self): 32 | client = self.session.client( 33 | service_name='ec2' 34 | ) 35 | return client 36 | 37 | def stop_instance(self): 38 | try: 39 | response = self.client.stop_instances( 40 | InstanceIds=[ 41 | self.compromised_resource['instance_id'] 42 | ], 43 | Force=True 44 | ) 45 | logger.info( 46 | 'Stop instance success for instance: {i}'.format( 47 | i=self.compromised_resource['instance_id'] 48 | ) 49 | ) 50 | except Exception as e: 51 | logger.info( 52 | 'Failed to stop instance: {i}, error {e}'.format( 53 | i=self.compromised_resource['instance_id'], 54 | e=e 55 | ) 56 | ) 57 | return response 58 | -------------------------------------------------------------------------------- /aws_ir_plugins/tag_host.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class Plugin(object): 8 | def __init__( 9 | self, 10 | boto_session, 11 | compromised_resource, 12 | dry_run 13 | ): 14 | self.session = boto_session 15 | self.compromised_resource = compromised_resource 16 | self.compromise_type = compromised_resource['compromise_type'] 17 | self.dry_run = dry_run 18 | self.setup() 19 | 20 | def setup(self): 21 | self.client = self._get_client() 22 | self.tagged = self.add_incident_tag_to_instance() 23 | 24 | def validate(self): 25 | instance_id = self.compromised_resource['instance_id'] 26 | response = self.client.describe_instances( 27 | InstanceIds=[instance_id], 28 | Filters=[ 29 | { 30 | 'Name': 'tag-key', 31 | 'Values': [ 32 | 'cr-case-number' 33 | ] 34 | }, 35 | ], 36 | ) 37 | if len(response['Reservations']) > 0: 38 | return True 39 | else: 40 | return False 41 | 42 | def _get_client(self): 43 | client = self.session.client( 44 | service_name='ec2' 45 | ) 46 | return client 47 | 48 | def _create_tags(self): 49 | tag = [ 50 | { 51 | 'Key': 'cr-case-number', 52 | 'Value': self.compromised_resource['case_number'] 53 | } 54 | ] 55 | 56 | return tag 57 | 58 | def add_incident_tag_to_instance(self): 59 | instance_id = self.compromised_resource['instance_id'] 60 | try: 61 | self.client.create_tags( 62 | DryRun=self.dry_run, 63 | Resources=[instance_id], 64 | Tags=self._create_tags() 65 | ) 66 | return True 67 | 68 | except Exception as e: 69 | print(e) 70 | if e.response['Error']['Message'] == """ 71 | Request would have succeeded, but DryRun flag is set. 72 | """: 73 | return None 74 | else: 75 | raise e 76 | -------------------------------------------------------------------------------- /aws_ir_plugins/templates/deny-sts-before-time.json.j2: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": { 4 | "Effect": "Deny", 5 | "Action": "*", 6 | "Resource": "*", 7 | "Condition": {"DateLessThan": {"aws:TokenIssueTime": "{{ before_date }}"}} 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | moto 3 | mock 4 | magicmock 5 | requests 6 | jinja2 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThreatResponse/aws_ir_plugins/b5128ef5cbd91fc0b5d55615f1c14cb036ae7c73/requirements.txt -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | 4 | [flake8] 5 | ignore=E402,H238 6 | max-line-length=99 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unittest 3 | 4 | from distutils.command.build import build 5 | from setuptools import setup 6 | 7 | VERSION = re.search( 8 | r"^__version__ = ['\"]([^'\"]*)['\"]", 9 | open('aws_ir_plugins/_version.py', 'r').read(), 10 | re.MULTILINE 11 | ).group(1) 12 | 13 | def test_suite(): 14 | test_loader = unittest.TestLoader() 15 | test_suite = test_loader.discover('tests', pattern='test_*.py') 16 | return test_suite 17 | 18 | setup(name="aws_ir_plugins", 19 | version=VERSION, 20 | author="Andrew Krug, Alex McCormack, Joel Ferrier, Jeff Parr", 21 | author_email="andrewkrug@gmail.com,developer@amccormack.net,joel@ferrier.io,jp@ephemeralsystems.com", 22 | packages=["aws_ir_plugins"], 23 | package_data={'aws_ir_plugins': ['templates/*.j2']}, 24 | license="MIT", 25 | description="AWS Incident Response ToolKit Core Supported plugins", 26 | url='https://github.com/ThreatResponse/aws_ir_plugins', 27 | download_url="", 28 | use_2to3=True, 29 | test_suite=('setup.test_suite'), 30 | install_requires=['boto3>=1.3.0', 31 | 'requests', 32 | 'jinja2', 33 | ], 34 | tests_require=['moto', 35 | 'mock', 36 | 'magicmock'], 37 | ) 38 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThreatResponse/aws_ir_plugins/b5128ef5cbd91fc0b5d55615f1c14cb036ae7c73/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_disableaccess.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import unittest 3 | 4 | from aws_ir_plugins import disableaccess_key 5 | from moto import mock_iam 6 | from unittest.mock import patch 7 | 8 | 9 | class DisableKeyTest(unittest.TestCase): 10 | @mock_iam 11 | def test_disable_plugin(self): 12 | self.iam = boto3.client('iam', region_name='us-west-2') 13 | self.user = self.iam.create_user( 14 | UserName='bobert' 15 | ) 16 | 17 | self.access_key = self.iam.create_access_key( 18 | UserName='bobert' 19 | ) 20 | 21 | self.access_key_id = self.access_key['AccessKey']['AccessKeyId'] 22 | 23 | self.compromised_resource = { 24 | 'case_number': '123456', 25 | 'access_key_id': self.access_key_id, 26 | 'compromise_type': 'key' 27 | } 28 | session = boto3.Session() 29 | with patch.object( 30 | disableaccess_key.Plugin, 31 | '_search_user_for_key', 32 | return_value='bobert' 33 | ) as mock_client: 34 | 35 | mock_client.return_value = 'bobert' 36 | 37 | plugin = disableaccess_key.Plugin( 38 | boto_session=session, 39 | compromised_resource=self.compromised_resource, 40 | dry_run=False 41 | ) 42 | 43 | assert plugin.validate() is not None 44 | -------------------------------------------------------------------------------- /tests/test_examineracl.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import unittest 3 | 4 | from aws_ir_plugins import examineracl_host 5 | from moto import mock_ec2 6 | 7 | 8 | class ExaminerACLHostTest(unittest.TestCase): 9 | @mock_ec2 10 | def test_tag_host(self): 11 | self.ec2 = boto3.client('ec2', region_name='us-west-2') 12 | session = boto3.Session(region_name='us-west-2') 13 | 14 | ec2_resource = session.resource('ec2') 15 | 16 | vpc = self.ec2.create_vpc( 17 | CidrBlock='10.0.0.0/8', 18 | InstanceTenancy='default' 19 | ) 20 | 21 | subnet = self.ec2.create_subnet( 22 | CidrBlock='10.0.1.0/24', 23 | VpcId=vpc['Vpc']['VpcId'] 24 | ) 25 | 26 | vpc_id = vpc['Vpc']['VpcId'] 27 | 28 | sec_group = self.ec2.create_security_group( 29 | Description='Test test', 30 | GroupName='test', 31 | VpcId=vpc_id, 32 | ) 33 | 34 | instance = ec2_resource.create_instances( 35 | ImageId='foo', 36 | MinCount=1, 37 | MaxCount=1, 38 | InstanceType='t2.medium', 39 | KeyName='akrug-key', 40 | Placement={ 41 | 'AvailabilityZone': subnet['Subnet']['AvailabilityZone'] 42 | }, 43 | SubnetId=subnet['Subnet']['SubnetId'], 44 | SecurityGroupIds=[ 45 | sec_group['GroupId'] 46 | ] 47 | ) 48 | 49 | self.compromised_resource = { 50 | 'case_number': '123456', 51 | 'instance_id': instance[0].id, 52 | 'vpc_id': vpc_id, 53 | 'compromise_type': 'host', 54 | 'examiner_cidr_range': '8.8.8.8/32' 55 | } 56 | 57 | plugin = examineracl_host.Plugin( 58 | boto_session=session, 59 | compromised_resource=self.compromised_resource, 60 | dry_run=False 61 | ) 62 | 63 | result = plugin.validate() 64 | 65 | assert result is True 66 | -------------------------------------------------------------------------------- /tests/test_gatherhost.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import unittest 3 | 4 | from aws_ir_plugins import gather_host 5 | from moto import mock_ec2 6 | 7 | 8 | class GatherHostTest(unittest.TestCase): 9 | @mock_ec2 10 | def test_gather_host(self): 11 | self.ec2 = boto3.client('ec2', region_name='us-west-2') 12 | session = boto3.Session(region_name='us-west-2') 13 | 14 | ec2_resource = session.resource('ec2') 15 | 16 | instance = ec2_resource.create_instances( 17 | ImageId='foo', 18 | MinCount=1, 19 | MaxCount=1, 20 | InstanceType='t2.medium', 21 | KeyName='akrug-key', 22 | ) 23 | 24 | self.compromised_resource = { 25 | 'case_number': '123456', 26 | 'instance_id': instance[0].id, 27 | 'compromise_type': 'host' 28 | } 29 | 30 | plugin = gather_host.Plugin( 31 | boto_session=session, 32 | compromised_resource=self.compromised_resource, 33 | dry_run=False 34 | ) 35 | 36 | result = plugin.validate() 37 | 38 | assert result is True 39 | 40 | @mock_ec2 41 | def test_gather_host_api(self): 42 | self.ec2 = boto3.client('ec2', region_name='us-west-2') 43 | session = boto3.Session(region_name='us-west-2') 44 | 45 | ec2_resource = session.resource('ec2') 46 | 47 | instance = ec2_resource.create_instances( 48 | ImageId='foo', 49 | MinCount=1, 50 | MaxCount=1, 51 | InstanceType='t2.medium', 52 | KeyName='akrug-key', 53 | ) 54 | 55 | self.compromised_resource = { 56 | 'case_number': '123456', 57 | 'instance_id': instance[0].id, 58 | 'compromise_type': 'host' 59 | } 60 | 61 | plugin = gather_host.Plugin( 62 | boto_session=session, 63 | compromised_resource=self.compromised_resource, 64 | dry_run=False, 65 | api=True 66 | ) 67 | 68 | result = plugin.validate() 69 | 70 | assert result is True 71 | -------------------------------------------------------------------------------- /tests/test_isolatehost.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import unittest 3 | 4 | from aws_ir_plugins import isolate_host 5 | from moto import mock_ec2 6 | 7 | 8 | class IsolateHostTest(unittest.TestCase): 9 | @mock_ec2 10 | def test_tag_host(self): 11 | self.ec2 = boto3.client('ec2', region_name='us-west-2') 12 | session = boto3.Session(region_name='us-west-2') 13 | 14 | ec2_resource = session.resource('ec2') 15 | 16 | vpc = self.ec2.create_vpc( 17 | CidrBlock='10.0.0.0/8', 18 | InstanceTenancy='default' 19 | ) 20 | 21 | subnet = self.ec2.create_subnet( 22 | CidrBlock='10.0.1.0/24', 23 | VpcId=vpc['Vpc']['VpcId'] 24 | ) 25 | 26 | vpc_id = vpc['Vpc']['VpcId'] 27 | 28 | sec_group = self.ec2.create_security_group( 29 | Description='Test test', 30 | GroupName='test', 31 | VpcId=vpc_id, 32 | ) 33 | 34 | instance = ec2_resource.create_instances( 35 | ImageId='foo', 36 | MinCount=1, 37 | MaxCount=1, 38 | InstanceType='t2.medium', 39 | KeyName='akrug-key', 40 | Placement={ 41 | 'AvailabilityZone': subnet['Subnet']['AvailabilityZone'] 42 | }, 43 | SubnetId=subnet['Subnet']['SubnetId'], 44 | SecurityGroupIds=[ 45 | sec_group['GroupId'] 46 | ] 47 | ) 48 | 49 | self.compromised_resource = { 50 | 'case_number': '123456', 51 | 'instance_id': instance[0].id, 52 | 'vpc_id': vpc_id, 53 | 'compromise_type': 'host', 54 | 'examiner_cidr_range': '8.8.8.8/32' 55 | } 56 | 57 | plugin = isolate_host.Plugin( 58 | boto_session=session, 59 | compromised_resource=self.compromised_resource, 60 | dry_run=False 61 | ) 62 | 63 | result = plugin.validate() 64 | 65 | assert result is True 66 | -------------------------------------------------------------------------------- /tests/test_revokests_key.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import unittest 4 | 5 | from aws_ir_plugins import revokests_key 6 | from moto import mock_iam 7 | from unittest.mock import patch 8 | 9 | 10 | class RevokeSTSTest(unittest.TestCase): 11 | @mock_iam 12 | def test_jinja_rendering(self): 13 | self.iam = boto3.client('iam', region_name='us-west-2') 14 | self.user = self.iam.create_user( 15 | UserName='bobert' 16 | ) 17 | 18 | self.access_key = self.iam.create_access_key( 19 | UserName='bobert' 20 | ) 21 | 22 | self.access_key_id = self.access_key['AccessKey']['AccessKeyId'] 23 | 24 | self.compromised_resource = { 25 | 'case_number': '123456', 26 | 'access_key_id': self.access_key_id, 27 | 'compromise_type': 'key' 28 | } 29 | session = None 30 | with patch.object( 31 | revokests_key.Plugin, '_get_client', return_value=self.iam 32 | ) as mock_client: 33 | 34 | mock_client.return_value = self.iam 35 | plugin = revokests_key.Plugin( 36 | boto_session=session, 37 | compromised_resource=self.compromised_resource, 38 | dry_run=True 39 | ) 40 | 41 | assert json.loads(plugin.template) 42 | 43 | @mock_iam 44 | @patch('aws_ir_plugins.revokests_key.Plugin') 45 | def test_plugin(self, mock_revokests): 46 | self.iam = boto3.client('iam', region_name='us-west-2') 47 | mock_revokests._get_username_for_key.return_value = 'bobert' 48 | mock_revokests.validate.return_value = 'True' 49 | 50 | self.user = self.iam.create_user( 51 | UserName='bobert' 52 | ) 53 | 54 | self.access_key = self.iam.create_access_key( 55 | UserName='bobert' 56 | ) 57 | 58 | self.access_key_id = self.access_key['AccessKey']['AccessKeyId'] 59 | 60 | self.compromised_resource = { 61 | 'case_number': '123456', 62 | 'access_key_id': self.access_key_id, 63 | 'compromise_type': 'key' 64 | } 65 | 66 | with patch.object( 67 | revokests_key.Plugin, '_get_client', return_value=self.iam 68 | ) as mock_client: 69 | 70 | mock_client.return_value = self.iam 71 | plugin = revokests_key.Plugin( 72 | client=self.iam, 73 | compromised_resource=self.compromised_resource, 74 | dry_run=False 75 | ) 76 | 77 | res1 = plugin.setup() 78 | 79 | res2 = plugin.validate() 80 | 81 | self.policies = self.iam.list_user_policies( 82 | UserName='bobert' 83 | ) 84 | 85 | assert res1 is not None 86 | assert res2 is not None 87 | -------------------------------------------------------------------------------- /tests/test_snapshotdisks.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import unittest 3 | 4 | from aws_ir_plugins import snapshotdisks_host 5 | from moto import mock_ec2 6 | from unittest.mock import patch 7 | 8 | 9 | class SnapshotDisksTest(unittest.TestCase): 10 | @mock_ec2 11 | def test_disk_snapshot(self): 12 | self.ec2 = boto3.client('ec2', region_name='us-west-2') 13 | 14 | volume = self.ec2.create_volume( 15 | AvailabilityZone='us-west-2', 16 | Encrypted=False, 17 | Size=1024, 18 | VolumeType='standard', 19 | ) 20 | 21 | self.compromised_resource = { 22 | 'case_number': '123456', 23 | 'volume_ids': [volume.get('VolumeId')], 24 | 'compromise_type': 'host' 25 | } 26 | 27 | session = boto3.Session(region_name='us-west-2') 28 | with patch.object( 29 | snapshotdisks_host.Plugin, '_get_client', return_value=self.ec2 30 | ) as mock_client: 31 | 32 | mock_client.return_value = self.ec2 33 | plugin = snapshotdisks_host.Plugin( 34 | boto_session=session, 35 | compromised_resource=self.compromised_resource, 36 | dry_run=False 37 | ) 38 | 39 | assert plugin is not None 40 | -------------------------------------------------------------------------------- /tests/test_stophost.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import unittest 3 | 4 | from aws_ir_plugins import stop_host 5 | from moto import mock_ec2 6 | 7 | 8 | class StopHostTest(unittest.TestCase): 9 | @mock_ec2 10 | def test_tag_host(self): 11 | self.ec2 = boto3.client('ec2', region_name='us-west-2') 12 | session = boto3.Session(region_name='us-west-2') 13 | 14 | ec2_resource = session.resource('ec2') 15 | 16 | instance = ec2_resource.create_instances( 17 | ImageId='foo', 18 | MinCount=1, 19 | MaxCount=1, 20 | InstanceType='t2.medium', 21 | KeyName='akrug-key', 22 | ) 23 | 24 | self.compromised_resource = { 25 | 'case_number': '123456', 26 | 'instance_id': instance[0].id, 27 | 'compromise_type': 'host' 28 | } 29 | 30 | plugin = stop_host.Plugin( 31 | boto_session=session, 32 | compromised_resource=self.compromised_resource, 33 | dry_run=False 34 | ) 35 | 36 | result = plugin.validate() 37 | 38 | assert result is True 39 | -------------------------------------------------------------------------------- /tests/test_taghost.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import unittest 3 | 4 | from aws_ir_plugins import tag_host 5 | from moto import mock_ec2 6 | 7 | 8 | class TagHostTest(unittest.TestCase): 9 | @mock_ec2 10 | def test_tag_host(self): 11 | self.ec2 = boto3.client('ec2', region_name='us-west-2') 12 | session = boto3.Session(region_name='us-west-2') 13 | 14 | ec2_resource = session.resource('ec2') 15 | 16 | instance = ec2_resource.create_instances( 17 | ImageId='foo', 18 | MinCount=1, 19 | MaxCount=1, 20 | InstanceType='t2.medium', 21 | KeyName='akrug-key', 22 | ) 23 | 24 | self.compromised_resource = { 25 | 'case_number': '123456', 26 | 'instance_id': instance[0].id, 27 | 'compromise_type': 'host' 28 | } 29 | 30 | plugin = tag_host.Plugin( 31 | boto_session=session, 32 | compromised_resource=self.compromised_resource, 33 | dry_run=False 34 | ) 35 | 36 | result = plugin.validate() 37 | 38 | assert result is True 39 | --------------------------------------------------------------------------------