def auto_auth_check(self): apikey = request.environ.get('HTTP_X_APIKEY', None) uname = request.environ.get('HTTP_X_USER', None) if apikey is not None and uname is not None: ip = request.headers.get("X-Forwarded-For", request.remote_addr) with elasticapm.capture_span(name="@api_login:auto_auth_check()", span_type="authentication"): try: # TODO: apikey_handler is slow to verify the password (bcrypt's fault) # We could fix this by saving the hash of the combinaison of the # APIkey and the username in an ExpiringSet and looking it up for # sub-sequent calls... validated_user, priv = validate_apikey( uname, apikey, STORAGE) except AuthenticationException: msg = "Invalid user or APIKey" LOGGER.warning( f"Authentication failure. (U:{uname} - IP:{ip}) [{msg}]" ) abort(401, msg) return if validated_user: LOGGER.info(f"Login successful. (U:{uname} - IP:{ip})") if not set(self.required_priv).intersection(set(priv)): abort( 403, "The method you've used to login does not give you access to this API" ) return return validated_user return None
def format_result(user_classification, r, min_classification, build_hierarchy=False): if not CLASSIFICATION.is_accessible(user_classification, min_classification): return None # Drop sections user does not have access and set others to at least min classification max_classification, r['result']['sections'] = filter_sections( r['result']['sections'], user_classification, min_classification) # Drop supplementary and extracted files that the user does not have access to for ftype in ['supplementary', 'extracted']: r['response'][ftype] = [ x for x in r['response'][ftype] if CLASSIFICATION.is_accessible( user_classification, x['classification']) ] # Set result classification to at least min but no more then viewable result classification r['classification'] = CLASSIFICATION.max_classification( max_classification, min_classification) if build_hierarchy: try: section_hierarchy, _ = build_heirarchy_rec(r['result']['sections']) r['section_hierarchy'] = section_hierarchy['children'] except InvalidSectionList: LOGGER.warning( f"Could not generate section hierarchy for {r['response']['service_name']} " f"service. Will use old display method.") return r
def signup_validate(**_): """ Validate a user's signup request Variables: None Arguments: None Data Block: { "registration_key": "234234...ADFCB" # Key used to validate the user's signup process } Result example: { "success": true } """ if not config.auth.internal.signup.enabled: return make_api_response({"success": False}, "Signup process has been disabled", 403) try: data = request.json except BadRequest: data = request.values registration_key = data.get('registration_key', None) if registration_key: try: signup_queue = get_signup_queue(registration_key) members = signup_queue.members() signup_queue.delete() if members: user_info = members[0] # Add dynamic classification group user_info['classification'] = get_dynamic_classification( user_info.get('classification', Classification.UNRESTRICTED), user_info['email']) user = User(user_info) username = user.uname STORAGE.user.save(username, user) return make_api_response({"success": True}) except (KeyError, ValueError) as e: LOGGER.warning(f"Fail to signup user: {str(e)}") pass else: return make_api_response( {"success": False}, "Not enough information to proceed with user creation", 400) return make_api_response({"success": False}, "Invalid registration key", 400)
def get_file_infos(keys): infos = {} missing = [] retry = 0 while keys and retry < max_retry: if retry: time.sleep(2 ** (retry - 7)) try: infos.update(STORAGE.file.multiget(keys, as_obj=False)) except MultiKeyError as e: LOGGER.warning(f"Trying to get multiple files but some are missing: {str(e.keys)}") infos.update(e.partial_output) missing.extend(e.keys) keys = [x for x in keys if x not in infos and x not in missing] retry += 1 return infos, missing
def get_errors(keys): out = {} err = {} missing = [] retry = 0 while keys and retry < max_retry: if retry: time.sleep(2 ** (retry - 7)) try: err.update(STORAGE.error.multiget(keys, as_obj=False)) except MultiKeyError as e: LOGGER.warning(f"Trying to get multiple errors but some are missing: {str(e.keys)}") err.update(e.partial_output) missing.extend(e.keys) keys = [x for x in keys if x not in err and x not in missing] retry += 1 out["errors"] = err out["missing_error_keys"] = keys + missing return out
def generate_ontology_file(results, user, updates={}, fnames={}): # Load ontology files sio = StringIO() for r in results: for supp in r.get('response', {}).get('supplementary', {}): if supp['name'].endswith('.ontology'): try: ontology = json.loads(FILESTORE.get(supp['sha256'])) sha256 = ontology['header']['sha256'] c12n = ontology['header']['classification'] if sha256 == r['sha256'] and Classification.is_accessible( user['classification'], c12n): # Update the ontology with the live values ontology['header'].update(updates) # Set filenames if any if sha256 in fnames: ontology['header']['filenames'] = fnames[sha256] elif 'filenames' in ontology['header']: del ontology['header']['filenames'] # Make sure parent is not equal to current hash if 'parent' in ontology['header'] and ontology[ 'header']['parent'] == sha256: del ontology['header']['parent'] sio.write( json.dumps( ontology, indent=None, separators=(',', ':')) + '\n') except Exception as e: LOGGER.warning( f"An error occured while fetching ontology files: {str(e)}" ) # Flush and reset buffer sio.flush() return sio
def get_file_submission_results(sid, sha256, **kwargs): """ Get the all the results and errors of a specific file for a specific Submission ID Variables: sid => Submission ID to get the result for sha256 => Resource locator to get the result for Arguments (POST only): extra_result_keys => List of extra result keys to get extra_error_keys => List of extra error keys to get Data Block: None Result example: {"errors": [], # List of error blocks "file_info": {}, # File information block (md5, ...) "results": [], # List of result blocks "tags": [] } # List of generated tags """ user = kwargs['user'] # Check if submission exist data = STORAGE.submission.get(sid, as_obj=False) if data is None: return make_api_response("", "Submission ID %s does not exists." % sid, 404) if data and user and Classification.is_accessible(user['classification'], data['classification']): # Prepare output output = { "file_info": {}, "results": [], "tags": {}, "errors": [], "attack_matrix": {}, 'heuristics': {}, "signatures": set() } # Extra keys - This is a live mode optimisation res_keys = data.get("results", []) err_keys = data.get("errors", []) if request.method == "POST" and request.json is not None and data[ 'state'] != "completed": extra_rkeys = request.json.get("extra_result_keys", []) extra_ekeys = request.json.get("extra_error_keys", []) # Load keys res_keys.extend(extra_rkeys) err_keys.extend(extra_ekeys) res_keys = list(set(res_keys)) err_keys = list(set(err_keys)) # Get File, results and errors temp_file = STORAGE.file.get(sha256, as_obj=False) if not temp_file: output['file_info']['sha256'] = sha256 output['signatures'] = list(output['signatures']) output['missing'] = True return make_api_response( output, "The file you are trying to view is missing from the system", 404) if not Classification.is_accessible(user['classification'], temp_file['classification']): return make_api_response( "", "You are not allowed to view the data of this file", 403) output['file_info'] = temp_file max_c12n = output['file_info']['classification'] temp_results = list( STORAGE.get_multiple_results( [x for x in res_keys if x.startswith(sha256)], cl_engine=Classification, as_obj=False).values()) results = [] for r in temp_results: r = format_result(user['classification'], r, temp_file['classification'], build_hierarchy=True) if r: max_c12n = Classification.max_classification( max_c12n, r['classification']) results.append(r) output['results'] = results try: output['errors'] = STORAGE.error.multiget( [x for x in err_keys if x.startswith(sha256)], as_obj=False, as_dictionary=False) except MultiKeyError as e: LOGGER.warning( f"Trying to get multiple errors but some are missing: {str(e.keys)}" ) output['errors'] = e.partial_output output['metadata'] = STORAGE.get_file_submission_meta( sha256, config.ui.statistics.submission, user["access_control"]) for res in output['results']: for sec in res['result']['sections']: h_type = "info" if sec.get('heuristic', False): # Get the heuristics data if sec['heuristic']['score'] < 100: h_type = "info" elif sec['heuristic']['score'] < 1000: h_type = "suspicious" else: h_type = "malicious" item = (sec['heuristic']['heur_id'], sec['heuristic']['name']) output['heuristics'].setdefault(h_type, []) if item not in output['heuristics'][h_type]: output['heuristics'][h_type].append(item) # Process Attack matrix for attack in sec['heuristic'].get('attack', []): attack_id = attack['attack_id'] for cat in attack['categories']: output['attack_matrix'].setdefault(cat, []) item = (attack_id, attack['pattern'], h_type) if item not in output['attack_matrix'][cat]: output['attack_matrix'][cat].append(item) # Process Signatures for signature in sec['heuristic'].get('signature', []): sig = (signature['name'], h_type) if sig not in output['signatures']: output['signatures'].add(sig) # Process tags for t in sec['tags']: output["tags"].setdefault(t['type'], {}) current_htype = output["tags"][t['type']].get( t['value'], None) if not current_htype: output["tags"][t['type']][t['value']] = h_type else: if current_htype == 'malicious' or h_type == 'malicious': output["tags"][t['type']][t['value']] = 'malicious' elif current_htype == 'suspicious' or h_type == 'suspicious': output["tags"][t['type']][ t['value']] = 'suspicious' else: output["tags"][t['type']][t['value']] = 'info' for t_type in output["tags"]: output["tags"][t_type] = [ (k, v) for k, v in output['tags'][t_type].items() ] output['signatures'] = list(output['signatures']) output['file_info']['classification'] = max_c12n return make_api_response(output) else: return make_api_response( "", "You are not allowed to view the data of this submission", 403)
def login(**_): """ Login the user onto the system Variables: None Arguments: None Data Block: { "user": <UID>, "password": <ENCRYPTED_PASSWORD>, "otp": <OTP_TOKEN>, "apikey": <ENCRYPTED_APIKEY>, "webauthn_auth_resp": <RESPONSE_TO_CHALLENGE_FROM_WEBAUTHN> } Result example: { "username": <Logged in user>, # Username for the logged in user "privileges": ["R", "W"], # Different privileges that the user will get for this session "session_duration": 60 # Time after which this session becomes invalid # Note: The timer reset after each call } """ data = request.json if not data: data = request.values user = data.get('user', None) password = data.get('password', None) apikey = data.get('apikey', None) webauthn_auth_resp = data.get('webauthn_auth_resp', None) oauth_provider = data.get('oauth_provider', None) oauth_token = data.get('oauth_token', None) if config.auth.oauth.enabled and oauth_provider: oauth = current_app.extensions.get('authlib.integrations.flask_client') provider = oauth.create_client(oauth_provider) if provider: redirect_uri = f'https://{request.host}/login.html?provider={oauth_provider}' return provider.authorize_redirect(redirect_uri=redirect_uri) try: otp = int(data.get('otp', 0) or 0) except Exception: raise AuthenticationException('Invalid OTP token') if (user and password) or (user and apikey) or (user and oauth_token): auth = { 'username': user, 'password': password, 'otp': otp, 'webauthn_auth_resp': webauthn_auth_resp, 'apikey': apikey, 'oauth_token': oauth_token } logged_in_uname = None ip = request.headers.get("X-Forwarded-For", request.remote_addr) try: logged_in_uname, priv = default_authenticator( auth, request, flsk_session, STORAGE) session_duration = config.ui.session_duration cur_time = now() xsrf_token = generate_random_secret() current_session = { 'duration': session_duration, 'ip': ip, 'privileges': priv, 'time': int(cur_time) - (int(cur_time) % session_duration), 'user_agent': request.headers.get("User-Agent", None), 'username': logged_in_uname, 'xsrf_token': xsrf_token } session_id = hashlib.sha512( str(current_session).encode("UTF-8")).hexdigest() current_session['expire_at'] = cur_time + session_duration flsk_session['session_id'] = session_id KV_SESSION.add(session_id, current_session) return make_api_response( { "username": logged_in_uname, "privileges": priv, "session_duration": session_duration }, cookies={'XSRF-TOKEN': xsrf_token}) except AuthenticationException as wpe: uname = auth.get('username', '(None)') LOGGER.warning( f"Authentication failure. (U:{uname} - IP:{ip}) [{wpe}]") return make_api_response("", err=str(wpe), status_code=401) finally: if logged_in_uname: LOGGER.info( f"Login successful. (U:{logged_in_uname} - IP:{ip})") return make_api_response( "", "Not enough information to proceed with authentication", 401)
def signup(**_): """ Signup a new user into the system Variables: None Arguments: None Data Block: { "user": <UID>, "password": <DESIRED_PASSWORD>, "password_confirm": <DESIRED_PASSWORD_CONFIRMATION>, "email": <EMAIL_ADDRESS> } Result example: { "success": true } """ if not config.auth.internal.signup.enabled: return make_api_response({"success": False}, "Signup process has been disabled", 403) try: data = request.json except BadRequest: data = request.values uname = data.get('user', None) password = data.get('password', None) password_confirm = data.get('password_confirm', None) email = data.get('email', None) if not uname or not password or not password_confirm or not email: return make_api_response( {"success": False}, "Not enough information to proceed with user creation", 400) if STORAGE.user.get(uname) or len(uname) < 3: return make_api_response( {"success": False}, "There is already a user registered with this name", 460) else: for c in uname: if not 97 <= ord(c) <= 122 and not ord(c) == 45: return make_api_response( {"success": False}, "Invalid username. [Lowercase letters and dashes " "only with at least 3 letters]", 460) if password_confirm != password: return make_api_response("", "Passwords do not match", 469) password_requirements = config.auth.internal.password_requirements.as_primitives( ) if not check_password_requirements(password, **password_requirements): error_msg = get_password_requirement_message(**password_requirements) return make_api_response({"success": False}, error_msg, 469) if STORAGE.user.search(f"email:{email.lower()}").get('total', 0) != 0: return make_api_response( {"success": False}, "There is already a user registered with this email address", 466) # Normalize email address email = email.lower() email_valid = False for r in config.auth.internal.signup.valid_email_patterns: matcher = re.compile(r) if matcher.findall(email): email_valid = True break if not email_valid: extra = "" if config.ui.email: extra = f". Contact {config.ui.email} for more information." return make_api_response({"success": False}, f"Invalid email address{extra}", 466) password = get_password_hash(password) key = hashlib.sha256( get_random_password(length=512).encode('utf-8')).hexdigest() try: send_signup_email(email, key) get_signup_queue(key).add({ "uname": uname, "password": password, "email": email, "groups": ['USERS'], "name": uname }) except Exception as e: LOGGER.warning(f"Sending email for signup process failed: {str(e)}") return make_api_response( {"success": False}, "The system failed to send signup confirmation link.", 400) return make_api_response({"success": True})
def reset_pwd(**_): """ Reset the password for the specified reset ID Variables: None Arguments: None Data Block: { "reset_id": <RESET_HASH>, "password": <PASSWORD TO RESET TO>, "password_confirm": <CONFIRMATION OF PASSWORD TO RESET TO> } Result example: { "success": true } """ if not config.auth.internal.signup.enabled: return make_api_response({"success": False}, "Signup process has been disabled", 403) try: data = request.json except BadRequest: data = request.values reset_id = data.get('reset_id', None) password = data.get('password', None) password_confirm = data.get('password_confirm', None) if reset_id and password and password_confirm: if password != password_confirm: return make_api_response({"success": False}, err="Password mismatch", status_code=469) password_requirements = config.auth.internal.password_requirements.as_primitives( ) if not check_password_requirements(password, **password_requirements): error_msg = get_password_requirement_message( **password_requirements) return make_api_response({"success": False}, error_msg, 469) try: reset_queue = get_reset_queue(reset_id) members = reset_queue.members() reset_queue.delete() if members: email = members[0] res = STORAGE.user.search(f"email:{email}", fl="*") if res.get('total', 0) == 1: user = res['items'][0] user.password = get_password_hash(password) STORAGE.user.save(user.uname, user) return make_api_response({"success": True}) except Exception as e: LOGGER.warning(f"Failed to reset the user's password: {str(e)}") pass return make_api_response( {"success": False}, err= "This reset link has expired, please restart the password reset process", status_code=403) return make_api_response({"success": False}, err="Invalid parameters passed", status_code=400)
def get_multiple_service_results(**kwargs): """ Get multiple result and error keys at the same time Variables: None Arguments: None Data Block: {"error": [], #List of error keys to lookup "result": [] #List of result keys to lookup } Result example: {"error": {}, #Dictionary of error object matching the keys "result": {} #Dictionary of result object matching the keys } """ user = kwargs['user'] data = request.json try: errors = STORAGE.error.multiget(data.get('error', []), as_dictionary=True, as_obj=False) except MultiKeyError as e: LOGGER.warning( f"Trying to get multiple errors but some are missing: {str(e.keys)}" ) errors = e.partial_output results = STORAGE.get_multiple_results(data.get('result', []), CLASSIFICATION, as_obj=False) try: file_infos = STORAGE.file.multiget(list( set([x[:64] for x in results.keys()])), as_dictionary=True, as_obj=False) except MultiKeyError as e: LOGGER.warning( f"Trying to get multiple files but some are missing: {str(e.keys)}" ) file_infos = e.partial_output for r_key in list(results.keys()): r_value = format_result(user['classification'], results[r_key], file_infos.get(r_key[:64], {}).get( 'classification', CLASSIFICATION.UNRESTRICTED), build_hierarchy=True) if not r_value: del results[r_key] else: results[r_key] = r_value out = {"error": errors, "result": results} return make_api_response(out)