├── .gitignore ├── GDPatrol ├── config.json └── lambda_function.py ├── LICENSE.md ├── README.md ├── deploy.py ├── lambda_policy.json └── role_policy.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | -------------------------------------------------------------------------------- /GDPatrol/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "playbooks": { 3 | "playbook": [ 4 | { 5 | "type": "Backdoor:EC2/C&CActivity.B!DNS", 6 | "actions": ["blacklist_domain", "asg_detach_instance", "quarantine_instance", "snapshot_instance"], 7 | "reliability": 5 8 | }, 9 | { 10 | "type": "Backdoor:EC2/Spambot", 11 | "actions": ["blacklist_ip", "asg_detach_instance", "quarantine_instance", "snapshot_instance"], 12 | "reliability": 5 13 | }, 14 | { 15 | "type": "Backdoor:EC2/XORDDOS", 16 | "actions": ["blacklist_ip", "asg_detach_instance", "quarantine_instance", "snapshot_instance"], 17 | "reliability": 5 18 | }, 19 | { 20 | "type": "Behavior:EC2/NetworkPortUnusual", 21 | "actions": ["blacklist_ip"], 22 | "reliability": 5 23 | }, 24 | { 25 | "type": "Behavior:EC2/TrafficVolumeUnusual", 26 | "actions": ["blacklist_ip", "asg_detach_instance", "quarantine_instance", "snapshot_instance"], 27 | "reliability": 5 28 | }, 29 | { 30 | "type": "CryptoCurrency:EC2/BitcoinTool.B!DNS", 31 | "actions": ["blacklist_domain", "snapshot_instance"], 32 | "reliability": 5 33 | }, 34 | { 35 | "type": "PenTest:IAMUser/KaliLinux", 36 | "actions": ["disable_account"], 37 | "reliability": 5 38 | }, 39 | { 40 | "type": "Persistence:IAMUser/NetworkPermissions", 41 | "actions": "disable_sg_access", 42 | "reliability": 5 43 | }, 44 | { 45 | "type": "Persistence:IAMUser/ResourcePermissions", 46 | "actions": ["disable_account"], 47 | "reliability": 5 48 | }, 49 | { 50 | "type": "Persistence:IAMUser/UserPermissions", 51 | "actions": ["disable_account"], 52 | "reliability": 5 53 | }, 54 | { 55 | "type": "Recon:EC2/PortProbeUnprotectedPort", 56 | "actions": ["blacklist_ip"], 57 | "reliability": 5 58 | }, 59 | { 60 | "type": "Recon:EC2/Portscan", 61 | "actions": ["snapshot_instance", "asg_detach_instance", "quarantine_instance"], 62 | "reliability": 5 63 | }, 64 | { 65 | "type": "Recon:IAMUser/MaliciousIPCaller", 66 | "actions": ["disable_account"], 67 | "reliability": 5 68 | }, 69 | { 70 | "type": "Recon:IAMUser/MaliciousIPCaller.Custom", 71 | "actions": ["disable_account"], 72 | "reliability": 5 73 | }, 74 | { 75 | "type": "Recon:IAMUser/NetworkPermissions", 76 | "actions": ["disable_sg_access"], 77 | "reliability": 5 78 | }, 79 | { 80 | "type": "Recon:IAMUser/ResourcePermissions", 81 | "actions": ["disable_account"], 82 | "reliability": 5 83 | }, 84 | { 85 | "type": "Recon:IAMUser/TorIPCaller", 86 | "actions": ["disable_account"], 87 | "reliability": 5 88 | }, 89 | { 90 | "type": "Recon:IAMUser/UserPermissions", 91 | "actions": ["disable_account"], 92 | "reliability": 5 93 | }, 94 | { 95 | "type": "ResourceConsumption:IAMUser/ComputeResources", 96 | "actions": ["disable_ec2_access"], 97 | "reliability": 5 98 | }, 99 | { 100 | "type": "Stealth:IAMUser/CloudTrailLoggingDisabled", 101 | "actions": ["disable_account"], 102 | "reliability": 5 103 | }, 104 | { 105 | "type": "Stealth:IAMUser/LoggingConfigurationModified", 106 | "actions": ["disable_account"], 107 | "reliability": 5 108 | }, 109 | { 110 | "type": "Stealth:IAMUser/PasswordPolicyChange", 111 | "actions": ["disable_account"], 112 | "reliability": 5 113 | }, 114 | { 115 | "type": "Trojan:EC2/BlackholeTraffic", 116 | "actions": ["blacklist_ip", "asg_detach_instance", "quarantine_instance", "snapshot_instance"], 117 | "reliability": 5 118 | }, 119 | { 120 | "type": "Trojan:EC2/BlackholeTraffic!DNS", 121 | "actions": ["blacklist_domain", "asg_detach_instance", "quarantine_instance", "snapshot_instance"], 122 | "reliability": 5 123 | }, 124 | { 125 | "type": "Trojan:EC2/DGADomainRequest.B", 126 | "actions": ["blacklist_domain", "asg_detach_instance", "quarantine_instance", "snapshot_instance"], 127 | "reliability": 5 128 | }, 129 | { 130 | "type": "Trojan:EC2/DGADomainRequest.C!DNS", 131 | "actions": ["blacklist_domain", "asg_detach_instance", "quarantine_instance", "snapshot_instance"], 132 | "reliability": 5 133 | }, 134 | { 135 | "type": "Trojan:EC2/DNSDataExfiltration", 136 | "actions": ["blacklist_domain", "asg_detach_instance", "quarantine_instance", "snapshot_instance"], 137 | "reliability": 5 138 | }, 139 | { 140 | "type": "Trojan:EC2/DriveBySourceTraffic!DNS", 141 | "actions": ["blacklist_domain", "asg_detach_instance", "quarantine_instance", "snapshot_instance"], 142 | "reliability": 5 143 | }, 144 | { 145 | "type": "Trojan:EC2/DropPoint", 146 | "actions": ["blacklist_ip", "asg_detach_instance", "quarantine_instance", "snapshot_instance"], 147 | "reliability": 5 148 | }, 149 | { 150 | "type": "Trojan:EC2/DropPoint!DNS", 151 | "actions": ["blacklist_domain", "asg_detach_instance", "quarantine_instance", "snapshot_instance"], 152 | "reliability": 5 153 | }, 154 | { 155 | "type": "Trojan:EC2/PhishingDomainRequest!DNS", 156 | "actions": ["blacklist_domain", "asg_detach_instance", "quarantine_instance", "snapshot_instance"], 157 | "reliability": 5 158 | }, 159 | { 160 | "type": "UnauthorizedAccess:EC2/MaliciousIPCaller.Custom", 161 | "actions": ["blacklist_ip"], 162 | "reliability": 5 163 | }, 164 | { 165 | "type": "UnauthorizedAccess:EC2/RDPBruteForce", 166 | "actions": ["blacklist_ip"], 167 | "reliability": 5 168 | }, 169 | { 170 | "type": "UnauthorizedAccess:EC2/SSHBruteForce", 171 | "actions": ["blacklist_ip"], 172 | "reliability": 5 173 | }, 174 | { 175 | "type": "UnauthorizedAccess:EC2/TorIPCaller", 176 | "actions": ["blacklist_ip"], 177 | "reliability": 5 178 | }, 179 | { 180 | "type": "UnauthorizedAccess:IAMUser/ConsoleLogin", 181 | "actions": ["disable_account"], 182 | "reliability": 5 183 | }, 184 | { 185 | "type": "UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B", 186 | "actions": ["disable_account"], 187 | "reliability": 5 188 | }, 189 | { 190 | "type": "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration", 191 | "actions": ["disable_account"], 192 | "reliability": 5 193 | }, 194 | { 195 | "type": "UnauthorizedAccess:IAMUser/MaliciousIPCaller", 196 | "actions": ["disable_account", "blacklist_ip"], 197 | "reliability": 5 198 | }, 199 | { 200 | "type": "UnauthorizedAccess:IAMUser/MaliciousIPCaller.Custom", 201 | "actions": ["disable_account", "blacklist_ip"], 202 | "reliability": 5 203 | }, 204 | { 205 | "type": "UnauthorizedAccess:IAMUser/TorIPCaller", 206 | "actions": ["disable_account", "blacklist_ip"], 207 | "reliability": 5 208 | }, 209 | { 210 | "type": "UnauthorizedAccess:IAMUser/UnusualASNCaller", 211 | "actions": ["disable_account"], 212 | "reliability": 5 213 | } 214 | ] 215 | } 216 | } -------------------------------------------------------------------------------- /GDPatrol/lambda_function.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import uuid 3 | import json 4 | from socket import gethostbyname, gaierror 5 | from inspect import stack 6 | import logging 7 | 8 | 9 | logger = logging.getLogger() 10 | logger.setLevel(logging.INFO) 11 | 12 | 13 | def blacklist_ip(ip_address): 14 | try: 15 | client = boto3.client('ec2') 16 | nacls = client.describe_network_acls() 17 | for nacl in nacls["NetworkAcls"]: 18 | min_rule_id = min( 19 | rule['RuleNumber'] for rule in nacl["Entries"] if not rule["Egress"] 20 | ) 21 | if min_rule_id < 1: 22 | raise Exception("Rule number is less than 1") 23 | r = client.create_network_acl_entry( 24 | CidrBlock='{}/32'.format(ip_address), 25 | Egress=False, 26 | NetworkAclId=nacl["NetworkAclId"], 27 | Protocol='-1', 28 | RuleAction='deny', 29 | RuleNumber=min_rule_id - 1, 30 | ) 31 | logger.info("GDPatrol: Successfully executed action {} for ".format( 32 | stack()[0][3], ip_address)) 33 | return True 34 | except Exception as e: 35 | logger.error("GDPatrol: Error executing {} - {}".format(stack()[0][3], e)) 36 | 37 | 38 | def whitelist_ip(ip_address): 39 | try: 40 | client = boto3.client('ec2') 41 | nacls = client.describe_network_acls() 42 | for nacl in nacls["NetworkAcls"]: 43 | for rule in nacl["Entries"]: 44 | if rule["CidrBlock"] == '{}/32'.format(ip_address): 45 | client.delete_network_acl_entry( 46 | NetworkAclId=nacl["NetworkAclId"], 47 | Egress=rule["Egress"], 48 | RuleNumber=rule["RuleNumber"] 49 | ) 50 | logger.info("GDPatrol: Successfully executed action {} for {}".format(stack()[0][3], ip_address)) 51 | return True 52 | 53 | except Exception as e: 54 | logger.error("GDPatrol: Error executing {} - {}".format(stack()[0][3], e)) 55 | return False 56 | 57 | 58 | def quarantine_instance(instance_id, vpc_id): 59 | try: 60 | client = boto3.client('ec2') 61 | sg = client.create_security_group( 62 | GroupName='Quarantine-{}'.format(str(uuid.uuid4().fields[-1])[:6]), 63 | Description='Quarantine for {}'.format(instance_id), 64 | VpcId=vpc_id 65 | ) 66 | sg_id = sg["GroupId"] 67 | 68 | # NOTE: Remove the default egress group 69 | client.revoke_security_group_egress( 70 | GroupId=sg_id, 71 | IpPermissions=[ 72 | { 73 | 'IpProtocol': '-1', 74 | 'FromPort': 0, 75 | 'ToPort': 65535, 76 | 'IpRanges': [ 77 | { 78 | 'CidrIp': "0.0.0.0/0" 79 | }, 80 | ] 81 | } 82 | ] 83 | ) 84 | 85 | 86 | # NOTE: Assign security group to instance 87 | client.modify_instance_attribute(InstanceId=instance_id, Groups=[sg_id]) 88 | 89 | 90 | logger.info("GDPatrol: Successfully executed action {} for {}".format(stack()[0][3], instance_id)) 91 | return True 92 | except Exception as e: 93 | logger.error("GDPatrol: Error executing {} - {}".format(stack()[0][3], e)) 94 | return False 95 | 96 | 97 | def snapshot_instance(instance_id): 98 | try: 99 | client = boto3.client('ec2') 100 | instance_described = client.describe_instances(InstanceIds=[instance_id]) 101 | blockmappings = instance_described['Reservations'][0]['Instances'][0]['BlockDeviceMappings'] 102 | for device in blockmappings: 103 | snapshot = client.create_snapshot( 104 | VolumeId=device["Ebs"]["VolumeId"], 105 | Description="Created by GDpatrol for {}".format(instance_id) 106 | ) 107 | logger.info("GDPatrol: Successfully executed action {} for {}".format(stack()[0][3], instance_id)) 108 | return True 109 | except Exception as e: 110 | logger.error("GDPatrol: Error executing {} - {}".format(stack()[0][3], e)) 111 | return False 112 | 113 | def disable_account(username): 114 | try: 115 | client = boto3.client('iam') 116 | client.put_user_policy( 117 | UserName=username, 118 | PolicyName='BlockAllPolicy', 119 | PolicyDocument="{\"Version\":\"2012-10-17\", \"Statement\"" 120 | ":{\"Effect\":\"Deny\", \"Action\":\"*\", " 121 | "\"Resource\":\"*\"}}" 122 | ) 123 | logger.info("GDPatrol: Successfully executed action {} for {}".format(stack()[0][3], username)) 124 | return True 125 | except Exception as e: 126 | logger.error("GDPatrol: Error executing {} - {}".format(stack()[0][3], e)) 127 | return False 128 | 129 | 130 | def disable_ec2_access(username): 131 | try: 132 | client = boto3.client('iam') 133 | client.put_user_policy( 134 | UserName=username, 135 | PolicyName='BlockEC2Policy', 136 | PolicyDocument="{\"Version\":\"2012-10-17\", \"Statement\"" 137 | ":{\"Effect\":\"Deny\", \"Action\":\"ec2:*\" , " 138 | "\"Resource\":\"*\"}}" 139 | ) 140 | logger.info("GDPatrol: Successfully executed action {} for {}".format(stack()[0][3], username)) 141 | return True 142 | except Exception as e: 143 | logger.error("GDPatrol: Error executing {} - {}".format(stack()[0][3], e)) 144 | return False 145 | 146 | def enable_ec2_access(username): 147 | try: 148 | client = boto3.client('iam') 149 | client.delete_user_policy( 150 | UserName=username, 151 | PolicyName='BlockEC2Policy', 152 | ) 153 | logger.info("GDPatrol: Successfully executed action {} for {}".format(stack()[0][3], username)) 154 | return True 155 | except Exception as e: 156 | logger.error("GDPatrol: Error executing {} - {}".format(stack()[0][3], e)) 157 | return False 158 | 159 | 160 | def disable_sg_access(username): 161 | try: 162 | client = boto3.client('iam') 163 | client.put_user_policy( 164 | UserName=username, 165 | PolicyName='BlockSecurityGroupPolicy', 166 | PolicyDocument="{\"Version\":\"2012-10-17\", \"Statement\"" 167 | ":{\"Effect\":\"Deny\", \"Action\": [ " 168 | "\"ec2:AuthorizeSecurityGroupIngress\", " 169 | "\"ec2:RevokeSecurityGroupIngress\", " 170 | "\"ec2:AuthorizeSecurityGroupEgress\", " 171 | "\"ec2:RevokeSecurityGroupEgress\" ], " 172 | "\"Resource\":\"*\"}}" 173 | ) 174 | logger.info("GDPatrol: Successfully executed action {} for {}".format(stack()[0][3], username)) 175 | return True 176 | except Exception as e: 177 | logger.error("GDPatrol: Error executing {} - {}".format(stack()[0][3], e)) 178 | return False 179 | 180 | 181 | def enable_sg_access(username): 182 | try: 183 | client = boto3.client('iam') 184 | client.delete_user_policy( 185 | UserName=username, 186 | PolicyName='BlockSecurityGroupPolicy', 187 | ) 188 | logger.info("GDPatrol: Successfully executed action {} for {}".format(stack()[0][3], username)) 189 | return True 190 | except Exception as e: 191 | logger.error("GDPatrol: Error executing {} - {}".format(stack()[0][3], e)) 192 | return False 193 | 194 | 195 | def asg_detach_instance(instance_id): 196 | try: 197 | client = boto3.client('autoscaling') 198 | response = client.describe_auto_scaling_instances( 199 | InstanceIds=[instance_id], 200 | MaxRecords=1 201 | ) 202 | asg_name = None 203 | instances = response['AutoScalingInstances'] 204 | if instances: 205 | asg_name = instances[0]['AutoScalingGroupName'] 206 | 207 | if asg_name is not None: 208 | response = client.detach_instances( 209 | InstanceIds=[instance_id], 210 | AutoScalingGroupName=asg_name, 211 | ShouldDecrementDesiredCapacity=False 212 | ) 213 | logger.info("GDPatrol: Successfully executed action {} for {}".format(stack()[0][3], instance_id)) 214 | return True 215 | except Exception as e: 216 | logger.error("GDPatrol: Error executing {} - {}".format(stack()[0][3], e)) 217 | return False 218 | 219 | 220 | class Config(object): 221 | def __init__(self, finding_type): 222 | self.finding_type = finding_type 223 | self.actions = [] 224 | self.reliability = 0 225 | 226 | 227 | def get_actions(self): 228 | with open('config.json', 'r') as config: 229 | jsonloads = json.loads(config.read()) 230 | for item in jsonloads['playbooks']['playbook']: 231 | if item['type'] == self.finding_type: 232 | self.actions = item['actions'] 233 | return self.actions 234 | 235 | def get_reliability(self): 236 | with open('config.json', 'r') as config: 237 | jsonloads = json.loads(config.read()) 238 | for item in jsonloads['playbooks']['playbook']: 239 | if item['type'] == self.finding_type: 240 | self.reliability = int(item['reliability']) 241 | return self.reliability 242 | 243 | 244 | def lambda_handler(event, context): 245 | logger.info("GDPatrol: Received JSON event - ".format(event)) 246 | try: 247 | 248 | finding_id = event['id'] 249 | finding_type = event['type'] 250 | logger.info("GDPatrol: Parsed Finding ID: {} - Finding Type: {}".format(finding_id, finding_type)) 251 | config = Config(event['type']) 252 | severity = int(event['severity']) 253 | 254 | config_actions = config.get_actions() 255 | config_reliability = config.get_reliability() 256 | resource_type = event['resource']['resourceType'] 257 | except KeyError as e: 258 | logger.error("GDPatrol: Could not parse the Finding fields correctly, please verify that the JSON is correct") 259 | exit(1) 260 | if resource_type == 'Instance': 261 | instance = event['resource']['instanceDetails'] 262 | instance_id = instance["instanceId"] 263 | vpc_id = instance['networkInterfaces'][0]['vpcId'] 264 | elif resource_type == 'AccessKey': 265 | username = event['resource']['accessKeyDetails']['userName'] 266 | 267 | if event['service']['action']['actionType'] == 'DNS_REQUEST': 268 | domain = event['service']['action']['dnsRequestAction']['domain'] 269 | elif event['service']['action']['actionType'] == 'AWS_API_CALL': 270 | ip_address = event['service']['action']['awsApiCallAction']['remoteIpDetails']['ipAddressV4'] 271 | elif event['service']['action']['actionType'] == 'NETWORK_CONNECTION': 272 | ip_address = event['service']['action']['networkConnectionAction']['remoteIpDetails']['ipAddressV4'] 273 | elif event['service']['action']['actionType'] == 'PORT_PROBE': 274 | ip_address = event['service']['action']['portProbeAction']['portProbeDetails'][0]['remoteIpDetails']['ipAddressV4'] 275 | 276 | successful_actions = 0 277 | total_config_actions = len(config_actions) 278 | actions_to_be_executed = 0 279 | for action in config_actions: 280 | logger.info("GDPatrol: Action: {}".format(action)) 281 | if action == 'blacklist_ip': 282 | if severity + config_reliability > 10: 283 | actions_to_be_executed += 1 284 | logger.info("GDPatrol: Executing action {}".format(action)) 285 | result = blacklist_ip(ip_address) 286 | successful_actions += int(result) 287 | elif action == 'whitelist_ip': 288 | if severity + config_reliability > 10: 289 | actions_to_be_executed += 1 290 | logger.info("GDPatrol: Executing action {}".format(action)) 291 | result = whitelist_ip(ip_address) 292 | successful_actions += int(result) 293 | elif action == 'blacklist_domain': 294 | if severity + config_reliability > 10: 295 | actions_to_be_executed += 1 296 | logger.info("GDPatrol: Executing action {}".format(action)) 297 | try: 298 | ip_address = gethostbyname(domain) 299 | result = blacklist_ip(ip_address) 300 | successful_actions += int(result) 301 | except gaierror as e: 302 | logger.error("GDPatrol: Error resolving domain {} - {}".format(domain, e)) 303 | pass 304 | elif action == 'quarantine_instance': 305 | if severity + config_reliability > 10: 306 | actions_to_be_executed += 1 307 | logger.info("GDPatrol: Executing action {}".format(action)) 308 | result = quarantine_instance(instance_id, vpc_id) 309 | successful_actions += int(result) 310 | elif action == 'snapshot_instance': 311 | if severity + config_reliability > 10: 312 | actions_to_be_executed += 1 313 | logger.info("GDPatrol: Executing action {}".format(action)) 314 | result = snapshot_instance(instance_id) 315 | successful_actions += int(result) 316 | elif action == 'disable_account': 317 | if severity + config_reliability > 10: 318 | actions_to_be_executed += 1 319 | logger.info("GDPatrol: Executing action {}".format(action)) 320 | result = disable_account(username) 321 | successful_actions += int(result) 322 | elif action == 'disable_ec2_access': 323 | if severity + config_reliability > 10: 324 | actions_to_be_executed += 1 325 | logger.info("GDPatrol: Executing action {}".format(action)) 326 | result = disable_ec2_access(username) 327 | successful_actions += int(result) 328 | elif action == 'enable_ec2_access': 329 | if severity + config_reliability > 10: 330 | actions_to_be_executed += 1 331 | logger.info("GDPatrol: Executing action {}".format(action)) 332 | result = enable_ec2_access(username) 333 | successful_actions += int(result) 334 | elif action == 'disable_sg_access': 335 | if severity + config_reliability > 10: 336 | actions_to_be_executed += 1 337 | logger.info("GDPatrol: Executing action {}".format(action)) 338 | result = disable_sg_access(username) 339 | successful_actions += int(result) 340 | elif action == 'enable_sg_access': 341 | if severity + config_reliability > 10: 342 | actions_to_be_executed += 1 343 | logger.info("GDPatrol: Executing action {}".format(action)) 344 | result = enable_sg_access(username) 345 | successful_actions += int(result) 346 | elif action == 'asg_detach_instance': 347 | if severity + config_reliability > 10: 348 | actions_to_be_executed += 1 349 | logger.info("GDPatrol: Executing action {}".format(action)) 350 | result = asg_detach_instance(instance_id) 351 | successful_actions += int(result) 352 | logger.info("GDPatrol: Total actions: {} - Actions to be executed: {} - Successful Actions: {} - Finding ID: {} - Finding Type: {}".format( 353 | total_config_actions, actions_to_be_executed, successful_actions, finding_id, finding_type)) 354 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Antonio Sorrentino 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GDPatrol 2 | 3 | A Serverless Security Orchestration Automation and Response (SOAR) Framework for AWS GuardDuty. 4 | The GDPatrol Lambda function receives the GuardDuty findings through the CloudWatch Event Rule and executes 5 | the appropriate actions to mitigate the threats according to their types and severity. 6 | The deployment script will enable GuardDuty and deploy the GDPatrol Lambda function in all 7 | supported regions. 8 | 9 | Supported actions: 10 | 11 | * blacklist_ip(at the VPC level, using a Network ACL) 12 | * whitelist_ip 13 | * block_domain 14 | * quarantine_instance (deny all traffic ingress and egress to the EC2 instance) 15 | * snapshot_instance 16 | * disable_account (disable every action for a particular account) 17 | * disable_ec2_access 18 | * enable_ec2_access 19 | * disable_sg_access (Disable Security Group Access) 20 | * enable_sg_access 21 | * asg_detach_instance (detach instance from an auto scaling group) 22 | 23 | 24 | The actions to be executed are configured in the config.json file: 25 | ``` 26 | { 27 | "type": "Backdoor:EC2/C&CActivity.B!DNS", 28 | "actions": ["block_domain", "asg_detach_instance", "quarantine_instance", "snapshot_instance"], 29 | "reliability": 5 30 | }, 31 | ``` 32 | 33 | ## Getting Started 34 | 35 | ### Prerequisites 36 | 37 | * Python 3.6 (should be compatible with 2.7 as well but I didn't test it) 38 | * Boto3 39 | 40 | ### Installing 41 | Clone the project and just run the deployment file: 42 | ``` 43 | python3 deploy.py 44 | ``` 45 | The deployment script makes the following calls, make sure your account has the appropriate permissions: 46 | ``` 47 | IAM: 48 | List Roles, Delete Role Policy, Delete Role, Create Role, Put Role Policy 49 | 50 | Lambda: 51 | List Functions, Delete Function, Create Function, Add Permission 52 | 53 | CloudWatch Events: 54 | List Rules, List Targets By Rule, Remove Targets, Delete Rule, Put Rule, Put Targets 55 | 56 | GuardDuty: 57 | List Detectors, Create Detector, Update Detector 58 | ``` 59 | 60 | ## Configuration 61 | 62 | You can easily create your own playbooks by just adding or removing the actions and changing the reliability in the config.json 63 | for the desired finding type. 64 | 65 | By default, all findings are assigned a reliability value of 5: the reliability is then added to the "severity" value 66 | found in the finding JSON, and the actions are only executed if the sum of the two values is higher than 10. 67 | 68 | This ensures that, by default, only the playbooks for the GuardDuty findings with a severity of 6 or higher will be executed, while 69 | providing a way to effectively yet simply modify 70 | the behavior by modifying the reliability value of the config file. 71 | 72 | After any change to the config file locally, run deploy.py again and the script will recreate the Lambda function with 73 | the updated config.json file. 74 | The GuardDuty findings types are documented [here](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types.html). 75 | 76 | ## Authors 77 | 78 | * **Antonio Sorrentino** - [https://siemdetection.com](https://siemdetection.com) 79 | 80 | ## License 81 | 82 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 83 | 84 | ## Acknowledgments 85 | 86 | * Most of the actions code was adapted from the AWS Phantom app published by Booz Allen Hamilton. 87 | 88 | 89 | **Note:** By enabling GuardDuty, you might incur in additional costs. However, since the service is 90 | billed per log consumption usage, the cost should be irrelevant for the regions you're not actively using, 91 | so there's no reason to leave it off as you will want to monitor unused regions as well. See GuardDuty pricing 92 | for more details. -------------------------------------------------------------------------------- /deploy.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | from shutil import make_archive 3 | from os import remove 4 | from random import randrange 5 | 6 | 7 | def run(): 8 | REGIONS = ['us-east-2', 'us-east-1', 'us-west-1', 'us-west-2', 'sa-east-1', 'eu-west-1', 'eu-west-2', 9 | 'eu-west-3', 'eu-central-1', 'ca-central-1', 'ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 10 | 'ap-southeast-2', 'ap-south-1'] 11 | OUTPUT_FILENAME = 'GDPatrol' 12 | with open('role_policy.json', 'r') as rp: 13 | assume_role_policy = rp.read() 14 | zipped = make_archive(OUTPUT_FILENAME, 'zip', root_dir='GDPatrol') 15 | 16 | with open('lambda_policy.json', 'r') as lp: 17 | lambda_policy = lp.read() 18 | 19 | iam = boto3.client('iam') 20 | # delete the role if it already exists so it can be deployed with 21 | # the latest configuration 22 | roles = iam.list_roles()['Roles'] 23 | for role in roles: 24 | if role['RoleName'] == 'GDPatrolRole': 25 | iam.delete_role_policy(RoleName='GDPatrolRole', 26 | PolicyName='GDPatrol_lambda_policy') 27 | iam.delete_role(RoleName='GDPatrolRole') 28 | 29 | created_role = iam.create_role(RoleName='GDPatrolRole', 30 | AssumeRolePolicyDocument=assume_role_policy) 31 | lambda_role_arn = created_role['Role']['Arn'] 32 | 33 | iam.put_role_policy(RoleName='GDPatrolRole', 34 | PolicyName='GDPatrol_lambda_policy', 35 | PolicyDocument=lambda_policy) 36 | 37 | for region in REGIONS: 38 | 39 | lmb = boto3.client('lambda', region_name=region) 40 | cw_events = boto3.client('events', region_name=region) 41 | gd = boto3.client('guardduty', region_name=region) 42 | if not gd.list_detectors()['DetectorIds']: 43 | created_detector = gd.create_detector(Enable=True) 44 | print("Created GuardDuty detector: {}".format(created_detector['DetectorId'])) 45 | else: 46 | gd.update_detector(DetectorId=gd.list_detectors()['DetectorIds'][0], Enable=True) 47 | print("Detector already exists: {}".format(gd.list_detectors()['DetectorIds'][0])) 48 | 49 | try: 50 | lmb.get_function(FunctionName='GDPatrol') 51 | lmb.delete_function(FunctionName='GDPatrol') 52 | except: 53 | pass 54 | lambda_response = lmb.create_function(FunctionName='GDPatrol', 55 | Runtime='python3.6', 56 | Role=lambda_role_arn, 57 | Handler='lambda_function.lambda_handler', 58 | Code={'ZipFile': open(zipped, 'rb').read()}, 59 | Timeout=300, MemorySize=128) 60 | target_arn = lambda_response['FunctionArn'] 61 | target_id = 'Id' + str(randrange(10 ** 11, 10 ** 12)) 62 | 63 | 64 | # Remove targets and delete the CloudWatch rule before recreating it 65 | rules = cw_events.list_rules(NamePrefix='GDPatrol')['Rules'] 66 | for rule in rules: 67 | if rule['Name'] == 'GDPatrol': 68 | targets = cw_events.list_targets_by_rule(Rule=rule['Name'])['Targets'] 69 | for target in targets: 70 | cw_events.remove_targets(Rule=rule['Name'], Ids=[target['Id']]) 71 | cw_events.delete_rule(Name='GDPatrol') 72 | created_rule = cw_events.put_rule(Name='GDPatrol', 73 | EventPattern='{"source":["aws.guardduty"],"detail-type":["GuardDuty Finding"]}') 74 | cw_events.put_targets(Rule='GDPatrol', 75 | Targets=[{'Id': target_id, 'Arn': target_arn, 'InputPath': '$.detail'}]) 76 | 77 | # We are adding the trigger to the Lambda function so that it will be invoked every time a finding is sent over 78 | statement_id = str(randrange(10 ** 11, 10 ** 12)) 79 | lmb.add_permission( 80 | FunctionName=lambda_response['FunctionName'], 81 | StatementId=statement_id, 82 | Action='lambda:InvokeFunction', 83 | Principal='events.amazonaws.com', 84 | SourceArn=created_rule['RuleArn'] 85 | ) 86 | print("Successfully deployed the GDPatrol lambda function in region {}.".format(str(region))) 87 | remove(zipped) 88 | if __name__ == '__main__': 89 | run() 90 | -------------------------------------------------------------------------------- /lambda_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "ec2:RevokeSecurityGroupEgress", 8 | "ec2:CreateSnapshot", 9 | "iam:PutUserPolicy", 10 | "iam:DeleteUserPolicy", 11 | "ec2:DeleteNetworkAclEntry", 12 | "autoscaling:DetachInstances" 13 | ], 14 | "Resource": [ 15 | "arn:aws:ec2:*::snapshot/*", 16 | "arn:aws:ec2:*:*:volume/*", 17 | "arn:aws:ec2:*:*:security-group/*", 18 | "arn:aws:ec2:*:*:network-acl/*", 19 | "arn:aws:iam::*:user/*", 20 | "arn:aws:autoscaling:*:*:autoScalingGroup:*:autoScalingGroupName/*" 21 | ] 22 | }, 23 | { 24 | "Effect": "Allow", 25 | "Action": [ 26 | "autoscaling:DescribeAutoScalingInstances", 27 | "ec2:DescribeInstances", 28 | "ec2:CreateSecurityGroup", 29 | "ec2:ModifyInstanceAttribute", 30 | "ec2:CreateNetworkAclEntry", 31 | "ec2:DescribeNetworkAcls" 32 | ], 33 | "Resource": "*" 34 | }, 35 | { 36 | "Effect": "Allow", 37 | "Action": [ 38 | "logs:CreateLogGroup", 39 | "logs:CreateLogStream", 40 | "logs:PutLogEvents", 41 | "logs:DescribeLogStreams" 42 | ], 43 | "Resource": [ 44 | "arn:aws:logs:*:*:*" 45 | ] 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /role_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "Service": "lambda.amazonaws.com" 8 | }, 9 | "Action": "sts:AssumeRole" 10 | } 11 | ] 12 | } --------------------------------------------------------------------------------