def delete_submission(sid, **kwargs): """ INCOMPLETE Delete a submission as well as all related files, results and errors Variables: sid => Submission ID to be deleted Arguments: None Data Block: None Result example: {success: true} """ user = kwargs['user'] submission = STORAGE.get_submission(sid) if submission and user \ and Classification.is_accessible(user['classification'], submission['classification']) \ and (submission['submission']['submitter'] == user['uname'] or user['is_admin']): with forge.get_filestore() as f_transport: STORAGE.delete_submission_tree(sid, transport=f_transport) STORAGE.commit_index('submission') return make_api_response({"success": True}) else: return make_api_response("", "Your are not allowed to delete this submission.", 403)
def get_error(error_key, **kwargs): """ Get the error details for a given error key Variables: error_key => Error key to get the details for Arguments: None Data Block: None Result example: { KEY: VALUE, # All fields of an error in key/value pair } """ user = kwargs['user'] data = STORAGE.get_error(error_key) if user and data and Classification.is_accessible(user['classification'], data['classification']): return make_api_response(data) else: return make_api_response("", "You are not allowed to see this error...", 403)
def get_signature(sid, rev, **kwargs): """ Get the detail of a signature based of its ID and revision Variables: sid => Signature ID rev => Signature revision number Arguments: None Data Block: None Result example: {"name": "sig_name", # Signature name "tags": ["PECheck"], # Signature tags "comments": [""], # Signature comments lines "meta": { # Meta fields ( **kwargs ) "id": "SID", # Mandatory ID field "rule_version": 1 }, # Mandatory Revision field "type": "rule", # Rule type (rule, private rule ...) "strings": ['$ = "a"'], # Rule string section (LIST) "condition": ["1 of them"]} # Rule condition section (LIST) """ user = kwargs['user'] data = STORAGE.get_signature("%sr.%s" % (sid, rev)) if data: if not Classification.is_accessible(user['classification'], data['meta'].get('classification', Classification.UNRESTRICTED)): return make_api_response("", "Your are not allowed to view this signature.", 403) return make_api_response(data) else: return make_api_response("", "Signature not found. (%s r.%s)" % (sid, rev), 404)
def setup_watch_queue(sid, **kwargs): """ Starts a watch queue to get live results Variables: sid => Submission ID Arguments: (optional) suffix => suffix to be appended to the queue name Data Block: None Result example: {"wq_id": "c7668cfa-...-c4132285142e"} #ID of the watch queue """ data = STORAGE.get_submission(sid) user = kwargs['user'] if user and data and Classification.is_accessible(user['classification'], data['classification']): watch_queue = reply_queue_name(request.args.get('suffix', "WQ")) SubmissionWrapper.watch(sid, watch_queue) return make_api_response({"wq_id": watch_queue}) else: return make_api_response( "", "You are not allowed to access this submissions.", 403)
def add_apikey(name, priv, **kwargs): """ Add an API Key for the currently logged in user with given privileges Variables: name => Name of the API key priv => Requested privileges Arguments: None Data Block: None Result example: {"apikey": <ramdomly_generated_password>} """ user = kwargs['user'] user_data = STORAGE.get_user(user['uname']) if name in [x[0] for x in user_data.get('apikeys', [])]: return make_api_response("", err="APIKey %s already exist" % name, status_code=400) if priv not in API_PRIV_MAP.keys(): return make_api_response("", err="Invalid APIKey privilege '%s'. " "Choose between: %s " % (priv, API_PRIV_MAP.keys()), status_code=400) keys = user_data.get('apikeys', []) random_pass = get_random_password(length=48) keys.append((name, bcrypt.encrypt(random_pass), API_PRIV_MAP[priv])) user_data['apikeys'] = keys STORAGE.save_user(user['uname'], user_data) return make_api_response({"apikey": random_pass})
def get_service_result(cache_key, **kwargs): """ Get the result for a given service cache key. Variables: cache_key => Service result cache key as SRL.ServiceName.ServiceVersion.ConfigHash Arguments: None Data Block: None Result example: {"response": { # Service Response "milestones": {}, # Timing object "supplementary": [], # Supplementary files "service_name": "Mcafee", # Service Name "message": "", # Service error message "extracted": [], # Extracted files "service_version": "v0"}, # Service Version "result": { # Result objects "score": 1302, # Total score for the file "sections": [{ # Result sections "body": "Text goes here", # Body of the section (TEXT) "classification": "", # Classification "links": [], # Links inside the section "title_text": "Title", # Title of the section "depth": 0, # Depth (for Display purposes) "score": 500, # Section's score "body_format": null, # Body format "subsections": [] # List of sub-sections }, ... ], "classification": "", # Maximum classification for service "tags": [{ # Generated Tags "usage": "IDENTIFICATION", # Tag usage "value": "Tag Value", # Tag value "type": "Tag Type", # Tag type "weight": 50, # Tag Weight "classification": "" # Tag Classification }, ...] } } """ user = kwargs['user'] data = STORAGE.get_result(cache_key) if data is None: return make_api_response("", "Cache key %s does not exists." % cache_key, 404) cur_file = STORAGE.get_file(cache_key[:64]) data = format_result(user['classification'], data, cur_file['classification']) if not data: return make_api_response( "", "You are not allowed to view the results for this key", 403) return make_api_response(data)
def remove_workflow(workflow_id, **_): """ Remove the specified workflow. Variables: workflow_id => ID of the workflow to remove Arguments: None Data Block: None Result example: { "success": true # Was the remove successful? } """ wf = STORAGE.get_workflow(workflow_id) if wf: STORAGE.delete_workflow(workflow_id) return make_api_response({"success": True}) else: return make_api_response({"success": False}, err="Workflow ID %s does not exist" % workflow_id, status_code=404)
def set_user_avatar(username, **_): """ Sets the user's Avatar Variables: username => Name of the user you want to set the avatar for Arguments: None Data Block: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD..." Result example: { "success": true # Was saving the avatar successful ? } """ data = request.json if not isinstance(data, str) or not STORAGE.set_user_avatar( username, data): make_api_response({"success": False}, "Data block should be a base64 encoded image " "that starts with 'data:image/<format>;base64,'") return make_api_response({"success": True})
def get_cluster_config(name, **kwargs): data = STORAGE.get_blob("prov_conf_%s" % name) if not data: return make_api_response({}, "Provisionning config %s not found." % name, 404) return make_api_response(data)
def remove_user_favorite(username, favorite_type, **_): """ Remove a favorite from the user's favorites. Variables: username => Name of the user to remove the favorite from favorite_type => Type of favorite to remove Arguments: None Data Block: "name_of_favorite" # Name of the favorite to remove Result example: { "success": true # Was the remove successful? } """ if favorite_type not in ALLOWED_FAVORITE_TYPE: return make_api_response({}, "%s is not a valid favorite type" % favorite_type, 500) name = request.data or "None" try: favorites = STORAGE.get_user_favorites(username) for fav in favorites[favorite_type]: if fav['name'] == name: favorites[favorite_type].remove(fav) except Exception: return make_api_response({}, "Favorite does not exists, (%s)" % name, 404) return make_api_response( {"success": STORAGE.set_user_favorites(username, favorites)})
def outstanding_services(sid, **kwargs): """ List outstanding services and the number of file each of them still have to process. Variables: sid => Submission ID Arguments: None Data Block: None Result example: {"MY SERVICE": 1, ... } # Dictionnary of services and number of files """ data = STORAGE.get_submission(sid) user = kwargs['user'] if user and data and Classification.is_accessible(user['classification'], data['classification']): return make_api_response(DispatchClient.get_outstanding_services(sid)) else: return make_api_response( {}, "You are not allowed to access this submissions.", 403)
def get_source_seed(**kwargs): """ Get the default configuration seed Variables: None Arguments: None Data Block: None Result example: { "KEY": "value", # Dictionary of key/value pair ... } """ seed_module = STORAGE.get_blob("seed_module") if not seed_module: return make_api_response({}) seed = module_attribute_by_name(seed_module) services_to_register = seed['services']['master_list'] for service, svc_detail in services_to_register.iteritems(): seed['services']['master_list'][service] = get_merged_svc_config(service, svc_detail, LOGGER) return make_api_response(seed)
def get_file_hex(srl, **kwargs): """ Returns the file hex representation Variables: srl => A resource locator for the file (sha256) Arguments: None Data Block: None API call example: /api/v3/file/hex/123456...654321/ Result example: <THE FILE HEX REPRESENTATION> """ user = kwargs['user'] file_obj = STORAGE.get_file(srl) if not file_obj: return make_api_response({}, "The file was not found in the system.", 404) if user and Classification.is_accessible(user['classification'], file_obj['classification']): with forge.get_filestore() as f_transport: data = f_transport.get(srl) if not data: return make_api_response({}, "This file was not found in the system.", 404) return make_api_response(hexdump(data)) else: return make_api_response({}, "You are not allowed to view this file.", 403)
def validate_otp(token, **kwargs): """ Validate newly setup OTP token Variables: token => Current token for temporary OTP sercret key Arguments: None Data Block: None Result example: { "success": true } """ uname = kwargs['user']['uname'] user_data = STORAGE.get_user(uname) try: token = int(token) except ValueError: return make_api_response({'success': False}, err="This is not a valid OTP token", status_code=400) secret_key = flsk_session.pop('temp_otp_sk', None) if get_totp_token(secret_key) == token: user_data['otp_sk'] = secret_key STORAGE.save_user(uname, user_data) return make_api_response({'success': True}) else: flsk_session['temp_otp_sk'] = secret_key return make_api_response({'success': False}, err="OTP token does not match secret key", status_code=400)
def download_file(srl, **kwargs): """ Download the file using the default encoding method. This api will force the browser in download mode. Variables: srl => A resource locator for the file (sha256) Arguments: name => Name of the file to download format => Format to encode the file in password => Password of the password protected zip Data Block: None API call example: /api/v3/file/download/123456...654321/ Result example: <THE FILE BINARY ENCODED IN SPECIFIED FORMAT> """ user = kwargs['user'] file_obj = STORAGE.get_file(srl) if not file_obj: return make_api_response({}, "The file was not found in the system.", 404) if user and Classification.is_accessible(user['classification'], file_obj['classification']): params = load_user_settings(user) name = request.args.get('name', srl) if name == "": name = srl else: name = basename(name) name = safe_str(name) file_format = request.args.get('format', params['download_encoding']) if file_format == "raw" and not ALLOW_RAW_DOWNLOADS: return make_api_response({}, "RAW file download has been disabled by administrators.", 403) password = request.args.get('password', None) with forge.get_filestore() as f_transport: data = f_transport.get(srl) if not data: return make_api_response({}, "The file was not found in the system.", 404) data, error, already_encoded = encode_file(data, file_format, name, password) if error: return make_api_response({}, error['text'], error['code']) if file_format != "raw" and not already_encoded: name = "%s.%s" % (name, file_format) return make_file_response(data, name, len(data)) else: return make_api_response({}, "You are not allowed to download this file.", 403)
def get_file_children(srl, **kwargs): """ Get the list of children files for a given file Variables: srl => A resource locator for the file (sha256) Arguments: None Data Block: None API call example: /api/v3/file/children/123456...654321/ Result example: [ # List of children {"name": "NAME OF FILE", # Name of the children "srl": "123..DEF"}, # SRL of the children (SHA256) ] """ user = kwargs['user'] file_obj = STORAGE.get_file(srl) if file_obj: if user and Classification.is_accessible(user['classification'], file_obj['classification']): return make_api_response(STORAGE.list_file_childrens(srl, access_control=user["access_control"])) else: return make_api_response({}, "You are not allowed to view this file.", 403) else: return make_api_response({}, "This file does not exists.", 404)
def get_file_raw(srl, **kwargs): """ Return the raw values for a file where non-utf8 chars are replaced by DOTs. Variables: srl => A resource locator for the file (sha256) Arguments: None Data Block: None Result example: <THE RAW FILE> """ user = kwargs['user'] file_obj = STORAGE.get_file(srl) if not file_obj: return make_api_response({}, "The file was not found in the system.", 404) if user and Classification.is_accessible(user['classification'], file_obj['classification']): with forge.get_filestore() as f_transport: data = f_transport.get(srl) if not data: return make_api_response({}, "This file was not found in the system.", 404) return make_api_response(data.translate(FILTER_RAW)) else: return make_api_response({}, "You are not allowed to view this file.", 403)
def get_alert(alert_key, **kwargs): """ Get the alert details for a given alert key Variables: alert_key => Alert key to get the details for Arguments: None Data Block: None API call example: /api/v3/alert/1234567890/ Result example: { KEY: VALUE, # All fields of an alert in key/value pair } """ user = kwargs['user'] data = STORAGE.get_alert(alert_key) if user and data and Classification.is_accessible(user['classification'], data['classification']): return make_api_response(data) else: return make_api_response("", "You are not allowed to see this alert...", 403)
def get_service_error(cache_key, **_): """ Get the content off a given service error cache key. Variables: cache_key => Service result cache key as (SRL.ServiceName) Arguments: None Data Block: None Result example: {"response": { # Service Response "milestones": {}, # Timing object "supplementary": [], # Supplementary files "status": "FAIL", # Status "service_version": "", # Service Version "service_name": "NSRL", # Service Name "extracted": [], # Extracted files "score": 0, # Service Score "message": "Err Message"}, # Error Message "result": []} # Result objets """ data = STORAGE.get_error(cache_key) if data is None: return make_api_response("", "Cache key %s does not exists." % cache_key, 404) return make_api_response(data)
def bind(**kwargs): """ Complete registration of the new key Variables: None Arguments: data => Response to the enroll challenge Data Block: None Result example: { "success": True } """ uname = kwargs['user']['uname'] data = request.json if "errorCode" in data: return make_api_response({'success': False}, err=U2F_CLIENT_ERROR_MAP[data['errorCode']], status_code=400) user = STORAGE.get_user(uname) current_enroll = session.pop('_u2f_enroll_') try: device, cert = complete_registration(current_enroll, data, [APP_ID]) except Exception as e: return make_api_response({'success': False}, err=e.message, status_code=400) user.setdefault('u2f_devices', []).append(device.json) STORAGE.save_user(uname, user) return make_api_response({"success": True})
def check_srl_exists(*args, **kwargs): """ Check if the the provided Resource locators exist in the system or not. Variables: None Arguments: None Data Block (REQUIRED): ["SRL1", SRL2] # List of SRLs (SHA256) Result example: { "existing": [], # List of existing SRLs "missing": [] # List of missing SRLs } """ srls_to_check = request.json if type(srls_to_check) != list: return make_api_response("", "Expecting a list of SRLs", 403) with forge.get_filestore() as f_transport: check_results = SubmissionWrapper.check_exists(f_transport, srls_to_check) return make_api_response(check_results)
def add_user_account(username, **_): """ Add a user to the system Variables: username => Name of the user to add Arguments: None Data Block: { "name": "Test user", # Name of the user "is_active": true, # Is the user active? "classification": "", # Max classification for user "uname": "usertest", # Username "is_admin": false, # Is the user admin? "avatar": null, # Avatar of the user "groups": ["TEST"] # Groups the user is member of } Result example: { "success": true # Saving the user info succeded } """ data = request.json if "{" in username or "}" in username: return make_api_response({"success": False}, "You can't use '{}' in the username", 412) if not STORAGE.get_user_account(username): new_pass = data.pop('new_pass', None) if new_pass: if not check_password_requirements( new_pass, strict=config.auth.internal.strict_requirements): if config.auth.internal.strict_requirements: error_msg = "Password needs to be 8 characters with at least an uppercase, " \ "a lowercase, a number and a special character." else: error_msg = "Password needs to be 8 alphanumeric characters." return make_api_response({"success": False}, error_msg, 469) data['password'] = get_password_hash(new_pass) STORAGE.save_user( username, validate_settings(data, ACCOUNT_DEFAULT, exceptions=[ 'avatar', 'agrees_with_tos', 'dn', 'password', 'otp_sk', 'u2f_devices' ])) return make_api_response({"success": True}) else: return make_api_response( {"success": False}, "The username you are trying to add already exists.", 400)
def find_related_alert_ids(**kwargs): """ Return the list of all IDs related to the currently selected query Variables: None Arguments: q => Main query to filter the data [REQUIRED] tc => Time constraint to apply to the search start => Time at which to start the days constraint fq => Filter query applied to the data Data Block: None API call example: /api/v3/alert/related/?q=event_id:1 Result example: ["1"] """ user = kwargs['user'] q = request.args.get('q', None) fq = request.args.getlist('fq') if not q and not fq: return make_api_response( { "success": False, "event_id": None }, err="You need to at least provide a query to filter the data", status_code=400) if not q: q = fq.pop(0) tc = request.args.get('tc', None) stime = request.args.get('start', None) fq_list = [] if tc is not None and tc != "": if stime is not None: fq_list.append("reporting_ts:[%s-%s TO %s]" % (stime, tc, stime)) else: fq_list.append("reporting_ts:[NOW-%s TO NOW]" % tc) elif stime is not None and stime != "": fq_list.append("reporting_ts:[* TO %s]" % stime) if fq: if isinstance(fq, list): fq_list.extend(fq) elif fq != "": fq_list.append(fq) return make_api_response([ x['event_id'] for x in STORAGE.stream_search( 'alert', q, fq=fq_list, access_control=user['access_control']) ])
def list_alerts(**kwargs): """ List all alert in the system (per page) Variables: None Arguments: start_time => Time offset at which to list alerts time_slice => Length after the start time that we query offset => Offset at which we start giving alerts length => Numbers of alerts to return filter => Filter to apply to the alert list fq => Post filter queries (you can have multiple of those) Data Block: None API call example: /api/v3/alert/list/ Result example: {"total": 201, # Total alerts found "offset": 0, # Offset in the alert list "count": 100, # Number of alerts returned "items": [] # List of alert blocks } """ user = kwargs['user'] offset = int(request.args.get('offset', 0)) length = int(request.args.get('length', 100)) query = request.args.get('filter', "*") if not query: query = "*" start_time = request.args.get('start_time', None) time_slice = request.args.get('time_slice', None) filter_queries = [x for x in request.args.getlist("fq") if x != ""] try: return make_api_response( STORAGE.list_alerts(query, start=offset, rows=length, access_control=user['access_control'], fq_list=filter_queries, start_time=start_time, time_slice=time_slice)) except SearchException: return make_api_response("", "The specified search query is not valid.", 400) except RiakError, e: if e.value == "Query unsuccessful check the logs.": return make_api_response( "", "The specified search query is not valid.", 400) else: raise
def change_status_by_batch(status, **kwargs): """ Apply status to all alerts matching the given filters using a background process Variables: status => Status to apply Arguments: q => Main query to filter the data [REQUIRED] tc => Time constraint to apply to the search start => Time at which to start the days constraint fq => Filter query applied to the data Data Block: None API call example: /api/v3/alert/status/batch/MALICIOUS/?q=al_av:* Result example: {"status": "QUEUED"} """ action_queue = queue.PriorityQueue('alert-actions', db=DATABASE_NUM) status = status.upper() user = kwargs['user'] q = request.args.get('q', None) fq = request.args.getlist('fq') if not q and not fq: return make_api_response( { "success": False, "event_id": None }, err="You need to at least provide a query to filter the data", status_code=400) if not q: q = fq.pop(0) tc = request.args.get('tc', None) start = request.args.get('start', None) msg = { "user": user['uname'], "action": "batch_workflow", "query": q, "tc": tc, "start": start, "fq": fq, "status": status, "queue_priority": QUEUE_PRIORITY } action_queue.push(QUEUE_PRIORITY, msg) return make_api_response({"status": "QUEUED"})
def set_user_account(username, **kwargs): """ Save the user account information. Variables: username => Name of the user to get the account info Arguments: None Data Block: { "name": "Test user", # Name of the user "is_active": true, # Is the user active? "classification": "", # Max classification for user "uname": "usertest", # Username "is_admin": false, # Is the user admin? "avatar": null, # Avatar of the user "groups": ["TEST"] # Groups the user is member of } Result example: { "success": true # Saving the user info succeded } """ try: data = request.json new_pass = data.pop('new_pass', None) old_user = STORAGE.get_user(username) if not old_user: return make_api_response({"success": False}, "User %s does not exists" % username, 404) data['apikeys'] = old_user.get('apikeys', []) data['otp_sk'] = old_user.get('otp_sk', None) data['u2f_devices'] = old_user.get('u2f_devices', []) if new_pass: if not check_password_requirements( new_pass, strict=config.auth.internal.strict_requirements): return make_api_response( {"success": False}, "Password does not meet minimum password requirements.", 469) data['password'] = get_password_hash(new_pass) data.pop('new_pass_confirm', None) else: data['password'] = old_user.get('password', None) return make_api_response( {"success": save_user_account(username, data, kwargs['user'])}) except AccessDeniedException, e: return make_api_response({"success": False}, e.message, 403)
def flowjs_check_chunk(**kwargs): """ Flowjs check file chunk. This API is reserved for the FLOWJS file uploader. It allows FLOWJS to check if the file chunk already exists on the server. Variables: None Arguments (REQUIRED): flowChunkNumber => Current chunk number flowFilename => Original filename flowTotalChunks => Total number of chunks flowIdentifier => File unique identifier flowCurrentChunkSize => Size of the current chunk Data Block: None Result example: {'exists': True} #Does the chunk exists on the server? """ flow_chunk_number = request.args.get("flowChunkNumber", None) flow_chunk_size = request.args.get("flowChunkSize", None) flow_total_size = request.args.get("flowTotalSize", None) flow_filename = request.args.get("flowFilename", None) flow_total_chunks = request.args.get("flowTotalChunks", None) flow_identifier = request.args.get("flowIdentifier", None) flow_current_chunk_size = request.args.get("flowCurrentChunkSize", None) if not flow_chunk_number or not flow_identifier or not flow_current_chunk_size or not flow_filename \ or not flow_total_chunks or not flow_chunk_size or not flow_total_size: return make_api_response( "", "Required arguments missing. flowChunkNumber, flowIdentifier, " "flowCurrentChunkSize, flowChunkSize and flowTotalSize " "should always be present.", 412) mydir = os.path.join(TEMP_DIR_CHUNKED, flow_identifier) myfile = os.path.join(mydir, 'chunk.part' + flow_chunk_number) if os.path.exists(myfile): if os.path.getsize(myfile) == int(flow_current_chunk_size): if validate_chunks(mydir, flow_total_chunks, flow_chunk_size, flow_total_size): reconstruct_file(mydir, flow_identifier, flow_filename, flow_total_chunks) return make_api_response({"exist": True}) else: return make_api_response({"exist": False}, "Chunk wrong size, please resend!", 404) else: return make_api_response({"exist": False}, "Chunk does not exist, please send it!", 404)
def set_cluster_config(name, **kwargs): data = request.json if not data: return make_api_response({}, "There are no provisionning config to save", 400) if 'allocation' not in data or 'flex' not in data or 'overrides' not in data: return make_api_response({}, "Invalid provisioning config", 400) STORAGE.save_blob("prov_conf_%s" % name, data) return make_api_response({"success": True})
def set_service(servicename, **_): """ Save the configuration of a given service Variables: servicename => Name of the service to save Arguments: None Data Block: {'accepts': '(archive|executable|java|android)/.*', 'category': 'Extraction', 'classpath': 'al_services.alsvc_extract.Extract', 'config': {'DEFAULT_PW_LIST': ['password', 'infected']}, 'cpu_cores': 0.1, 'description': "Extract some stuff", 'enabled': True, 'install_by_default': True, 'name': 'Extract', 'ram_mb': 256, 'rejects': 'empty|metadata/.*', 'stage': 'EXTRACT', 'submission_params': [{'default': u'', 'name': 'password', 'type': 'str', 'value': u''}, {'default': False, 'name': 'extract_pe_sections', 'type': 'bool', 'value': False}, {'default': False, 'name': 'continue_after_extract', 'type': 'bool', 'value': False}], 'supported_platforms': ['Linux'], 'timeout': 60} Result example: {"success": true } #Saving the user info succeded """ data = request.json try: if servicename != data['name']: raise AccessDeniedException( "You are not allowed to change the service name.") return make_api_response( {"success": STORAGE.save_service(servicename, data)}) except AccessDeniedException, e: return make_api_response({"success": False}, e.message, 403)
def add_labels(alert_id, labels, **kwargs): """ Add one or multiple labels to a given alert Variables: alert_id => ID of the alert to add the label to labels => List of labels to add as comma separated string Arguments: None Data Block: None API call example: /api/v3/alert/label/1234567890/EMAIL/ Result example: {"success": true, "event_id": 0} """ user = kwargs['user'] labels = set(labels.upper().split(",")) alert = STORAGE.get_alert(alert_id) if not alert: return make_api_response({ "success": False, "event_id": None }, err="Alert ID %s not found" % alert_id, status_code=404) if not Classification.is_accessible(user['classification'], alert['classification']): return make_api_response("", "You are not allowed to see this alert...", 403) cur_label = set(alert.get('label', [])) if labels.difference(labels.intersection(cur_label)): cur_label = cur_label.union(labels) alert['label'] = list(cur_label) STORAGE.save_alert(alert_id, alert) return make_api_response({"success": True}) else: return make_api_response({"success": False}, err="Alert already has labels %s" % ", ".join(labels), status_code=403)