def process_action(resource, action, action_issuer='unknown'): """Process an audit action for a resource, if possible Args: resource (:obj:`Resource`): A resource object to perform the action on action (`str`): Type of action to perform (`kill` or `stop`) action_issuer (`str`): The issuer of the action Returns: `ActionStatus` """ from cinq_collector_aws import AWSRegionCollector func_action = action_mapper[resource.resource_type][action] extra_info = {} action_status = ActionStatus.UNKNOWN if func_action: if action_mapper[resource.resource_type]['service_name'] == 'lambda': client = get_aws_session( AWSAccount.get( dbconfig.get('rds_collector_account', AWSRegionCollector.ns, ''))).client( 'lambda', dbconfig.get('rds_collector_region', AWSRegionCollector.ns, '')) else: client = get_aws_session(AWSAccount(resource.account)).client( action_mapper[resource.resource_type]['service_name'], region_name=resource.location) try: logger.info( f'Trying to {action} resource {resource.id} for account {resource.account.account_name} / region {resource.location}' ) action_status, extra_info = func_action(client, resource) if action_status == ActionStatus.SUCCEED: Enforcement.create(resource.account.account_id, resource.id, action, datetime.now(), extra_info) except Exception as ex: action_status = ActionStatus.FAILED logger.exception('Failed to apply action {} to {}: {}'.format( action, resource.id, ex)) finally: auditlog(event='{}.{}.{}.{}'.format(action_issuer, resource.resource_type, action, action_status), actor=action_issuer, data={ 'resource_id': resource.id, 'account_name': resource.account.account_name, 'location': resource.location, 'info': extra_info }) return action_status else: logger.error('Failed to apply action {} to {}: Not supported'.format( action, resource.id)) return ActionStatus.FAILED
def manage_policies(self, accounts): if not accounts: return self.git_policies = self.get_policies_from_git() self.manage_roles = self.dbconfig.get('manage_roles', self.ns, True) self.cfg_roles = self.dbconfig.get('roles', self.ns) self.aws_managed_policies = { policy['PolicyName']: policy for policy in self.get_policies_from_aws( get_aws_session(accounts[0]).client('iam'), 'AWS') } for account in accounts: try: if not account.ad_group_base: self.log.info( 'Account {} does not have AD Group Base set, skipping'. format(account.account_name)) continue # List all policies and roles from AWS, and generate a list of policies from Git sess = get_aws_session(account) iam = sess.client('iam') aws_roles = { role['RoleName']: role for role in self.get_roles(iam) } aws_policies = { policy['PolicyName']: policy for policy in self.get_policies_from_aws(iam) } account_policies = copy.deepcopy(self.git_policies['GLOBAL']) if account.account_name in self.git_policies: for role in self.git_policies[account.account_name]: account_policies.update( self.git_policies[account.account_name][role]) aws_policies.update( self.check_policies(account, account_policies, aws_policies)) self.check_roles(account, aws_policies, aws_roles) except Exception as exception: self.log.info( 'Unable to process account {}. Unhandled Exception {}'. format(account.account_name, exception))
def run(self, **kwargs): self.kwargs = kwargs try: key_id = self.kwargs['key_id'] if self.kwargs['data'].startswith('@'): path = self.kwargs['data'][1:] try: with open(path, 'rb') as fh: data = fh.read(-1) except Exception as ex: self.log.exception( 'Failed loading data from file: {}'.format(ex)) return else: data = kwargs['data'] session = get_local_aws_session() if session.get_credentials().method != 'iam-role': kms_account_name = app_config.kms_account_name if not kms_account_name: print( 'you must set the kms_account_name setting in your configuration file to the name of the ' 'account that is able to decrypt the user data') return acct = Account.get(kms_account_name) if not acct: print( 'You must add the {} account to the system for this to work' .format(kms_account_name)) return session = get_aws_session(acct) kms = session.client('kms', region_name=self.dbconfig.get( 'region', self.ns, 'us-west-2')) if kwargs['mode'] == 'encrypt': if not kwargs['key_id']: print( 'You must provide a key id to use for encryption to work' ) return compressed = zlib.compress( bytes(json.dumps(json.loads(data)), 'utf-8')) res = kms.encrypt(KeyId=key_id, Plaintext=compressed) self.output(res['CiphertextBlob']) else: res = kms.decrypt(CiphertextBlob=b64decode(data)) self.output( json.dumps(json.loads( str(zlib.decompress(res['Plaintext']), 'utf-8')), indent=4)) except Exception: self.log.exception( 'An error occured while doing userdata.py stuff')
def create_s3_bucket(cls, bucket_name, bucket_region, bucket_account, template): """Creates the S3 bucket on the account specified as the destination account for log files Args: bucket_name (`str`): Name of the S3 bucket bucket_region (`str`): AWS Region for the bucket bucket_account (:obj:`Account`): Account to create the S3 bucket in template (:obj:`Template`): Jinja2 Template object for the bucket policy Returns: `None` """ s3 = get_aws_session(bucket_account).client('s3', region_name=bucket_region) # Check to see if the bucket already exists and if we have access to it try: s3.head_bucket(Bucket=bucket_name) except ClientError as ex: status_code = ex.response['ResponseMetadata']['HTTPStatusCode'] # Bucket exists and we do not have access if status_code == 403: raise Exception( 'Bucket {} already exists but we do not have access to it and so cannot continue' .format(bucket_name)) # Bucket does not exist, lets create one elif status_code == 404: try: s3.create_bucket(Bucket=bucket_name, CreateBucketConfiguration={ 'LocationConstraint': bucket_region }) auditlog(event='cloudtrail.create_s3_bucket', actor=cls.ns, data={ 'account': bucket_account.account_name, 'bucket_region': bucket_region, 'bucket_name': bucket_name }) except Exception: raise Exception( 'An error occured while trying to create the bucket, cannot continue' ) try: bucket_acl = template.render( bucket_name=bucket_name, account_id=bucket_account.account_number) s3.put_bucket_policy(Bucket=bucket_name, Policy=bucket_acl) except Exception as ex: raise Warning( 'An error occurred while setting bucket policy: {}'.format(ex))
def check_policies(self, account, account_policies, aws_policies): """Iterate through the policies of a specific account and create or update the policy if its missing or does not match the policy documents from Git. Returns a dict of all the policies added to the account (does not include updated policies) Args: account (:obj:`Account`): Account to check policies for account_policies (`dict` of `str`: `dict`): A dictionary containing all the policies for the specific account aws_policies (`dict` of `str`: `dict`): A dictionary containing the non-AWS managed policies on the account Returns: :obj:`dict` of `str`: `str` """ self.log.debug('Fetching policies for {}'.format(account.account_name)) sess = get_aws_session(account) iam = sess.client('iam') added = {} for policyName, account_policy in account_policies.items(): # policies pulled from github a likely bytes and need to be converted if isinstance(account_policy, bytes): account_policy = account_policy.decode('utf-8') # Using re.sub instead of format since format breaks on the curly braces of json gitpol = json.loads( re.sub(r'{AD_Group}', account.ad_group_base or account.account_name, account_policy)) if policyName in aws_policies: pol = aws_policies[policyName] awspol = iam.get_policy_version( PolicyArn=pol['Arn'], VersionId=pol['DefaultVersionId'] )['PolicyVersion']['Document'] if awspol != gitpol: self.log.warn( 'IAM Policy {} on {} does not match Git policy documents, updating' .format(policyName, account.account_name)) self.create_policy(account, iam, json.dumps(gitpol, indent=4), policyName, arn=pol['Arn']) else: self.log.debug('IAM Policy {} on {} is up to date'.format( policyName, account.account_name)) else: self.log.warn('IAM Policy {} is missing on {}'.format( policyName, account.account_name)) response = self.create_policy(account, iam, json.dumps(gitpol), policyName) added[policyName] = response['Policy'] return added
def __init__(self, account): super().__init__() if type(account) == str: account = AWSAccount.get(account) if not isinstance(account, AWSAccount): raise InquisitorError('The AWS Collector only supports AWS Accounts, got {}'.format( account.__class__.__name__ )) self.account = account self.session = get_aws_session(self.account)
def process_action(resource, action, resource_type): """Process an audit action for a resource, if possible Args: resource (:obj:`Resource`): A resource object to perform the action on action (`str`): Type of action to perform (`kill` or `stop`) resource_type (`str`): Type of the resource Returns: `bool` - Returns the result from the action function """ func_action = action_mapper[resource_type][action] if func_action: session = get_aws_session(AWSAccount(resource.account)) client = session.client( action_mapper[resource_type]['service_name'], region_name=resource.location ) return func_action(client, resource) return False
def __init__(self, account, bucket_name, bucket_region, logger): self.account = account self.bucket_region = bucket_region self.bucket_name = bucket_name self.log = logger # Config settings self.global_ct_region = dbconfig.get('global_cloudtrail_region', self.ns, 'us-west-2') self.topic_name = dbconfig.get('sns_topic_name', self.ns, 'cloudtrail-log-notification') self.trail_name = dbconfig.get('trail_name', self.ns) sqs_queue_name = dbconfig.get('sqs_queue_name', self.ns) sqs_queue_region = dbconfig.get('sqs_queue_region', self.ns) sqs_account = AWSAccount.get(dbconfig.get('sqs_queue_account', self.ns)) self.sqs_queue = 'arn:aws:sqs:{}:{}:{}'.format( sqs_queue_region, sqs_account.account_number, sqs_queue_name) self.session = get_aws_session(account)
def run(self): """Main entry point for the auditor worker. Returns: `None` """ # Loop through all accounts that are marked as enabled accounts = list(AWSAccount.get_all(include_disabled=False).values()) for account in accounts: self.log.debug('Updating VPC Flow Logs for {}'.format(account)) self.session = get_aws_session(account) role_arn = self.confirm_iam_role(account) # region specific for aws_region in AWS_REGIONS: try: vpc_list = VPC.get_all(account, aws_region).values() need_vpc_flow_logs = [ x for x in vpc_list if x.vpc_flow_logs_status != 'ACTIVE' ] for vpc in need_vpc_flow_logs: if self.confirm_cw_log(account, aws_region, vpc.id): self.create_vpc_flow_logs(account, aws_region, vpc.id, role_arn) else: self.log.info( 'Failed to confirm log group for {}/{}'.format( account, aws_region)) except Exception: self.log.exception( 'Failed processing VPCs for {}/{}.'.format( account, aws_region)) db.session.commit()
def process_action(resource, action, action_issuer='unknown'): """Process an audit action for a resource, if possible Args: resource (:obj:`Resource`): A resource object to perform the action on action (`str`): Type of action to perform (`kill` or `stop`) action_issuer (`str`): The issuer of the action Returns: `ActionStatus` """ func_action = action_mapper[resource.resource_type][action] if func_action: client = get_aws_session(AWSAccount(resource.account)).client( action_mapper[resource.resource_type]['service_name'], region_name=resource.location ) try: action_status, metrics = func_action(client, resource) Enforcement.create(resource.account.account_name, resource.id, action, datetime.now(), metrics) except Exception as ex: action_status = ActionStatus.FAILED logger.error('Failed to apply action {} to {}: {}'.format(action, resource.id, ex)) finally: auditlog( event='{}.{}.{}.{}'.format(action_issuer, resource.resource_type, action, action_status), actor=action_issuer, data={ 'resource_id': resource.id, 'account_name': resource.account.account_name, 'location': resource.location } ) return action_status else: logger.error('Failed to apply action {} to {}: Not supported'.format(action, resource.id)) return ActionStatus.FAILED
def validate_sqs_policy(self, accounts): """Given a list of accounts, ensures that the SQS policy allows all the accounts to write to the queue Args: accounts (`list` of :obj:`Account`): List of accounts Returns: `None` """ sqs_queue_name = self.dbconfig.get('sqs_queue_name', self.ns) sqs_queue_region = self.dbconfig.get('sqs_queue_region', self.ns) sqs_account = AWSAccount.get( self.dbconfig.get('sqs_queue_account', self.ns)) session = get_aws_session(sqs_account) sqs = session.client('sqs', region_name=sqs_queue_region) sqs_queue_url = sqs.get_queue_url( QueueName=sqs_queue_name, QueueOwnerAWSAccountId=sqs_account.account_number) sqs_attribs = sqs.get_queue_attributes( QueueUrl=sqs_queue_url['QueueUrl'], AttributeNames=['Policy']) policy = json.loads(sqs_attribs['Attributes']['Policy']) for account in accounts: arn = 'arn:aws:sns:*:{}:{}'.format(account.account_number, sqs_queue_name) if arn not in policy['Statement'][0]['Condition'][ 'ForAnyValue:ArnEquals']['aws:SourceArn']: self.log.warning( 'SQS policy is missing condition for ARN {}'.format(arn)) policy['Statement'][0]['Condition']['ForAnyValue:ArnEquals'][ 'aws:SourceArn'].append(arn) sqs.set_queue_attributes(QueueUrl=sqs_queue_url['QueueUrl'], Attributes={'Policy': json.dumps(policy)})
def aws_get_client(client_type, region=CINQ_TEST_REGION): return get_aws_session().client(client_type, region)
def check_roles(self, account, aws_policies, aws_roles): """Iterate through the roles of a specific account and create or update the roles if they're missing or does not match the roles from Git. Args: account (:obj:`Account`): The account to check roles on aws_policies (:obj:`dict` of `str`: `dict`): A dictionary containing all the policies for the specific account aws_roles (:obj:`dict` of `str`: `dict`): A dictionary containing all the roles for the specific account Returns: `None` """ self.log.debug('Checking roles for {}'.format(account.account_name)) max_session_duration = self.dbconfig.get('role_timeout_in_hours', self.ns, 8) * 60 * 60 sess = get_aws_session(account) iam = sess.client('iam') # Build a list of default role policies and extra account specific role policies account_roles = copy.deepcopy(self.cfg_roles) if account.account_name in self.git_policies: for role in self.git_policies[account.account_name]: if role in account_roles: account_roles[role]['policies'] += list( self.git_policies[account.account_name][role].keys()) for role_name, data in list(account_roles.items()): if role_name not in aws_roles: iam.create_role(Path='/', RoleName=role_name, AssumeRolePolicyDocument=json.dumps( data['trust'], indent=4), MaxSessionDuration=max_session_duration) self.log.info('Created role {}/{}'.format( account.account_name, role_name)) else: try: if aws_roles[role_name][ 'MaxSessionDuration'] != max_session_duration: iam.update_role( RoleName=aws_roles[role_name]['RoleName'], MaxSessionDuration=max_session_duration) self.log.info( 'Adjusted MaxSessionDuration for role {} in account {} to {} seconds' .format(role_name, account.account_name, max_session_duration)) except ClientError: self.log.exception( 'Unable to adjust MaxSessionDuration for role {} in account {}' .format(role_name, account.account_name)) aws_role_policies = [ x['PolicyName'] for x in iam.list_attached_role_policies( RoleName=role_name)['AttachedPolicies'] ] aws_role_inline_policies = iam.list_role_policies( RoleName=role_name)['PolicyNames'] cfg_role_policies = data['policies'] missing_policies = list( set(cfg_role_policies) - set(aws_role_policies)) extra_policies = list( set(aws_role_policies) - set(cfg_role_policies)) if aws_role_inline_policies: self.log.info( 'IAM Role {} on {} has the following inline policies: {}'. format(role_name, account.account_name, ', '.join(aws_role_inline_policies))) if self.dbconfig.get('delete_inline_policies', self.ns, False) and self.manage_roles: for policy in aws_role_inline_policies: iam.delete_role_policy(RoleName=role_name, PolicyName=policy) auditlog( event='iam.check_roles.delete_inline_role_policy', actor=self.ns, data={ 'account': account.account_name, 'roleName': role_name, 'policy': policy }) if missing_policies: self.log.info( 'IAM Role {} on {} is missing the following policies: {}'. format(role_name, account.account_name, ', '.join(missing_policies))) if self.manage_roles: for policy in missing_policies: iam.attach_role_policy( RoleName=role_name, PolicyArn=aws_policies[policy]['Arn']) auditlog(event='iam.check_roles.attach_role_policy', actor=self.ns, data={ 'account': account.account_name, 'roleName': role_name, 'policyArn': aws_policies[policy]['Arn'] }) if extra_policies: self.log.info( 'IAM Role {} on {} has the following extra policies applied: {}' .format(role_name, account.account_name, ', '.join(extra_policies))) for policy in extra_policies: if policy in aws_policies: polArn = aws_policies[policy]['Arn'] elif policy in self.aws_managed_policies: polArn = self.aws_managed_policies[policy]['Arn'] else: polArn = None self.log.info( 'IAM Role {} on {} has an unknown policy attached: {}' .format(role_name, account.account_name, policy)) if self.manage_roles and polArn: iam.detach_role_policy(RoleName=role_name, PolicyArn=polArn) auditlog(event='iam.check_roles.detach_role_policy', actor=self.ns, data={ 'account': account.account_name, 'roleName': role_name, 'policyArn': polArn })
def update_rds_databases(self): """Update list of RDS Databases for the account / region Returns: `None` """ self.log.info('Updating RDS Databases for {} / {}'.format( self.account, self.region )) # All RDS resources are polled via a Lambda collector in a central account rds_collector_account = AWSAccount.get(self.rds_collector_account) rds_session = get_aws_session(rds_collector_account) # Existing RDS resources come from database existing_rds_dbs = RDSInstance.get_all(self.account, self.region) try: # Special session pinned to a single account for Lambda invocation so we # don't have to manage lambdas in every account & region lambda_client = rds_session.client('lambda', region_name=self.rds_collector_region) # The AWS Config Lambda will collect all the non-compliant resources for all regions # within the account input_payload = json.dumps({"account_id": self.account.account_number, "region": self.region, "role": self.rds_role, "config_rule_name": self.rds_config_rule_name }).encode('utf-8') response = lambda_client.invoke(FunctionName=self.rds_function_name, InvocationType='RequestResponse', Payload=input_payload ) response_payload = json.loads(response['Payload'].read().decode('utf-8')) if response_payload['success']: rds_dbs = response_payload['data'] if rds_dbs: for db_instance in rds_dbs: tags = {t['Key']: t['Value'] for t in db_instance['tags'] or {}} properties = { 'tags': tags, 'metrics': None, 'engine': db_instance['engine'], 'creation_date': db_instance['creation_date'] } if db_instance['resource_name'] in existing_rds_dbs: rds = existing_rds_dbs[db_instance['resource_name']] if rds.update(db_instance, properties): self.log.debug('Change detected for RDS instance {}/{} ' .format(db_instance['resource_name'], properties)) else: RDSInstance.create( db_instance['resource_name'], account_id=self.account.account_id, location=db_instance['region'], properties=properties, tags=tags ) # Removal of RDS instances rk = set() erk = set() for database in rds_dbs: rk.add(database['resource_name']) for existing in existing_rds_dbs.keys(): erk.add(existing) for resource_id in erk - rk: db.session.delete(existing_rds_dbs[resource_id].resource) self.log.debug('Removed RDS instances {}/{}'.format( self.account.account_name, resource_id )) db.session.commit() else: self.log.error('RDS Lambda Execution Failed / {} / {} / {}'. format(self.account.account_name, self.region, response_payload)) except Exception as e: self.log.exception('There was a problem during RDS collection for {}/{}/{}'.format( self.account.account_name, self.region, e )) db.session.rollback()
def delete_s3_bucket(client, resource): try: session = get_aws_session(AWSAccount(resource.account)) bucket = session.resource('s3', resource.location).Bucket(resource.resource_id) days_until_expiry = dbconfig.get('lifecycle_expiration_days', NS_AUDITOR_REQUIRED_TAGS, 3) # Separate rule for Object Markers is needed and can't be combined into a single rule per AWS API lifecycle_policy = { 'Rules': [ {'Status': 'Enabled', 'NoncurrentVersionExpiration': {u'NoncurrentDays': days_until_expiry}, 'Filter': {u'Prefix': ''}, 'Expiration': { 'Date': datetime.utcnow().replace( hour=0, minute=0, second=0, microsecond=0 ) + timedelta(days=days_until_expiry) }, 'AbortIncompleteMultipartUpload': {u'DaysAfterInitiation': days_until_expiry}, 'ID': 'cinqRemoveObjectsAndVersions'}, {'Status': 'Enabled', 'Filter': {u'Prefix': ''}, 'Expiration': { 'ExpiredObjectDeleteMarker': True }, 'ID': 'cinqRemoveDeletedExpiredMarkers'} ] } bucket_policy = { 'Version': '2012-10-17', 'Id': 'PutObjPolicy', 'Statement': [ {'Sid': 'cinqDenyObjectUploads', 'Effect': 'Deny', 'Principal': '*', 'Action': ['s3:PutObject', 's3:GetObject'], 'Resource': 'arn:aws:s3:::{}/*'.format(resource.resource_id) } ] } metrics = {'Unavailable': 'Unavailable'} for prop in resource.properties: if prop.name == "metrics": metrics = prop.value objects = list(bucket.objects.limit(count=1)) versions = list(bucket.object_versions.limit(count=1)) if not objects and not versions: bucket.delete() logger.info('Deleted s3 bucket {} in {}'.format(resource.resource_id, resource.account)) Enforcement.create(resource.account_id, resource.resource_id, 'DELETED', datetime.now(), metrics) auditlog( event='required_tags.s3.terminate', actor=NS_AUDITOR_REQUIRED_TAGS, data={ 'resource_id': resource.resource_id, 'account_name': resource.account.account_name, 'location': resource.location } ) return True else: try: rules = bucket.LifecycleConfiguration().rules for rule in rules: if rule['ID'] == 'cinqRemoveDeletedExpiredMarkers': rules_exists = True break else: rules_exists = False except ClientError: rules_exists = False try: current_bucket_policy = bucket.Policy().policy except ClientError as error: if error.response['Error']['Code'] == 'NoSuchBucketPolicy': current_bucket_policy = 'missing' try: if not rules_exists: # Grab S3 Metrics before lifecycle policies start removing objects bucket.LifecycleConfiguration().put(LifecycleConfiguration=lifecycle_policy) logger.info('Added policies to delete bucket contents in s3 bucket {} in {}'.format( resource.resource_id, resource.account )) Enforcement.create(resource.account_id, resource.resource_id, 'LIFECYCLE_APPLIED', datetime.now(), metrics) if 'cinqDenyObjectUploads' not in current_bucket_policy: bucket.Policy().put(Policy=json.dumps(bucket_policy)) logger.info('Added policy to prevent putObject in s3 bucket {} in {}'.format( resource.resource_id, resource.account )) except ClientError as error: logger.error( 'Problem applying the bucket policy or lifecycle configuration to bucket {} / account {} / {}' .format(resource.resource_id, resource.account_id, error.response['Error']['Code'])) if rules_exists and 'cinqDenyObjectUploads' in current_bucket_policy: # We're waiting for the lifecycle policy to delete data raise ResourceActionError({'msg': 'wait_for_deletion'}) except ResourceActionError as error: raise ResourceActionError(error) except Exception as error: logger.info( 'Failed to delete s3 bucket {} in {}, error is {}'.format(resource.resource_id, resource.account, error)) raise ResourceKillError( 'Failed to delete s3 bucket {} in {}. Reason: {}'.format(resource.resource_id, resource.account, error) )
def get_aws_regions(service): return get_aws_session().get_available_regions(service)