| |
| |
| |
| |
| |
|
|
| import sys |
| import json |
| import duo_client |
| import mozdef_client as mozdef |
| import pickle |
| from configlib import getConfig, OptionParser |
| from mozdef_util.utilities.toUTC import toUTC |
|
|
|
|
| def normalize(details): |
| |
| |
| |
| normalized = {} |
|
|
| for f in details: |
| if f in ("ip", "ip_address", "client_ip"): |
| normalized["sourceipaddress"] = details[f] |
| continue |
| if f in ("eventtype", "event_type"): |
| normalized["eventtype"] = details[f] |
| continue |
| if f in ("host", "hostname"): |
| normalized["hostname"] = details[f] |
| continue |
| if f == "result": |
| if details[f].lower() == "success": |
| normalized["success"] = True |
| else: |
| normalized["success"] = False |
| normalized[f] = details[f] |
|
|
| if "user" in normalized and type(normalized["user"]) is dict: |
| if "name" in normalized["user"]: |
| normalized["username"] = normalized["user"]["name"] |
| if "key" in normalized["user"]: |
| normalized["userkey"] = normalized["user"]["key"] |
| del (normalized["user"]) |
|
|
| return normalized |
|
|
|
|
| def process_events(mozmsg, duo_events, etype, state): |
| """ |
| Data format of duo_events in api_version == 2 (str): |
| duo_events.metadata = {u'total_objects': 49198, u'next_offset': [u'1547244648000', u'4da7180c-b1e5-47b4-9f4d-ee10dc3b5ac8']} |
| duo_events.authlogs = [{...}, {...}, ...] |
| authlogs entry = {u'access_device': {u'ip': u'a.b.c.d', u'location': {u'city': None, u'state': u'Anhui', u'country': |
| u'China'}}, u'event_type': u'authentication', u'timestamp': 1547244800, u'factor': u'not_available', u'reason': |
| u'deny_unenrolled_user', u'txid': u'68b33dd3-d341-46c6-a985-0640592fb7b0', u'application': {u'name': u'Integration |
| Name Here', u'key': u'SOME KEY HERE'}, u'host': u'api-blah.duosecurity.com', u'result': u'denied', u'eventtype': u'authentication', u'auth_device': {u'ip': None, u'location': {u'city': None, u'state': None, u'country': None}, u'name': None}, u'user': {u'name': u'root', u'key': None}} |
| """ |
| |
| |
| |
|
|
| if etype == "administration": |
| noconsume = ["timestamp", "host", "action"] |
| elif etype == "telephony": |
| noconsume = ["timestamp", "host", "context"] |
| elif etype == "authentication": |
| noconsume = ["timestamp", "host", "event_type"] |
| else: |
| return |
|
|
| |
| if isinstance(duo_events, dict) and "authlogs" in duo_events: |
| offset = duo_events["metadata"]["next_offset"] |
| if offset is not None: |
| state["{}_offset".format(etype)] = offset |
| duo_events = duo_events["authlogs"] |
| api_version = 2 |
| else: |
| api_version = 1 |
|
|
| for e in duo_events: |
| details = {} |
| |
| |
| if 'timestamp' in e: |
| mozmsg.timestamp = toUTC(e['timestamp']).isoformat() |
| |
| |
| if 'host' in e and not None: |
| mozmsg.log["hostname"] = e["host"] |
| if 'hostname' in e and not None: |
| mozmsg.log["hostname"] = e["hostname"] |
| for i in e: |
| if i in noconsume: |
| continue |
|
|
| |
| if e[i] is not None and type(e[i]) == str and e[i].startswith("{"): |
| j = json.loads(e[i]) |
| for x in j: |
| details[x] = j[x] |
| continue |
|
|
| details[i] = e[i] |
| mozmsg.set_category(etype) |
| localdetails = normalize(details) |
| if "access_device" in localdetails: |
| if "ip" in localdetails["access_device"]: |
| localdetails["sourceipaddress"] = localdetails["access_device"]["ip"] |
| if "hostname" in localdetails["access_device"]: |
| if localdetails["access_device"]["hostname"] is None: |
| del localdetails["access_device"]["hostname"] |
| mozmsg.details = localdetails |
| del(localdetails) |
| mozmsg.hostname = options.URL |
| if etype == "administration": |
| if 'error' in e: |
| mozmsg.summary = ( |
| e["action"] + " because of " + e["error"] + " by " + e["username"] |
| ) |
| else: |
| mozmsg.summary = ( |
| e["action"] + " by " + e["username"] |
| ) |
| elif etype == "telephony": |
| mozmsg.summary = e["context"] |
| elif etype == "authentication": |
| if api_version == 1: |
| mozmsg.summary = ( |
| e["eventtype"] + " " + e["result"] + " for " + e["username"] |
| ) |
| else: |
| if 'reason' in e and e['reason'] is not None: |
| mozmsg.summary = ( |
| e["eventtype"] + " " + e["result"] + " for " + e["user"]["name"] + " due to " + e["reason"] |
| ) |
| else: |
| mozmsg.summary = ( |
| e["eventtype"] + " " + e["result"] + " for " + e["user"]["name"] |
| ) |
|
|
| mozmsg.send() |
|
|
| |
| try: |
| state[etype] = e["timestamp"] |
| except UnboundLocalError: |
| |
| pass |
| return state |
|
|
|
|
| def main(): |
| try: |
| state = pickle.load(open(options.statepath, "rb")) |
| except IOError: |
| |
| |
| |
| state = { |
| "administration": 0, |
| "administration_offset": None, |
| "authentication": 1547000000000, |
| "authentication_offset": None, |
| "telephony": 0, |
| "telephony_offset": None, |
| } |
|
|
| |
| if state["authentication"] < 1547000000000: |
| state["authentication"] = int(str(state["authentication"]) + "000") |
|
|
| duo = duo_client.Admin(ikey=options.IKEY, skey=options.SKEY, host=options.URL) |
| mozmsg = mozdef.MozDefEvent(options.MOZDEF_URL) |
| mozmsg.tags = ["duosecurity"] |
| if options.update_tags != "": |
| mozmsg.tags.append(options.update_tags) |
| mozmsg.set_category("authentication") |
| mozmsg.source = "DuoSecurityAPI" |
| if options.DEBUG: |
| mozmsg.debug = options.DEBUG |
| mozmsg.set_send_to_syslog(True, only_syslog=True) |
|
|
| |
| |
| |
| |
| state = process_events( |
| mozmsg, |
| duo.get_administrator_log(mintime=state["administration"] + 1), |
| "administration", |
| state, |
| ) |
| state = process_events( |
| mozmsg, |
| duo.get_authentication_log( |
| api_version=2, |
| limit="1000", |
| sort="ts:asc", |
| mintime=state["authentication"] + 1, |
| next_offset=state["authentication_offset"], |
| ), |
| "authentication", |
| state, |
| ) |
| state = process_events( |
| mozmsg, |
| duo.get_telephony_log(mintime=state["telephony"] + 1), |
| "telephony", |
| state, |
| ) |
|
|
| pickle.dump(state, open(options.statepath, "wb")) |
|
|
|
|
| def initConfig(): |
| options.IKEY = getConfig("IKEY", "", options.configfile) |
| options.SKEY = getConfig("SKEY", "", options.configfile) |
| options.URL = getConfig("URL", "", options.configfile) |
| options.MOZDEF_URL = getConfig("MOZDEF_URL", "", options.configfile) |
| options.DEBUG = getConfig("DEBUG", True, options.configfile) |
| options.statepath = getConfig("statepath", "", options.configfile) |
| options.update_tags = getConfig("addtag", "", options.configfile) |
|
|
|
|
| if __name__ == "__main__": |
| parser = OptionParser() |
| defaultconfigfile = sys.argv[0].replace(".py", ".conf") |
| parser.add_option( |
| "-c", |
| dest="configfile", |
| default=defaultconfigfile, |
| help="configuration file to use", |
| ) |
| (options, args) = parser.parse_args() |
| initConfig() |
| main() |
|
|