def get_file_infos(keys): infos = {} retry = 0 while keys and retry < max_retry: if retry: time.sleep(2 ** (retry - 7)) infos.update(STORAGE.get_files_dict(keys)) keys = [x for x in keys if x not in infos] retry += 1 return infos
def list_heuritics_stats(**kwargs): """ Gather all heuristics stats in system Variables: None Arguments: None Data Block: None Result example: {"total": 201, # Total heuristics found "timestamp": # Timestamp of last heuristics stats "items": # List of heuristics [{"id": "AL_HEUR_001", # Heuristics ID "count": "100", # Count of times heuristics seen "min": 0, # Lowest score found "avg": 172, # Average of all scores "max": 780, # Highest score found }, ... """ user = kwargs['user'] output = {"total": 0, "items": [], "timestamp": None} heur_blob = STORAGE.get_blob("heuristics_stats") if heur_blob: cleared = [] try: for k, v in heur_blob["stats"].iteritems(): heur_id = k if heur_id in HEUR_MAP: if user and Classification.is_accessible(user['classification'], HEUR_MAP[heur_id]['classification']) and v[0] != 0: cleared.append({ "id": heur_id, "count": v[0], "min": v[1], "avg": int(v[2]), "max": v[3], "classification": HEUR_MAP[heur_id]['classification'] }) except AttributeError: pass output["items"] = cleared output["total"] = len(cleared) output["timestamp"] = heur_blob["timestamp"] return make_api_response(output)
def get_summary(sid, **kwargs): """ Retrieve the executive summary of a given submission ID. This is a MAP of tags to SRL combined with a list of generated Tags. Variables: sid => Submission ID to get the summary for Arguments: None Data Block: None Result example: {"map": { # Map of TAGS to SRL "TYPE__VAL": [ # Type and value of the tags "SRL" # List of related SRLs ...], "SRL": [ # SRL "TYPE__VAL" # List of related type/value ...], ... } "tags": { # Dictionary of tags "TYPE": { # Type of tag "VALUE": { # Value of the tag "usage": "", # Usage "classification": "" # Classification }, ... }, ... } """ user = kwargs['user'] data = STORAGE.get_submission(sid) if data is None: return make_api_response("", "Submission ID %s does not exists." % sid, 404) if user and Classification.is_accessible(user['classification'], data['classification']): return make_api_response(STORAGE.create_summary(data, user)) else: return make_api_response("", "You are not allowed to view the data of this submission", 403)
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 get_multiple_service_keys(**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 errors = STORAGE.get_errors_dict(data['error']) results = STORAGE.get_results_dict(data['result']) srls = list(set([x[:64] for x in results.keys()])) file_infos = STORAGE.get_files_dict(srls) for r_key in results.keys(): r_value = format_result(user['classification'], results[r_key], file_infos[r_key[:64]]['classification']) if not r_value: del results[r_key] else: results[r_key] = r_value out = {"error": errors, "result": results} return make_api_response(out)
def add_workflow(**_): """ Add a workflow to the system Variables: None Arguments: None Data Block: { "name": "Workflow name", # Name of the workflow "classification": "", # Max classification for workflow "label": ['label1'], # Labels for the workflow "priority": "LOW", # Priority of the workflow "status": "MALICIOUS", # Status of the workflow "query": "*:*" # Query to match the data } Result example: { "success": true # Saving the user info succeded } """ data = request.json name = data.get('name', None) query = data.get('query', None) if not name: return make_api_response({"success": False}, err="Name field is required", status_code=400) if not query: return make_api_response({"success": False}, err="Query field is required", status_code=400) STORAGE.save_workflow(str(uuid.uuid4()), data) return make_api_response({"success": True})
def search_all(*_, **kwargs): """ Search through all relevant buckets for a given query. Uses Apache Solr Search language for query. Variables: None Arguments: None Data Block: {"query": "query", # Query to search for "offset": 0, # Offset in the results "length": 100} # Max number of results Result example: {"files": # File results {"total": 201, # Total results found "offset": 0, # Offset in the result list "count": 100, # Number of results returned "bucket": file, # Name of the bucket queried "items": []}, # List of file results "results": # Result results {...}, # Same layout as file results "submission": # Submission results {...}, # Same layout as file results } """ user = kwargs['user'] if request.method == "POST": offset = request.json.get('offset', 0) length = request.json.get('length', 100) query = request.json.get('query', "") else: offset = int(request.args.get('offset', 0)) length = int(request.args.get('length', 100)) query = request.args.get('query', "") if not query: return make_api_response("", "The specified search query is not valid.", 400) try: return make_api_response(STORAGE.search_all(query, start=offset, rows=length, access_control=user['access_control'])) 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 get_submission(sid, **kwargs): """ Get the submission details for a given Submission ID Variables: sid => Submission ID to get the details for Arguments: None Data Block: None Result example: {"files": [ # List of source files ["FNAME", "SRL"], ...], # Each file = List of name/srl "errors": [], # List of error keys (SRL.ServiceName) "submission": { # Submission Block "profile": true, # Should keep stats about execution? "description": "", # Submission description "ttl": 30, # Submission days to live "ignore_filtering": false, # Ignore filtering services? "priority": 1000, # Submission priority, higher = faster "ignore_cache": true, # Force reprocess even is result exist? "groups": ["group", ...], # List of groups with access "sid": "ab9...956", # Submission ID "submitter": "user", # Uname of the submitter "max_score": 1422, # Score of highest scoring file "ignore_tag": false }, # Send all files to all service? "results": [], # List of Results keys (SRL.ServiceName.Version.Config) "times": { # Timing block "completed": "2014-...", # Completed time "submitted": "2014-..." # Submitted time }, "state": "completed", # State of the submission "services": { # Service Block "selected": ["mcafee"], # List of selected services "params": {}, # Service specific parameters "excluded": [] # List of excluded services } } """ user = kwargs['user'] data = STORAGE.get_submission(sid) 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']): return make_api_response(data) else: return make_api_response("", "You are not allowed to view the data of this submission", 403)
def get_file_tree(sid, **kwargs): """ Get the file hierarchy of a given Submission ID. This is an N deep recursive process but is limited to the max depth set in the system settings. Variables: sid => Submission ID to get the tree for Arguments: None Data Block: None API call example: /api/v3/submission/tree/12345678-1234-1234-1234-1234567890AB/ Result example: { # Dictionary of file blocks "1f...11": { # File SRL (sha256) "score": 923, # Score for the file "name": ["file.exe",...] # List of possible names for the file "children": {...} # Dictionary of children file blocks }, ... """ user = kwargs['user'] data = STORAGE.get_submission(sid) 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']): output = STORAGE.create_file_tree(data) return make_api_response(output) else: return make_api_response("", "You are not allowed to view the data of this submission", 403)
def disable_otp(**kwargs): """ Disable OTP for the currently logged in user Variables: None Arguments: None Data Block: None Result example: { "success": true } """ uname = kwargs['user']['uname'] user_data = STORAGE.get_user(uname) user_data.pop('otp_sk', None) STORAGE.save_user(uname, user_data) return make_api_response({"success": True})
def clear(**kwargs): """ Remove currently configured security token Variables: None Arguments: None Data Block: None Result example: { "success": true } """ uname = kwargs['user']['uname'] user = STORAGE.get_user(uname) user.pop('u2f_devices', None) STORAGE.save_user(uname, user) return make_api_response({'success': True})
def list_submissions_for_group(group, **kwargs): """ List all submissions of a given group. Variables: None Arguments: offset => Offset at which we start giving submissions length => Numbers of submissions to return filter => Filter to apply to the submission list Data Block: None Result example: {"total": 201, # Total results found "offset": 0, # Offset in the result list "count": 100, # Number of results returned "items": [ # List of submissions {"submission": { # Submission Block "description": "", # Description of the submission "sid": "ad2...234", # Submission ID "groups": "GROUP", # Accessible groups "ttl": "30", # Days to live "submitter": "user", # ID of the submitter "max_score": "1422"}, # Max score of all files "times": { # Timing "submitted": # Time submitted "2014-06-17T19:20:19Z"}, "state": "completed" # State of the submission }, ... ]} """ user = kwargs['user'] offset = int(request.args.get('offset', 0)) length = int(request.args.get('length', 100)) query = request.args.get('filter', "*") if group == "ALL": group = "*" try: return make_api_response(STORAGE.list_submissions_group(group, start=offset, rows=length, qfilter=query, access_control=user['access_control'])) 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 resubmit_submission_for_analysis(sid, *args, **kwargs): """ Resubmit a submission for analysis with the exact same parameters as before Variables: sid => Submission ID to re-submit Arguments: None Data Block: None Result example: { "submission":{}, # Submission Block "request": {}, # Request Block "times": {}, # Timing Block "state": "submitted", # Submission state "services": {}, # Service selection Block "fileinfo": {} # File information Block } """ user = kwargs['user'] submission = STORAGE.get_submission(sid) if submission: if not Classification.is_accessible(user['classification'], submission['classification']): return make_api_response("", "You are not allowed to re-submit a submission that you don't have access to", 403) task = {k: v for k, v in submission['submission'].iteritems() if k not in STRIP_KW} task.update({k: v for k, v in submission['services'].iteritems() if k not in STRIP_KW}) task['classification'] = submission['classification'] else: return make_api_response({}, "Submission %s does not exists." % sid, status_code=404) task['submitter'] = user['uname'] if 'priority' not in task: task['priority'] = 500 names = [] for name, _ in submission["files"]: names.append(name) task['description'] = "Resubmit %s for analysis" % ", ".join(names) with forge.get_filestore() as f_transport: return make_api_response(SubmissionWrapper.submit_multi(STORAGE, f_transport, submission["files"], **task))
def agree_with_tos(username, **kwargs): """ Specified user send agreement to Terms of Service Variables: username => Name of the user that agrees with tos Arguments: None Data Block: None Result example: { "success": true # Saving the user info succeded } """ logged_in_user = kwargs['user'] if logged_in_user['uname'] != username: return make_api_response( {"success": False}, "You can't agree to Terms Of Service on behalf of someone else!", 400) user = STORAGE.get_user_account(username) if not user: return make_api_response({"success": False}, "User %s does not exist." % username, 403) else: user['agrees_with_tos'] = now_as_iso() if config.ui.get('tos_lockout', False): user['is_active'] = False STORAGE.save_user(username, user) return make_api_response({"success": True})
def add_virtual_machine(vm, **_): """ Add the vm configuration to the system Variables: vm => Name of the vm Arguments: None Data Block: { enabled: true, # Is VM enabled name: "Extract", # Name of the VM num_workers: 1, # Number of service workers os_type: "windows", # Type of OS os_variant: "win7", # Variant of OS ram: 1024, # Amount of RAM revert_every: 600, # Auto revert seconds interval vcpus: 1, # Number of CPUs virtual_disk_url: "img.qcow2" # Name of the virtual disk to download } Result example: { "success" : True } """ data = request.json if not STORAGE.get_virtualmachine(vm): STORAGE.save_virtualmachine(vm, data) return make_api_response({"success": True}) else: return make_api_response({"success": False}, "You cannot add a vm that already exists...", 400)
def alert_detail(*_, **kwargs): user = kwargs['user'] alert_key = angular_safe(request.args.get("alert_key", None)) if not alert_key: abort(404) alert = STORAGE.get_alert(alert_key) if user and alert and Classification.is_accessible( user['classification'], alert['classification']): return custom_render("alert_detail.html", alert_key=alert_key, **kwargs) else: abort(403)
def get_errors(keys): out = {} err = {} retry = 0 while keys and retry < max_retry: if retry: time.sleep(2 ** (retry - 7)) err.update(STORAGE.get_errors_dict(keys)) keys = [x for x in err_keys if x not in err] retry += 1 out["errors"] = err out["missing_error_keys"] = keys return out
def search_help(**kwargs): field_list = { k: sorted([(x, y) for x, y in v.iteritems()]) for k, v in STORAGE.generate_field_list(False).iteritems() } lookup = { "text_ws": "whitespace separated text", "text_ws_dsplit": "dot and whitespace separated text", "text_general": "tokenized text", "text_fuzzy": "separated fuzzy patterns", } return custom_render("search_help.html", field_list=field_list, lookup=lookup, **kwargs)
def delete_apikey(name, **kwargs): """ Delete an API Key matching specified name for the currently logged in user Variables: name => Name of the API key Arguments: None Data Block: None Result example: { "success": True } """ user = kwargs['user'] user_data = STORAGE.get_user(user['uname']) user_data['apikeys'] = [x for x in user_data.get('apikeys', []) if x[0] != name] STORAGE.save_user(user['uname'], user_data) return make_api_response({"success": True})
def create_bundle(sid, **kwargs): """ Creates a bundle containing the submission results and the associated files Variables: sid => ID of the submission to create the bundle for Arguments: None Data Block: None API call example: /api/v3/bundle/create/234f334-...-31232/ Result example: -- THE BUNDLE FILE BINARY -- """ user = kwargs['user'] submission = STORAGE.get_submission(sid) if user and submission and Classification.is_accessible( user['classification'], submission['classification']): temp_target_file = None try: temp_target_file = bundle_create(sid, working_dir=WORKING_DIR) f_size = os.path.getsize(temp_target_file) return stream_file_response(open(temp_target_file, 'rb'), "%s.al_bundle" % sid, f_size) except SubmissionNotFound as snf: return make_api_response( "", "Submission %s does not exist. [%s]" % (sid, snf.message), 404) except BundlingException as be: return make_api_response( "", "An error occured while bundling submission %s. [%s]" % (sid, be.message), 404) finally: try: if temp_target_file: os.remove(temp_target_file) except: pass else: return make_api_response( "", "You are not allowed create a bundle for this submission...", 403)
def alerts_statuses(**kwargs): """ Run a facet search to find the different statuses matching the query. Variables: None Arguments: start_time => Time offset at which to list alerts time_slice => Length after the start time that we query filter => Filter to apply to the alert list fq => Post filter queries (you can have multiple of those) Data Block: None Result example: """ user = kwargs['user'] 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.get_alert_statistics(query, access_control=user['access_control'], fq_list=filter_queries, start_time=start_time, time_slice=time_slice, field_list=['status' ]).get('status', [])) 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 get_file_information(srl, **kwargs): """ Get information about the file like: Hashes, size, frequency count, etc... Variables: srl => A resource locator for the file (sha256) Arguments: None Data Block: None API call example: /api/v3/file/info/123456...654321/ Result example: { # File information block "ascii": "PK..", # First 64 bytes as ASCII "classification": "UNRESTRICTED", # Access control for the file "entropy": 7.99, # File's entropy "hex": "504b...c0b2", # First 64 bytes as hex "magic": "Zip archive data", # File's identification description (from magic) "md5": "8f31...a048", # File's MD5 hash "mime": "application/zip", # Mimetype of the file (from magic) "seen_count": 7, # Number of time we've seen this file "seen_first": "2015-03-04T21:59:13.204861Z", # Time at which we first seen this file "seen_last": "2015-03-10T19:42:04.587233Z", # Last time we've seen the file "sha256": "e021...4de2", # File's sha256 hash "sha1": "354f...fdab", # File's sha1 hash "size": 3417, # Size of the file "ssdeep": "4:Smm...OHY+", # File's SSDEEP hash "tag": "archive/zip" # Type of file that we identified } """ 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(file_obj) 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 file_viewer(**kwargs): user = kwargs['user'] srl = angular_safe(request.args.get("srl", None)) if not srl: abort(404) data = STORAGE.get_file(srl) if not data: abort(404) if not Classification.is_accessible(user['classification'], data['classification']): abort(403) return custom_render("file_viewer.html", srl=srl, **kwargs)
def get_heuristic(heuristic_id, **kwargs): """ Get a specific heuristic's detail from the system Variables: heuristic_id => ID of the heuristic Arguments: None Data Block: None Result example: {"id": "AL_HEUR_001", # Heuristics ID "filetype": ".*", # Target file type "name": "HEURISTICS_NAME", # Heuristics name "description": ""} # Heuristics description """ user = kwargs['user'] h = deepcopy(HEUR_MAP.get(heuristic_id, None)) if not h: return make_api_response("", "Not found", 404) # Add counters h["count"] = 0 h["min"] = 0 h["avg"] = 0 h["max"] = 0 heur_blob = STORAGE.get_blob("heuristics_stats") if heur_blob: data = heur_blob.get('stats', {}).get(heuristic_id, None) if data: h["count"] = data[0] h["min"] = data[1] h["avg"] = data[2] h["max"] = data[3] if user and Classification.is_accessible(user['classification'], h['classification']): return make_api_response(h) else: return make_api_response("", "You are not allowed to see this heuristic...", 403)
def get_user_favorites(username, **kwargs): """ Loads the user's favorites. Variables: username => Name of the user you want to get the avatar for Arguments: None Data Block: None Result example: { # Dictionary of "<name_of_query>": # Named queries "*:*", # The actual query to run ... } """ user = kwargs['user'] favorites = { "alert": [], "search": [], "signature": [], "submission": [], "error": [] } res_favorites = STORAGE.get_user_favorites(username) if res_favorites: if username == "__global__" or username != user['uname']: for key in favorites.keys(): for fav in res_favorites[key]: if 'classification' in fav: if CLASSIFICATION.is_accessible( user['classification'], fav['classification']): favorites[key].append(fav) else: favorites[key].append(fav) else: favorites.update(res_favorites) return make_api_response(favorites)
def set_host(mac, *args, **kwargs): """ Set the host information Variables: mac => MAC Address of the host to get the info Arguments: None Data Block: { "profile": "Default profile", # Host current profile "machine_info": { # Host Machine info block "uid": "Core-001122334455", # Machine UID "ip": "127.0.0.1", # Machine IP "memory": "23.5", # Machine RAM (GB) "cores": 16, # Machine Num Cores "os": "Linux", # Machine OS "name": "computer1" }, # Machine Name "ip": "127.0.0.1", # Host IP "hostname": "computer1", # Host Name "enabled": true, # Is host enabled? "platform": { # Host platform block "node": "computer1", # Node name "system": "Linux", # Node system "machine": "x86_64", # Node Architecture "version": "#47-Ubuntu SMP", # Node Kernel version "release": "3.13.0-24", # Node Kernel release "proc": "x86_64" }, # Node proc Architecture "mac_address": "001122334455" # Host Mac address } Result example: { "status": "success" # Was saving successful ? } """ data = request.json if mac != data['mac_address']: raise AccessDeniedException("You are not allowed to change the host MAC Address.") return make_api_response(STORAGE.save_node(mac, data))
def get_user_avatar(username, **_): """ Loads the user's avatar. Variables: username => Name of the user you want to get the avatar for Arguments: None Data Block: None Result example: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD..." """ avatar = STORAGE.get_user_avatar(username) return make_api_response(avatar)
def list_signatures(**kwargs): """ List all the signatures in the system. Variables: None Arguments: offset => Offset at which we start giving signatures length => Numbers of signatures to return filter => Filter to apply on the signature list Data Block: None Result example: {"total": 201, # Total signatures found "offset": 0, # Offset in the signature list "count": 100, # Number of signatures returned "items": [{ # List of Signatures: "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'] offset = int(request.args.get('offset', 0)) length = int(request.args.get('length', 100)) query = request.args.get('filter', "meta.id:*") try: return make_api_response(STORAGE.list_signatures(start=offset, rows=length, query=query, access_control=user['access_control'])) 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 list_users(**_): """ List all users of the system. Variables: None Arguments: offset => Offset in the user bucket length => Max number of user returned filter => Filter to apply to the user list Data Block: None Result example: { "count": 100, # Max number of users "items": [{ # List of user blocks "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 (Always null here) "groups": ["TEST"] # Groups the user is member of }, ...], "total": 10, # Total number of users "offset": 0 # Offset in the user bucket } """ offset = int(request.args.get('offset', 0)) length = int(request.args.get('length', 100)) query = request.args.get('filter', None) try: return make_api_response( STORAGE.list_users(start=offset, rows=length, query=query)) 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 login(): if request.environ.get("HTTP_X_REMOTE_CERT_VERIFIED", "FAILURE") == "SUCCESS": dn = ",".join( request.environ.get("HTTP_X_REMOTE_DN").split("/")[::-1][:-1]) else: dn = "" avatar = None username = '' alternate_login = '******' if dn: u_list = STORAGE.advanced_search('user', 'dn:"%s"' % dn, args=[('fl', '_yz_rk') ])['response']['docs'] if len(u_list): username = u_list[0]['_yz_rk'] avatar = STORAGE.get_user_avatar( username) or "/static/images/user_default.png" alternate_login = '******' else: try: username = dn.rsplit('CN=', 1)[1] except IndexError: username = dn avatar = "/static/images/user_default.png" alternate_login = '******' if config.auth.get('encrypted_login', True): public_key = STORAGE.get_blob('id_rsa.pub') if not public_key: public_key, private_key = generate_async_keys( key_size=config.ui.get('rsa_key_size', 2048)) STORAGE.save_blob('id_rsa.pub', public_key) STORAGE.save_blob('id_rsa', private_key) else: public_key = None next_url = angular_safe(request.args.get('next', "/")) return custom_render("login.html", next=next_url, public_key=public_key, avatar=avatar, username=username, alternate_login=alternate_login)