def deleteGrant(self, principal, principal_type, perms): acl = [] for g in self.getGrants(): if g['Grantee'][ 'Type'] == principal_type: # matched on type at least if principal_type == "CanonicalUser": if g['Grantee']['ID'] == principal and g[ 'Permission'] in perms: # we found the offending permission, skip adding to new ACL continue elif principal_type == "Group": if g['Grantee']['URI'] == principal and g[ 'Permission'] in perms: # we found it, skip to prevent it being added continue else: # not aware of any other principal types, but best not to assume KLog.log( "krampus cannot modify ACEs for principal type: %s" % principal_type, "critical") return False # we maintain a new list to pass to the BucketAcl.put method # if we made it here, then we want to keep the ACE 'g' if g not in acl: acl.append(g) # it's over, update the ACL on AWS' side return self.bucket.put(AccessControlPolicy={ "Grants": acl, "Owner": self.bucket.owner }) # alternate remediation could be changing owner
def getSession(self): if self.session: return self.session else: KLog.log("odd, getSession failed while retrieving existing object", "warning") raise Warning("could not get a session object")
def getTasks(self, key): # in case we're dealing with multiple files for some reason, save current ref self.key = key try: # we'll actually want to save this for later to rebuild task list self.json_data = json.load(self.bucket.Object(key).get()['Body']) except ClientError as e: print "[!] failed to download tasks file: {0}".format(str(e)) KLog.log("Failed to download tasks file: {0}".format(str(e)), "critical") exit() for job in self.json_data['tasks']: # resolve the arn arn_obj = KTask.ARN(job[KEYS['arn']]) obj_service = arn_obj.service obj_account_id = arn_obj.account_id obj_resource_type = arn_obj.resource_type # Skip task if AWS IAM Managed Policy if obj_account_id == 'aws' and obj_service == 'iam' and obj_resource_type == 'policy': KLog.log( "Can't action AWS managed policy: {0}, will not be retried" .format(job[KEYS['arn']], "warn")) continue # Skip task if action_time is in the future or task is in whitelist if job[KEYS['action_time']] >= time.time(): KLog.log( "deferring job of type: {0}, not time to action".format( obj_service), "info") self.deferred_tasks.append(job) continue elif job[KEYS['arn']] in self.whitelist: KLog.log( "can't action whitelisted object: {0}, will not be retried" .format(job[KEYS['arn']], "critical")) continue # Collect params if we can classify and instantiate opts = {} for k in job: if k in KEYS.keys(): # collect valid ones opts[k] = job[KEYS[k]] # Add the ARN object and role name opts['arn'] = arn_obj opts['krampus_role'] = self.krampus_role # task obj if/else series determines how the additional args outside action etc used t = KTask.Task(opts) if obj_service not in SERVICES: KLog.log( "Got unrecognized AWS object type: {0}".format( obj_service), "warn") continue # don't append a non-existant task brah # add it to the list of things to action on # save json representation for convenience t.as_json = job self.tasks.append(t)
def disable(self, cidr_ip, from_port, to_port, proto, direction="ingress"): if not self.group: return None # will cause this invalid group's job to dequeue if direction == "ingress": return self.group.revoke_ingress(CidrIp=cidr_ip, FromPort=from_port, ToPort=to_port, GroupName=self.group.group_name, IpProtocol=proto) # though the docs say this call *should* be identical to the above, it doesn't work # so we have to take some extra steps here unfortunately elif direction == "egress": for rule in self.group.ip_permissions_egress: if rule['FromPort'] == from_port and rule[ 'ToPort'] == to_port and rule[ 'IpProtocol'] == proto and self.hasRange( rule['IpRanges'], cidr_ip): # good enough for me, remove it from the list self.group.revoke_egress(IpPermissions=[rule]) # update the permissions return self.group.ip_permissions_egress else: KLog.log( "rule direction must be (in|e)gress, got {0}".format( direction), "info")
def __init__(self, bucket_name, sess, region="us-east-1"): try: self.conn = sess.resource("s3", region_name=region) except: KLog.log("issue connecting to AWS", "critical") exit("[!] issue connecting to AWS") # set it - as far as krampus is concerned the acls are the bucket self.bucket = self.conn.BucketAcl(bucket_name)
def kill(self): try: return self.instance.terminate() except ClientError: KLog.log( "instance {0} already dead or invalid, will be dequeued". format(self.instance.instance_id), "info") return None # colloquial dequeue
def disable(self): try: return self.instance.stop() except ClientError: KLog.log( "instance %s already stopped or invalid, will be dequeued" % self.instance.instance_id, "info") return None # colloquial dequeue
def __init__(self, instance_name, region, sess): try: self.conn = sess.client('rds', region_name=region) except: KLog.log("issue connecting to AWS", "critical") exit("[!] issue connecting to AWS") # set it self.name = instance_name self.disable_groups = ['sg-c6d41cae']
def __init__(self, volume_id, region, sess): try: self.conn = sess.resource("ec2", region_name=region) except Exception as e: KLog.log("issue connecting to AWS %s" % str(e), "critical") exit("[!] issue connecting to AWS: %s" % str(e)) # get volume reference self.volume = self.conn.Volume(volume_id) self.region = region # save raw sess in case of instance actions self.sess = sess
def __init__(self, func_name, region, sess): try: self.conn = sess.client("lambda", region_name=region) except Exception as e: KLog.log("issue connecting to AWS %s" % str(e), "critical") exit("[!] issue connecting to AWS: %s" % str(e)) # get volume reference self.func = func_name self.region = region # save raw sess in case of instance actions self.sess = sess
def __init__(self, group_id, region, sess): try: self.conn = sess.resource("ec2", region_name=region) # set it self.group = self.conn.SecurityGroup(group_id) self.group.load() # bails us out if the group is bogus except ClientError: # typical when group id does not exist KLog.log("security group %s does not exist" % group_id, "warning") self.group = False except: KLog.log("issue connecting to AWS", "critical") exit("[!] issue connecting to AWS")
def kill(self): try: # low level call, just pass the resp back return self.conn.delete_function(FunctionName=self.func) except Exception as e: if str(e).find("ResourceNotFoundException") is not -1: KLog.log("could not find function '%s', dequeueing task" % self.func) else: KLog.log( "could not delete function '%s', unknown error: %s" % str(e), "critical") return None
def __init__(self, instance_id, region, sess): try: self.conn = sess.resource("ec2", region) except Exception as e: KLog.log("issue connecting to AWS %s" % str(e), "critical") exit("[!] issue connecting to AWS: %s" % str(e)) # set it self.instance = self.getInstanceByID(instance_id) # verify the instance try: self.instance.load() except: KLog.log("provided instance id %s appears invalid" % instance_id, "warn")
def responseHandler(self, resp): if not resp: return "success" if type(resp) is not list: resp = [resp] success_count = 0 for r in resp: # some things need multiple calls code = r['ResponseMetadata']['HTTPStatusCode'] if code >= 200 and code < 400: # that's it all right success_count += 1 KLog.log("%s aws success response!" % self.job_params['arn'].arn_str, "info") else: KLog.log("%s aws failed response: %s" % (self.job_params['arn'].arn_str, r), "warn") if success_count == 0: # complete failure raise Warning("all calls for task %s failed, please check logs" % self.job_params['arn'].arn_str) elif success_count == len(resp): # complete success KLog.log( "the object '%s' of type '%s' was %sed on %d" % (self.job_params['arn'].arn_str, self.job_params['arn'].service, self.action, time.time()) ) else: # something... else KLog.log("at least one call failed for %s, please check logs" % self.job_params['arn'].arn_str, "critical")
def rebuildTaskList(self): # in python dicts are immutable, so we need to build a new obj # first add deferred tasks updated_json = {"tasks": self.deferred_tasks} # then add whatever else is in that obj, skipping tasks key of course for k in self.json_data: if k == "tasks": continue else: updated_json[k] = self.json_data[k] # convert from dict so aws doesn't complain updated_json = json.dumps(updated_json) # put it to the bucket resp = self.bucket.Object(self.key).put(Body=updated_json) KLog.log("done updating tasks list: {0}".format(self.key), "info") return resp
def __init__(self, account_id, role_name): # before anything see if we already have this session if account_id in sessions: self.session = sessions[account_id] return None # otherwise lets get a session going sts = boto3.client("sts") arn_str = "arn:aws:iam::%s:role/%s" % (account_id, role_name) try: sess = sts.assume_role(RoleArn=arn_str, RoleSessionName=account_id) except ClientError as e: # prob does not have perms to assume print "[!] issue assuming role %s: %s" % (arn_str, str(e)) KLog.log("issue assuming role {0}: {1}".format(arn_str, str(e)), "critical") return None # if that works lets save the session sessions[account_id] = boto3.Session( aws_access_key_id=sess['Credentials']['AccessKeyId'], aws_secret_access_key=sess['Credentials']['SecretAccessKey'], aws_session_token=sess['Credentials']['SessionToken'] ) self.session = sessions[account_id]
def kill(self): # no exceptions etc, let them bubble up to krampus.py so task re-queues resp = None try: resp = self.volume.delete() except Exception as e: # doesn't look like boto has one for VolInUse error # get the attached instance and detach from it if str(e).find("VolumeInUse" ) is not -1: # we use the exception to get vol id m = search("(i-[a-f0-9]+)$", str(e)) instance = m.group(0) if EC2(instance, self.region, self.sess).status()['Name'] == "stopped": # in a state we can work with, detach self.volume.detach_from_instance(InstanceId=instance) KLog.log( "volume %s detached, deleting now..." % self.volume.id, "info") resp = self.volume.delete() else: # shut down, we can detach and delete next run EC2(instance, self.region, self.sess).disable() msg = "parent instance of {0} stopped, volume will be deleted next run".format( self.volume.id) KLog.log(msg, "warn") raise Exception(msg) # requeues job elif str(e).find("NotFound") is not -1: KLog.log( "looks like volume {0} was already deleted, dequeueing job" .format(self.volume.id), "warn") return resp
def responseHandler(self, resp): if not resp: return "success" if type(resp) is not list: resp = [resp] success_count = 0 for r in resp: # some things need multiple calls code = r['ResponseMetadata']['HTTPStatusCode'] if code >= 200 and code < 400: # that's it all right success_count += 1 KLog.log( "{0} AWS success response!".format( self.job_params['arn'].arn_str), "info") else: KLog.log( "{0} AWS failed response: {1}".format( self.job_params['arn'].arn_str, r), "warn") if success_count == 0: # complete failure KLog.log( "All calls for task {0} failed, please check logs {1}". format(self.job_params['arn'].arn_str), "critical", self.job_params['arn'].account_id) elif success_count == len(resp): # complete success KLog.log( "The object '{0}' of type '{1}' was {2}ed on {3}".format( (self.job_params['arn'].arn_str, self.job_params['arn'].service, self.action, time.time())), "critical", self.job_params['arn'].account_id) else: # something... else KLog.log( "At least one call failed for {0}, please check logs". format(self.job_params['arn'].arn_str), "critical", self.job_params['arn'].account_id)
def getTasks(self, key): # in case we're dealing with multiple files for some reason, save current ref self.key = key try: # we'll actually want to save this for later to rebuild task list self.json_data = json.load(self.bucket.Object(key).get()['Body']) except ClientError as e: KLog.log("failed to download tasks file: %s" % str(e), "critical") exit() for job in self.json_data['tasks']: # resolve the arn arn_obj = KTask.ARN(job[KEYS['arn']]) obj_type = arn_obj.service # first, is this something we should worry about right now? if job[KEYS['action_time']] >= time.time(): KLog.log("skipping job of type: %s" % obj_type, "info") self.deferred_tasks.append(job) continue # over it elif job[KEYS['arn']] in self.whitelist: KLog.log("can't action whitelisted object: %s, will not be retried" % job[KEYS['arn']], "critical") continue # otherwise we can classify and instantiate # but first, collect all the params opts = {} for k in job: if k in KEYS.keys(): # collect valid ones opts[k] = job[KEYS[k]] # add the arn object too opts['arn'] = arn_obj # pass role name opts['krampus_role'] = self.krampus_role # task obj if/else series determines how the additional args outside action etc used t = KTask.Task(opts) if (obj_type not in SERVICES): KLog.log("got unrecognized aws object type: " + obj_type, "warn") continue # don't append a non-existant task brah # add it to the list of things to action on # save json representation for convenience t.as_json = job self.tasks.append(t)
def disable(self): KLog.log( "no disable action for lambda function '%s', will delete instead" % self.func, "warning") return self.kill()
def complete(self): # now we go through and see what type of action and object and call the appropriate kinder methods arn_obj = self.job_params['arn'] obj_service = arn_obj.service obj_account_id = arn_obj.account_id obj_resource = arn_obj.resource obj_resource_type = arn_obj.resource_type # ebsvolume job if obj_service == "ec2" and obj_resource_type == "volume": ebs_volume = obj_resource if self.action == "kill": # only ebs action right now KLog.log( "Deleting EBS volume with ID: {0}".format(ebs_volume), "info") resp = ebs.EBS(ebs_volume, self.aws_region, self.session).kill() elif self.action == "disable": KLog.log( "'disable' action makes no sense for EBS volume: {0}, deleting instead" .format(ebs_volume), "warn") resp = ebs.EBS(ebs_volume, self.aws_region, self.session).kill() else: KLog.log( "Did not understand action '{0}' for EBS job type on {1}" .format(self.action, ebs_volume), "critical", obj_account_id, ) resp = None self.responseHandler(resp) # security_group job elif obj_service == "ec2" and obj_resource_type == "security-group": security_group_id = obj_resource if self.action == "kill": KLog.log("Deleting security_group: {0}".format( security_group_id)) resp = security_group.SecurityGroup( security_group_id, self.aws_region, self.session).kill() elif self.action == "disable": KLog.log("Pulling rule on: {0}".format(security_group_id)) resp = security_group.SecurityGroup( security_group_id, self.aws_region, self.session).disable(self.job_params['cidr_range'], self.job_params['from_port'], self.job_params['to_port'], self.job_params['proto']) else: KLog.log( "Did not understand action '{0}' for security-group job type on {1}" .format(self.action, security_group_id), "critical", obj_account_id) resp = None self.responseHandler(resp) # ec2instance job elif obj_service == "ec2" and obj_resource_type == "instance": ec2_instance = obj_resource if self.action == "disable": KLog.log( "Disabling EC2 instance: {0}".format(ec2_instance)) resp = ec2.EC2(ec2_instance, self.aws_region, self.session).disable() elif self.action == "kill": KLog.log("Deleting EC2 instance: {0}".format(ec2_instance)) resp = ec2.EC2(ec2_instance, self.aws_region, self.session).kill() else: KLog.log( "Did not understand action '{0}' for EC2 job on {1}". format(self.action, ec2_instance), "critical", obj_account_id) resp = None self.responseHandler(resp) # s3 job elif obj_service == "s3": bucket = obj_resource remove_all = False try: s3_permissions = self.job_params[KEYS['s3_permission']] s3_principal = self.job_params[KEYS['s3_principal']] s3_principal_type = "Group" if self.job_params[KEYS[ 's3_principal']].find("http") > -1 else "CanonicalUser" except KeyError: KLog.log( "S3 job {0} was not passed with principal/permissions - all perms will be removed" .format(bucket), "warn") remove_all = True if self.action == "disable" and not remove_all: KLog.log( "Deleting permissions '{0}' for principal '{1}' on bucket '{2}'" .format(", ".join(map(str, s3_permissions)), s3_principal, bucket)) resp = s3.S3(bucket, self.session).deleteGrant( s3_principal, s3_principal_type, s3_permissions) elif self.action == "disable" and remove_all: KLog.log("removing all permissions on '%s'" % bucket, "info") resp = s3.S3(bucket, self.session).deleteAllGrants() else: KLog.log( "Did not understand action '{0}' for S3 job type on {1}" .format(self.action, bucket), "critical", obj_account_id) resp = None self.responseHandler(resp) # iam job elif obj_service == "iam": iam_obj = obj_resource iam_obj_type = obj_resource_type if self.action == "kill": KLog.log("Deleting IAM Object: {0}".format(iam_obj)) resp = iam.IAM(iam_obj, iam_obj_type, self.session, self.aws_region).kill() elif self.action == "disable": KLog.log("Disabling IAM Object: {0}".format(iam_obj)) resp = iam.IAM(iam_obj, iam_obj_type, self.session, self.aws_region).disable() else: KLog.log( "Did not understand action '{0}' for IAM job type on {1}" .format(self.action, iam_obj), "critical", obj_account_id) resp = None self.responseHandler(resp) # rds job elif obj_service == "rds": rds_instance = obj_resource if self.action == "disable": KLog.log( "Disabling RDS instance: {0}".format(rds_instance)) resp = rds.RDS(rds_instance, self.aws_region, self.session).disable() elif self.action == "kill": KLog.log( "'kill' action too dangerous for RDS job: {0}, will be dequeued" .format(rds_instance), "critical") resp = None # will cause responseHandler to dequeue this job else: KLog.log( "Did not understand action '{0}' for RDS job type on {1}" .format(self.action, rds_instance), "critical", obj_account_id) resp = None self.responseHandler(resp) # lambda job elif obj_service == "lambda": func_name = obj_resource if self.action == "disable": KLog.log( "Lambda job '{0}' has no disable action, will kill instead" .format(arn_obj.arn_str)) resp = lambda_funcs.Lambda(func_name, self.aws_region, self.session).kill() elif self.action == "kill": KLog.log("Deleting Lambda function '{0}'".format( arn_obj.arn_str)) resp = lambda_funcs.Lambda(func_name, self.aws_region, self.session).kill() else: KLog.log( "Did not understand action '{0}' for Lambda job '{1}'". format(self.action, func_name), "critical", obj_account_id) resp = None # send it back self.responseHandler(resp)
def complete(self): # now we go through and see what type of action and object and call the appropriate kinder methods arn_obj = self.job_params['arn'] obj_type = arn_obj.service # this is an ebs volume job if obj_type == "ec2" and arn_obj.resource.find("volume") is not -1: ebs_volume = arn_obj.resource.split("/")[1] if self.action == "kill": # only ebs action right now KLog.log("deleting ebs volume with id: %s" % ebs_volume, "info") resp = ebs.EBS(ebs_volume, self.aws_region, self.session).kill() elif self.action == "disable": KLog.log("'disable' action makes no sense for EBS volume: %s, will be deleted instead" % ebs_volume, "warn") resp = ebs.EBS(ebs_volume, self.aws_region, self.session).kill() else: KLog.log("did not understand action '%s' for ebs job type on %s" % (self.action, ebs_volume), "critical") resp = None self.responseHandler(resp) # security group job elif obj_type == "ec2" and arn_obj.resource.find("security-group") is not -1: security_group_id = arn_obj.resource.split("/")[1] if self.action == "kill": KLog.log("deleting security group: %s" % security_group_id) resp = security_group.SecurityGroup(security_group_id, self.aws_region, self.session).kill() elif self.action == "disable": KLog.log("pulling rule on: %s" % security_group_id) resp = security_group.SecurityGroup(security_group_id, self.aws_region, self.session).disable( self.job_params['cidr_range'], self.job_params['from_port'], self.job_params['to_port'], self.job_params['proto'] ) else: KLog.log("did not understand action '%s' for secgroup job type on %s" % (self.action, security_group_id), "critical") resp = None self.responseHandler(resp) # standard ec2 instance job elif obj_type == "ec2": ec2_instance = arn_obj.resource.split("/")[1] if self.action == "disable": KLog.log("disabling ec2 instance: %s" % ec2_instance) resp = ec2.EC2(ec2_instance, self.aws_region, self.session).disable() elif self.action == "kill": KLog.log("deleting ec2 instance: %s" % ec2_instance) resp = ec2.EC2(ec2_instance, self.aws_region, self.session).kill() else: KLog.log("did not understand action '%s' for ec2 job type on %s" % (self.action, ec2_instance), "critical") resp = None self.responseHandler(resp) # s3 job elif obj_type == "s3": bucket = arn_obj.resource remove_all = False try: s3_permissions = self.job_params[KEYS['s3_permission']] s3_principal = self.job_params[KEYS['s3_principal']] s3_principal_type = "Group" if self.job_params[KEYS['s3_principal']].find("http") > -1 else "CanonicalUser" except KeyError: KLog.log("s3 job %s was not passed with principal and permission info - all perms will be removed" % bucket, "warn") remove_all = True if self.action == "disable" and not remove_all: KLog.log( "deleting permissions '%s' for principal '%s' on bucket '%s'" % (", ".join(map(str, s3_permissions)), s3_principal, bucket) ) resp = s3.S3(bucket, self.session).deleteGrant(s3_principal, s3_principal_type, s3_permissions) elif self.action == "disable" and remove_all: KLog.log("removing all permissions on '%s'" % bucket, "info") resp = s3.S3(bucket, self.session).deleteAllGrants() else: KLog.log("did not understand action '%s' for s3 job type on %s" % (self.action, bucket), "critical") resp = None self.responseHandler(resp) # iam job elif obj_type == "iam": iam_obj = arn_obj.resource if self.action == "kill": KLog.log("deleting iam object: %s" % iam_obj) resp = iam.IAM(iam_obj, self.session, self.aws_region).kill() elif self.action == "disable": KLog.log("disabling iam object: %s" % iam_obj) resp = iam.IAM(iam_obj, self.session, self.aws_region).disable() else: KLog.log("did not understand action '%s' for iam job type on %s" % (self.action, iam_obj), "critical") resp = None self.responseHandler(resp) # rds job elif obj_type == "rds": rds_instance = arn_obj.resource if self.action == "disable": KLog.log("disabling rds instance: %s" % rds_instance) resp = rds.RDS(rds_instance, self.aws_region, self.session).disable() elif self.action == "kill": KLog.log("'kill' action too dangerous for rds job: %s, will be dequeued" % rds_instance, "critical") resp = None # will cause responseHandler to dequeue this job else: KLog.log("did not understand action '%s' for rds job type on %s" % (self.action, rds_instance), "critical") resp = None self.responseHandler(resp) # lambda job elif obj_type == "lambda": func_name = arn_obj.resource KLog.log("deleting lambda function '%s'" % arn_obj.arn_str) if self.action == "disable": KLog.log("lambda job '%s' has no disable action, will kill instead" % arn_obj.arn_str, "critical") resp = lambda_funcs.Lambda(func_name, self.aws_region, self.session).kill() elif self.action == "kill": resp = lambda_funcs.Lambda(func_name, self.aws_region, self.session).kill() else: KLog.log("did not understand action '%s' for lambda job '%s'" % (self.action, func_name), "critical") resp = None # send it back self.responseHandler(resp)