def create_cloudtrail(self, region): """Creates a new CloudTrail Trail Args: region (str): Name of the AWS region Returns: `None` """ ct = self.session.client('cloudtrail', region_name=region) # Creating the sns topic for the trail prior to creation self.create_sns_topic(region) ct.create_trail(Name=self.trail_name, S3BucketName=self.bucket_name, S3KeyPrefix=self.account.account_name, IsMultiRegionTrail=True, IncludeGlobalServiceEvents=True, SnsTopicName=self.topic_name) self.subscribe_sns_topic_to_sqs(region) auditlog(event='cloudtrail.create_cloudtrail', actor=self.ns, data={ 'account': self.account.account_name, 'region': region }) self.log.info('Created CloudTrail for {} in {} ({})'.format( self.account, region, self.bucket_name))
def create_sns_topic(self, region): """Creates an SNS topic if needed. Returns the ARN if the created SNS topic Args: region (str): Region name Returns: `str` """ sns = self.session.client('sns', region_name=region) self.log.info('Creating SNS topic for {}/{}'.format( self.account, region)) # Create the topic res = sns.create_topic(Name=self.topic_name) arn = res['TopicArn'] # Allow CloudTrail to publish messages with a policy update tmpl = get_template('cloudtrail_sns_policy.json') policy = tmpl.render(region=region, account_id=self.account.account_number, topic_name=self.topic_name) sns.set_topic_attributes(TopicArn=arn, AttributeName='Policy', AttributeValue=policy) auditlog(event='cloudtrail.create_sns_topic', actor=self.ns, data={ 'account': self.account.account_name, 'region': region }) return arn
def put(self, template_name): """Update a template""" self.reqparse.add_argument('template', type=str, required=True) args = self.reqparse.parse_args() template = db.Template.find_one(template_name=template_name) if not template: return self.make_response('No such template found', HTTP.NOT_FOUND) changes = diff(template.template, args['template']) template.template = args['template'] template.is_modified = True db.session.add(template) db.session.commit() auditlog(event='template.update', actor=session['user'].username, data={ 'template_name': template_name, 'template_changes': changes }) return self.make_response( 'Template {} has been updated'.format(template_name))
def _import_templates(force=False): """Import templates from disk into database Reads all templates from disk and adds them to the database. By default, any template that has been modified by the user will not be updated. This can however be changed by setting `force` to `True`, which causes all templates to be imported regardless of status Args: force (`bool`): Force overwrite any templates with local changes made. Default: `False` Returns: `None` """ tmplpath = os.path.join(resource_filename('cloud_inquisitor', 'data'), 'templates') disk_templates = { f: os.path.join(root, f) for root, directory, files in os.walk(tmplpath) for f in files } db_templates = {tmpl.template_name: tmpl for tmpl in db.Template.find()} for name, template_file in disk_templates.items(): with open(template_file, 'r') as f: body = f.read() disk_hash = get_hash(body) if name not in db_templates: template = Template() template.template_name = name template.template = body db.session.add(template) auditlog(event='template.import', actor='init', data={ 'template_name': name, 'template': body }) logger.info('Imported template {}'.format(name)) else: template = db_templates[name] db_hash = get_hash(template.template) if db_hash != disk_hash: if force or not db_templates[name].is_modified: template.template = body db.session.add(template) auditlog(event='template.update', actor='init', data={ 'template_name': name, 'template_diff': diff(template.template, body) }) logger.info('Updated template {}'.format(name)) else: logger.warning( 'Updated template available for {}. Will not import as it would' ' overwrite user edited content and force is not enabled' .format(name))
def post(self): """Create a new configuration namespace""" self.reqparse.add_argument('namespacePrefix', type=str, required=True) self.reqparse.add_argument('name', type=str, required=True) self.reqparse.add_argument('sortOrder', type=int, required=True) args = self.reqparse.parse_args() if self.dbconfig.namespace_exists(args['namespacePrefix']): return self.make_response( 'Namespace {} already exists'.format(args['namespacePrefix']), HTTP.CONFLICT) ns = ConfigNamespace() ns.namespace_prefix = args['namespacePrefix'] ns.name = args['name'] ns.sort_order = args['sortOrder'] db.session.add(ns) db.session.commit() self.dbconfig.reload_data() auditlog(event='configNamespace.create', actor=session['user'].username, data=args) return self.make_response('Namespace created', HTTP.CREATED)
def delete(self, user_id): """Delete a user""" auditlog(event='user.delete', actor=session['user'].username, data={'userId': user_id}) if session['user'].user_id == user_id: return self.make_response( 'You cannot delete the user you are currently logged in as', HTTP.FORBIDDEN) user = db.User.find_one(User.user_id == user_id) if not user: return self.make_response( 'No such user id found: {}'.format(user_id), HTTP.UNAUTHORIZED) if user.username == 'admin' and user.auth_system == 'builtin': return self.make_response( 'You cannot delete the built-in admin user', HTTP.FORBIDDEN) username = user.username auth_system = user.auth_system db.session.delete(user) db.session.commit() return self.make_response( 'User {}/{} has been deleted'.format(auth_system, username), HTTP.OK)
def put(self, user_id): self.reqparse.add_argument('password', type=str, required=False) args = self.reqparse.parse_args() auditlog(event='user.passwordReset', actor=session['user'].username, data=args) user = db.User.find_one(User.user_id == user_id) if not user: return self.make_response('User not found', HTTP.NOT_FOUND) if ROLE_ADMIN not in session['user'].roles and user_id != session['user'].user_id: self.log.warning('{} tried to change the password for another user'.format(session['user'].user_id)) return self.make_response('You cannot change other users passwords', HTTP.FORBIDDEN) authsys = current_app.available_auth_systems[user.auth_system] if authsys.readonly: return self.make_response( 'You cannot reset passwords for the {} based users'.format(authsys.name), HTTP.FORBIDDEN ) new_pass = args['password'] or generate_password() user.password = hash_password(new_pass) db.session.add(user) db.session.commit() return self.make_response({ 'user': user.to_json(), 'newPassword': new_pass if not args['password'] else None }, HTTP.OK)
def post(self): """Create a new template""" self.reqparse.add_argument('templateName', type=str, required=True) self.reqparse.add_argument('template', type=str, required=True) args = self.reqparse.parse_args() template = db.Template.find_one(template_name=args['templateName']) if template: return self.make_response( 'Template already exists, update the existing template instead', HTTP.CONFLICT) template = Template() template.template_name = args['templateName'] template.template = args['template'] db.session.add(template) db.session.commit() auditlog(event='template.create', actor=session['user'].username, data=args) return self.make_response( 'Template {} has been created'.format(template.template_name), HTTP.CREATED)
def put(self, emailId): email = db.Email.find_one(Email.email_id == emailId) if not email: return self.make_response( { 'message': 'Email not found', 'email': None }, HTTP.NOT_FOUND) try: send_notification(subsystem=email.subsystem, recipients=[ NotificationContact('email', x) for x in email.recipients ], subject=email.subject, body_html=email.message_html, body_text=email.message_text) auditlog(event='email.resend', actor=session['user'].username, data={'emailId': emailId}) return self.make_response('Email resent successfully') except EmailSendError as ex: self.log.exception('Failed resending email {0}: {1}'.format( email.email_id, ex)) return self.make_response( 'Failed resending the email: {0}'.format(ex), HTTP.UNAVAILABLE)
def put(self, user_id): """Update a user object""" self.reqparse.add_argument('roles', type=str, action='append') args = self.reqparse.parse_args() auditlog(event='user.create', actor=session['user'].username, data=args) user = db.User.find_one(User.user_id == user_id) roles = db.Role.find(Role.name.in_(args['roles'])) if not user: return self.make_response('No such user found: {}'.format(user_id), HTTP.NOT_FOUND) if user.username == 'admin' and user.auth_system == 'builtin': return self.make_response( 'You cannot modify the built-in admin user', HTTP.FORBIDDEN) user.roles = [] for role in roles: if role in args['roles']: user.roles.append(role) db.session.add(user) db.session.commit() return self.make_response({'message': 'User roles updated'}, HTTP.OK)
def post(self): """Create a new config item""" self.reqparse.add_argument('namespacePrefix', type=str, required=True) self.reqparse.add_argument('description', type=str, required=True) self.reqparse.add_argument('key', type=str, required=True) self.reqparse.add_argument('value', required=True) self.reqparse.add_argument('type', type=str, required=True) args = self.reqparse.parse_args() if not self.dbconfig.namespace_exists(args['namespacePrefix']): return self.make_response('The namespace doesnt exist', HTTP.NOT_FOUND) if self.dbconfig.key_exists(args['namespacePrefix'], args['key']): return self.make_response('This config item already exists', HTTP.CONFLICT) self.dbconfig.set(args['namespacePrefix'], args['key'], _to_dbc_class(args), description=args['description']) auditlog(event='configItem.create', actor=session['user'].username, data=args) return self.make_response('Config item added', HTTP.CREATED)
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 get(self): out = [ns.to_dict() for ns in db.ConfigNamespace.find()] auditlog(event='config.export', actor=session['user'].username, data={}) return Response(response=b64encode( bytes(json.dumps(out, cls=InquisitorJSONEncoder), 'utf-8')), status=HTTP.OK)
def get(self): out = [ns.to_json(is_admin=True) for ns in db.Account.find()] auditlog(event='account.export', actor=session['user'].username, data={}) return Response(response=b64encode( bytes(json.dumps(out, cls=InquisitorJSONEncoder), 'utf-8')), status=HTTP.OK)
def terminate_ec2_instance(client, resource): """Terminate an EC2 Instance This function will terminate an EC2 Instance. Returns `True` if succesful, or raises an exception if not Args: client (:obj:`boto3.session.Session.client`): A boto3 client object resource (:obj:`Resource`): The resource object to terminate Returns: `bool` - True if the instance was terminated. Will raise an exception if failed """ try: # Gather instance metrics before termination instance_type = "Not Found" public_ip = "Not Found" for prop in resource.properties: if prop.name == "instance_type": instance_type = prop.value if prop.name == "public_ip": public_ip = prop.value metrics = {"instance_type": instance_type, "public_ip": public_ip} client.modify_instance_attribute(InstanceId=resource.resource_id, Attribute='disableApiTermination', Value='False') client.terminate_instances(InstanceIds=[resource.resource_id]) logger.info('Terminated instance {}/{}/{}'.format( resource.account, resource.location, resource.resource_id )) Enforcement.create(resource.account_id, resource.resource_id, 'TERMINATE', datetime.now(), metrics) auditlog( event='required_tags.ec2.terminate', actor=NS_AUDITOR_REQUIRED_TAGS, data={ 'resource_id': resource.resource_id, 'account_name': resource.account.account_name, 'location': resource.location } ) return True except Exception as error: logger.info('Failed to kill instance {}/{}/{}: {}'.format( resource.account.account_name, resource.location, resource.resource_id, error )) raise ResourceKillError('Failed to kill instance {}/{}/{}: {}'.format( resource.account.account_name, resource.location, resource.resource_id, error ))
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 delete(self, accountId): """Delete an account""" acct = BaseAccount.get(accountId) if not acct: raise Exception('No such account found') acct.delete() auditlog(event='account.delete', actor=session['user'].username, data={'accountId': accountId}) return self.make_response('Account deleted')
def post(self): """Create a new account""" self.reqparse.add_argument('accountName', type=str, required=True) self.reqparse.add_argument('accountNumber', type=str, required=True) self.reqparse.add_argument('contacts', type=dict, required=True, action='append') self.reqparse.add_argument('enabled', type=int, required=True, choices=(0, 1)) self.reqparse.add_argument('requiredGroups', type=str, action='append', default=()) self.reqparse.add_argument('adGroupBase', type=str, default=None) args = self.reqparse.parse_args() validate_contacts(args['contacts']) for key in ('accountName', 'accountNumber', 'contacts', 'enabled'): value = args[key] if type(value) == str: value = value.strip() if type(value) in (int, tuple, list, str): if not value: raise Exception('{} cannot be empty'.format( key.replace('_', ' ').title())) else: raise ValueError( 'Invalid type: {} for value {} for argument {}'.format( type(value), value, key)) if db.Account.filter_by(account_name=args['accountName']).count() > 0: raise Exception('Account already exists') acct = Account(args['accountName'], args['accountNumber'].zfill(12), args['contacts'], args['enabled'], args['adGroupBase']) acct.required_groups = json.dumps(args['requiredGroups']) db.session.add(acct) db.session.commit() # Add the newly created account to the session so we can see it right away session['accounts'].append(acct.account_id) auditlog(event='account.create', actor=session['user'].username, data=args) return self.make_response( { 'message': 'Account created', 'accountId': acct.account_id }, HTTP.CREATED)
def stop_ec2_instance(client, resource): """Stop an EC2 Instance This function will attempt to stop a running instance. If the instance is already stopped the function will return False, else True. Args: client (:obj:`boto3.session.Session.client`): A boto3 client object resource (:obj:`Resource`): The resource object to stop Returns: `bool` """ try: instance = EC2Instance.get(resource.resource_id) if instance.state not in ('stopped', 'terminated'): instance_type = "Not Found" public_ip = "Not Found" for prop in resource.properties: if prop.name == "instance_type": instance_type = prop.value if prop.name == "public_ip": public_ip = prop.value metrics = {"instance_type": instance_type, "public_ip": public_ip} client.stop_instances(InstanceIds=[resource.resource_id]) logger.debug('Stopped instance {}/{}'.format(resource.account.account_name, resource.resource_id)) Enforcement.create(resource.account_id, resource.resource_id, 'STOP', datetime.now(), metrics) auditlog( event='required_tags.ec2.stop', actor=NS_AUDITOR_REQUIRED_TAGS, data={ 'resource_id': resource.resource_id, 'account_name': resource.account.account_name, 'location': resource.location } ) return True else: return False except Exception as error: logger.info('Failed to stop instance {}/{}: {}'.format( resource.account.account_name, resource.resource_id, error )) raise ResourceStopError('Failed to stop instance {}/{}: {}'.format( resource.account, resource.resource_id, error ))
def log(cls, event=None, actor=None, data=None): """Generate and insert a new event Args: event (str): Action performed actor (str): Actor (user or subsystem) triggering the event data (dict): Any extra data necessary for describing the event Returns: `None` """ from cloud_inquisitor.log import auditlog auditlog(event=event, actor=actor, data=data)
def delete(self): self.reqparse.add_argument('maxAge', type=int, default=31) args = self.reqparse.parse_args() db.LogEvent.filter( func.datesub( LogEvent.timestamp < datetime.now() - timedelta(days=args['maxAge']) ) ).delete() db.session.commit() auditlog(event='logs.prune', actor=session['user'].username, data=args) return self.make_response('Pruned logs older than {} days'.format(args['maxAge']))
def delete(self, namespace, key): """Delete a specific configuration item""" if not self.dbconfig.key_exists(namespace, key): return self.make_response( 'No such config entry exists: {}/{}'.format(namespace, key), HTTP.BAD_REQUEST) self.dbconfig.delete(namespace, key) auditlog(event='configItem.delete', actor=session['user'].username, data={ 'namespace': namespace, 'key': key }) return self.make_response('Config entry deleted')
def post(self): self.reqparse.add_argument('config', type=str, required=True) args = self.reqparse.parse_args() try: config = json.loads(args['config'], cls=InquisitorJSONDecoder) for nsdata in config: ns = ConfigNamespace.get(nsdata['namespacePrefix']) # Update existing namespace if ns: ns.namespace_prefix = nsdata['namespacePrefix'] ns.name = nsdata['name'] ns.sort_order = nsdata['sortOrder'] else: ns = ConfigNamespace() ns.namespace_prefix = nsdata['namespacePrefix'] ns.name = nsdata['name'] ns.sort_order = nsdata['sortOrder'] db.session.add(ns) for itmdata in nsdata['configItems']: itm = ConfigItem.get(ns.namespace_prefix, itmdata['key']) if itm: itm.value = itmdata['value'] itm.type = itmdata['type'] itm.description = itmdata['description'] else: itm = ConfigItem() itm.namespace_prefix = ns.namespace_prefix itm.key = itmdata['key'] itm.value = itmdata['value'] itm.description = itmdata['description'] db.session.add(itm) db.session.commit() auditlog(event='config.import', actor=session['user'].username, data=config) return self.make_response('Configuration imported') except Exception as ex: self.log.exception('Failed importing configuration data') return self.make_response( 'Error importing configuration data: {}'.format(ex), HTTP.SERVER_ERROR)
def delete(self, template_name): """Delete a template""" template = db.Template.find_one(template_name=template_name) if not template: return self.make_response('No such template found', HTTP.NOT_FOUND) db.session.delete(template) db.session.commit() auditlog(event='template.delete', actor=session['user'].username, data={'template_name': template_name}) return self.make_response({ 'message': 'Template has been deleted', 'templateName': template_name })
def post(self): self.reqparse.add_argument('config', type=str, required=True) args = self.reqparse.parse_args() try: config = json.loads(args['config'], cls=InquisitorJSONDecoder) apply_config(config) auditlog(event='config.import', actor=session['user'].username, data=config) return self.make_response('Configuration imported') except Exception as ex: self.log.exception('Failed importing configuration data') return self.make_response( 'Error importing configuration data: {}'.format(ex), HTTP.SERVER_ERROR)
def post(self): """Create a new account""" self.reqparse.add_argument('accountName', type=str, required=True) self.reqparse.add_argument('accountType', type=str, required=True) self.reqparse.add_argument('contacts', type=dict, required=True, action='append') self.reqparse.add_argument('enabled', type=int, required=True, choices=(0, 1)) self.reqparse.add_argument('requiredGroups', type=str, action='append', default=()) self.reqparse.add_argument('properties', type=dict, required=True) args = self.reqparse.parse_args() account_class = get_plugin_by_name(PLUGIN_NAMESPACES['accounts'], args['accountType']) if not account_class: raise InquisitorError('Invalid account type: {}'.format(args['accountType'])) validate_contacts(args['contacts']) for key in ('accountName', 'accountType', 'contacts', 'enabled'): value = args[key] if type(value) == str: value = value.strip() if type(value) in (int, tuple, list, str): if not value: raise Exception('{} cannot be empty'.format(key.replace('_', ' ').title())) else: raise ValueError('Invalid type: {} for value {} for argument {}'.format(type(value), value, key)) class_properties = {from_camelcase(key): value for key, value in args['properties'].items()} for prop in account_class.class_properties: if prop['key'] not in class_properties: raise InquisitorError('Missing required property {}'.format(prop)) acct = account_class.create( account_name=args['accountName'], contacts=args['contacts'], enabled=args['enabled'], required_roles=args['requiredGroups'], properties=class_properties, auto_commit=False ) db.session.commit() # Add the newly created account to the session so we can see it right away session['accounts'].append(acct.account_id) auditlog(event='account.create', actor=session['user'].username, data=args) return self.make_response({'message': 'Account created', 'accountId': acct.account_id}, HTTP.CREATED)
def confirm_cw_log(self, account, region, vpcname): """Create a new CloudWatch log group based on the VPC Name if none exists. Returns `True` if succesful Args: account (:obj:`Account`): Account to create the log group in region (`str`): Region to create the log group in vpcname (`str`): Name of the VPC the log group is fow Returns: `bool` """ try: cw = self.session.client('logs', region) token = None log_groups = [] while True: result = cw.describe_log_groups( ) if not token else cw.describe_log_groups(nextToken=token) token = result.get('nextToken') log_groups.extend( [x['logGroupName'] for x in result.get('logGroups', [])]) if not token: break if vpcname not in log_groups: cw.create_log_group(logGroupName=vpcname) cw_vpc = VPC.get(vpcname) cw_vpc.set_property('vpc_flow_logs_log_group', vpcname) self.log.info('Created log group {}/{}/{}'.format( account.account_name, region, vpcname)) auditlog(event='vpc_flow_logs.create_cw_log_group', actor=self.ns, data={ 'account': account.account_name, 'region': region, 'log_group_name': vpcname, 'vpc': vpcname }) return True except Exception: self.log.exception( 'Failed creating log group for {}/{}/{}.'.format( account, region, vpcname))
def delete(self, roleId): """Delete a user role""" role = db.Role.find_one(Role.role_id == roleId) if not role: return self.make_response('No such role found', HTTP.NOT_FOUND) if role.name in ('User', 'Admin'): return self.make_response('Cannot delete the built-in roles', HTTP.BAD_REQUEST) db.session.delete(role) db.session.commit() auditlog(event='role.delete', actor=session['user'].username, data={'roleId': roleId}) return self.make_response('Role has been deleted')
def delete(self, namespacePrefix): """Delete a specific configuration namespace""" ns = db.ConfigNamespace.find_one( ConfigNamespace.namespace_prefix == namespacePrefix) if not ns: return self.make_response( 'No such namespace: {}'.format(namespacePrefix), HTTP.NOT_FOUND) db.session.delete(ns) db.session.commit() self.dbconfig.reload_data() auditlog(event='configNamespace.delete', actor=session['user'].username, data={'namespacePrefix': namespacePrefix}) return self.make_response('Namespace deleted')
def post(self): """Create a new role""" self.reqparse.add_argument('name', type=str, required=True) self.reqparse.add_argument('color', type=str, required=True) args = self.reqparse.parse_args() role = Role() role.name = args['name'] role.color = args['color'] db.session.add(role) db.session.commit() auditlog(event='role.create', actor=session['user'].username, data=args) return self.make_response( 'Role {} has been created'.format(role.role_id), HTTP.CREATED)