зеркало из https://github.com/mozilla/MozDef.git
473 строки
12 KiB
Python
473 строки
12 KiB
Python
#!/usr/bin/env python
|
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
# Copyright (c) 2016 Mozilla Corporation
|
|
# Author: gdestuynder@mozilla.com
|
|
|
|
# Imports auth0.com logs into MozDef
|
|
|
|
import hjson
|
|
import sys
|
|
import os
|
|
import requests
|
|
import mozdef_client as mozdef
|
|
try:
|
|
import urllib.parse
|
|
quote_url = urllib.parse.quote
|
|
except ImportError:
|
|
#Well hello there python2 user!
|
|
import urllib
|
|
quote_url = urllib.quote
|
|
import traceback
|
|
|
|
|
|
class DotDict(dict):
|
|
'''dict.item notation for dict()'s'''
|
|
__getattr__ = dict.__getitem__
|
|
__setattr__ = dict.__setitem__
|
|
__delattr__ = dict.__delitem__
|
|
|
|
def __init__(self, dct):
|
|
for key, value in dct.items():
|
|
if hasattr(value, 'keys'):
|
|
value = DotDict(value)
|
|
self[key] = value
|
|
|
|
|
|
def fatal(msg):
|
|
print(msg)
|
|
sys.exit(1)
|
|
|
|
|
|
def debug(msg):
|
|
sys.stderr.write('+++ {}\n'.format(msg))
|
|
|
|
|
|
# This is from https://auth0.com/docs/api/management/v2#!/Logs/get_logs
|
|
# and https://github.com/auth0/auth0-logs-to-logentries/blob/master/index.js (MIT)
|
|
# levels
|
|
# 0 = Debug
|
|
# 1 = Info
|
|
# 2 = Warning
|
|
# 3 = Error
|
|
# 4 = Critical
|
|
log_types = DotDict({
|
|
's': {
|
|
"event": 'Success Login',
|
|
"level": 1
|
|
},
|
|
'slo': {
|
|
"event": 'Success Logout',
|
|
"level": 1
|
|
},
|
|
'flo': {
|
|
"event": 'Failed Logout',
|
|
"level": 3
|
|
},
|
|
'seacft': {
|
|
"event": 'Success Exchange (Authorization Code for Access Token)',
|
|
"level": 1
|
|
},
|
|
'feacft': {
|
|
"event": 'Failed Exchange (Authorization Code for Access Token)',
|
|
"level": 3
|
|
},
|
|
'f': {
|
|
"event": 'Failed Login',
|
|
"level": 3
|
|
},
|
|
'w': {
|
|
"event": 'Warnings During Login',
|
|
"level": 2
|
|
},
|
|
'du': {
|
|
"event": 'Deleted User',
|
|
"level": 1
|
|
},
|
|
'fu': {
|
|
"event": 'Failed Login (invalid email/username)',
|
|
"level": 3
|
|
},
|
|
'fp': {
|
|
"event": 'Failed Login (wrong password)',
|
|
"level": 3
|
|
},
|
|
'fc': {
|
|
"event": 'Failed by Connector',
|
|
"level": 3
|
|
},
|
|
'fco': {
|
|
"event": 'Failed by CORS',
|
|
"level": 3
|
|
},
|
|
'con': {
|
|
"event": 'Connector Online',
|
|
"level": 1
|
|
},
|
|
'coff': {
|
|
"event": 'Connector Offline',
|
|
"level": 3
|
|
},
|
|
'fcpro': {
|
|
"event": 'Failed Connector Provisioning',
|
|
"level": 4
|
|
},
|
|
'ss': {
|
|
"event": 'Success Signup',
|
|
"level": 1
|
|
},
|
|
'fs': {
|
|
"event": 'Failed Signup',
|
|
"level": 3
|
|
},
|
|
'cs': {
|
|
"event": 'Code Sent',
|
|
"level": 0
|
|
},
|
|
'cls': {
|
|
"event": 'Code/Link Sent',
|
|
"level": 0
|
|
},
|
|
'sv': {
|
|
"event": 'Success Verification Email',
|
|
"level": 0
|
|
},
|
|
'fv': {
|
|
"event": 'Failed Verification Email',
|
|
"level": 0
|
|
},
|
|
'scp': {
|
|
"event": 'Success Change Password',
|
|
"level": 1
|
|
},
|
|
'fcp': {
|
|
"event": 'Failed Change Password',
|
|
"level": 3
|
|
},
|
|
'sce': {
|
|
"event": 'Success Change Email',
|
|
"level": 1
|
|
},
|
|
'fce': {
|
|
"event": 'Failed Change Email',
|
|
"level": 3
|
|
},
|
|
'scu': {
|
|
"event": 'Success Change Username',
|
|
"level": 1
|
|
},
|
|
'fcu': {
|
|
"event": 'Failed Change Username',
|
|
"level": 3
|
|
},
|
|
'scpn': {
|
|
"event": 'Success Change Phone Number',
|
|
"level": 1
|
|
},
|
|
'fcpn': {
|
|
"event": 'Failed Change Phone Number',
|
|
"level": 3
|
|
},
|
|
'svr': {
|
|
"event": 'Success Verification Email Request',
|
|
"level": 0
|
|
},
|
|
'fvr': {
|
|
"event": 'Failed Verification Email Request',
|
|
"level": 3
|
|
},
|
|
'scpr': {
|
|
"event": 'Success Change Password Request',
|
|
"level": 0
|
|
},
|
|
'fcpr': {
|
|
"event": 'Failed Change Password Request',
|
|
"level": 3
|
|
},
|
|
'fn': {
|
|
"event": 'Failed Sending Notification',
|
|
"level": 3
|
|
},
|
|
'sapi': {
|
|
"event": 'API Operation',
|
|
"level": 1
|
|
},
|
|
'fapi': {
|
|
"event": 'Failed API Operation',
|
|
"level": 3
|
|
},
|
|
'limit_wc': {
|
|
"event": 'Blocked Account',
|
|
"level": 4
|
|
},
|
|
'limit_ui': {
|
|
"event": 'Too Many Calls to /userinfo',
|
|
"level": 4
|
|
},
|
|
'api_limit': {
|
|
"event": 'Rate Limit On API',
|
|
"level": 4
|
|
},
|
|
'sdu': {
|
|
"event": 'Successful User Deletion',
|
|
"level": 1
|
|
},
|
|
'fdu': {
|
|
"event": 'Failed User Deletion',
|
|
"level": 3
|
|
},
|
|
'sd': {
|
|
"event": 'Success Delegation',
|
|
"level": 3
|
|
},
|
|
'fd': {
|
|
"event": 'Failed Delegation',
|
|
"level": 3
|
|
},
|
|
'seccft': {
|
|
"event": "Success Exchange (Client Credentials for Access Token)",
|
|
"level": 1
|
|
},
|
|
'feccft': {
|
|
"event": "Failed Exchange (Client Credentials for Access Token)",
|
|
"level": 1
|
|
},
|
|
'fsa': {
|
|
"event": "Failed Silent Auth",
|
|
"level": 3
|
|
},
|
|
'ssa': {
|
|
"event": "Success Silent Auth",
|
|
"level": 1
|
|
},
|
|
'fepft': {
|
|
"event": "Failed Exchange (Password for Access Token)",
|
|
"level": 3
|
|
},
|
|
'limit_mu': {
|
|
"event": "Blocked IP Address",
|
|
"level": 3
|
|
},
|
|
'sepft': {
|
|
"event": "Success Exchange (Password for Access Token)",
|
|
"level": 1
|
|
},
|
|
'fcoa': {
|
|
"event": "Failed Cross Origin Authentication",
|
|
"level": 3
|
|
}
|
|
})
|
|
|
|
|
|
def process_msg(mozmsg, msg):
|
|
"""Normalization function for auth0 msg.
|
|
@mozmsg: MozDefEvent (mozdef message)
|
|
@msg: DotDict (json with auth0 raw message data).
|
|
|
|
All the try-except loops handle cases where the auth0 msg may or may not contain expected fields.
|
|
The msg structure is not garanteed.
|
|
See also https://auth0.com/docs/api/management/v2#!/Logs/get_logs
|
|
"""
|
|
details = DotDict({})
|
|
|
|
try:
|
|
mozmsg.useragent = msg.user_agent
|
|
except KeyError:
|
|
pass
|
|
|
|
try:
|
|
details['type'] = log_types[msg.type].event
|
|
except KeyError:
|
|
#New message type, check https://manage-dev.mozilla.auth0.com/docs/api/management/v2#!/Logs/get_logs for ex.
|
|
debug('New auth0 message type, please add support: {}'.format(msg.type))
|
|
details['type'] = msg.type
|
|
|
|
if log_types[msg.type].level == 3:
|
|
mozmsg.set_severity(mozdef.MozDefEvent.SEVERITY_ERROR)
|
|
elif log_types[msg.type].level > 3:
|
|
mozmsg.set_severity(mozdef.MozDefEvent.SEVERITY_CRITICAL)
|
|
details['sourceipaddress'] = msg.ip
|
|
|
|
try:
|
|
details['description'] = msg.description
|
|
except KeyError:
|
|
details['description'] = ""
|
|
|
|
mozmsg.timestamp = msg.date
|
|
details['auth0_msg_id'] = msg._id
|
|
|
|
try:
|
|
details['auth0_client'] = msg.client_name
|
|
except KeyError:
|
|
pass
|
|
|
|
try:
|
|
details['connection'] = msg.connection
|
|
except KeyError:
|
|
pass
|
|
|
|
try:
|
|
details['auth0_client_id'] = msg.client_id
|
|
except KeyError:
|
|
pass
|
|
|
|
try:
|
|
details['username'] = msg.details.request.auth.user.name
|
|
details['action'] = msg.details.response.body.name
|
|
except KeyError:
|
|
try:
|
|
details['error'] = 'true'
|
|
details['errormsg'] = msg.details.error.message
|
|
except KeyError:
|
|
pass
|
|
except AttributeError:
|
|
pass
|
|
try:
|
|
details['username'] = msg.user_name
|
|
except KeyError:
|
|
pass
|
|
|
|
# Differenciate auto login (session cookie check validated) from logged in and had password verified
|
|
|
|
try:
|
|
for i in msg.details.prompt:
|
|
# Session cookie check
|
|
if i.get('name') == 'authenticate':
|
|
details['auth_type'] = 'Login succeeded due to a valid session cookie being supplied'
|
|
elif i.get('name') == 'lock-password-authenticate':
|
|
details['auth_type'] = 'Login succeeded due to a valid plaintext password being supplied'
|
|
except KeyError:
|
|
pass
|
|
|
|
mozmsg.summary = "{mtype} {desc}".format(
|
|
mtype=details.type,
|
|
desc=details.description
|
|
)
|
|
|
|
mozmsg.details = details
|
|
mozmsg.details['auth0_raw'] = str(msg)
|
|
|
|
return mozmsg
|
|
|
|
|
|
def load_state(fpath):
|
|
"""Load last msg id we've read from auth0 (log index).
|
|
@fpath string (path to state file)
|
|
"""
|
|
state = 0
|
|
try:
|
|
with open(fpath) as fd:
|
|
state = fd.read().split('\n')[0]
|
|
except IOError:
|
|
pass
|
|
return state
|
|
|
|
|
|
def save_state(fpath, state):
|
|
"""Saves last msg id we've read from auth0 (log index).
|
|
@fpath string (path to state file)
|
|
@state int (state value)
|
|
"""
|
|
with open(fpath, mode='w') as fd:
|
|
fd.write(str(state)+'\n')
|
|
|
|
|
|
def byteify(input):
|
|
"""Convert input to ascii"""
|
|
if isinstance(input, dict):
|
|
return {byteify(key): byteify(value)
|
|
for key, value in input.iteritems()}
|
|
elif isinstance(input, list):
|
|
return [byteify(element) for element in input]
|
|
elif isinstance(input, unicode):
|
|
return input.encode('utf-8')
|
|
else:
|
|
return input
|
|
|
|
|
|
def fetch_auth0_logs(config, headers, fromid):
|
|
lastid = fromid
|
|
|
|
r = requests.get('{url}?take={reqnr}&sort=date:1&per_page={reqnr}&include_totals=true&from={fromid}'.format(
|
|
url=config.auth0.url,
|
|
reqnr=config.auth0.reqnr,
|
|
fromid=fromid),
|
|
headers=headers)
|
|
|
|
#If we fail here, auth0 is not responding to us the way we expected it
|
|
if (not r.ok):
|
|
raise Exception(r.url, r.reason, r.status_code, r.json())
|
|
ret = r.json()
|
|
|
|
#Sometimes API give us the requested totals.. sometimes not.
|
|
if (type(ret) is dict) and ('logs' in ret.keys()):
|
|
have_totals = True
|
|
all_msgs = ret['logs']
|
|
else:
|
|
have_totals = False
|
|
all_msgs = ret
|
|
|
|
#Process all new auth0 log msgs, normalize and send them to mozdef
|
|
for msg in all_msgs:
|
|
mozmsg = mozdef.MozDefEvent(config.mozdef.url)
|
|
if config.DEBUG == 'True':
|
|
mozmsg.set_send_to_syslog(True, only_syslog=True)
|
|
mozmsg.hostname = config.auth0.url
|
|
mozmsg.tags = ['auth0']
|
|
msg = byteify(msg)
|
|
msg = DotDict(msg)
|
|
lastid = msg._id
|
|
|
|
#Fill in mozdef msg fields from the auth0 msg
|
|
try:
|
|
mozmsg = process_msg(mozmsg, msg)
|
|
except KeyError as e:
|
|
#if this happens the msg was malformed in some way
|
|
mozmsg.details['error'] = 'true'
|
|
mozmsg.details['errormsg'] = '"'+str(e)+'"'
|
|
mozmsg.summary = 'Failed to parse auth0 message'
|
|
if config.DEBUG == 'True':
|
|
traceback.print_exc()
|
|
mozmsg.send()
|
|
|
|
if have_totals:
|
|
return (int(ret['total']), int(ret['start']), int(ret['length']), lastid)
|
|
else:
|
|
return (0, 0, 0, lastid)
|
|
|
|
|
|
def main():
|
|
# Configuration loading
|
|
config_location = os.path.dirname(sys.argv[0]) + '/' + 'auth02mozdef.json'
|
|
with open(config_location) as fd:
|
|
config = DotDict(hjson.load(fd))
|
|
|
|
if config is None:
|
|
print("No configuration file 'auth02mozdef.json' found.")
|
|
sys.exit(1)
|
|
|
|
headers = {
|
|
'Authorization': 'Bearer {}'.format(config.auth0.token),
|
|
'Accept': 'application/json'
|
|
}
|
|
|
|
fromid = load_state(config.state_file)
|
|
# Auth0 will interpret a 0 state as an error on our hosted instance, but will accept an empty parameter "as if it was 0"
|
|
if (fromid == 0 or fromid == "0"):
|
|
fromid = ""
|
|
totals = 1
|
|
start = 0
|
|
length = 0
|
|
|
|
# Fetch until we've gotten all messages
|
|
while (totals > start+length):
|
|
(totals, start, length, lastid) = fetch_auth0_logs(config, headers, fromid)
|
|
fromid = lastid
|
|
|
|
save_state(config.state_file, lastid)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|