MozDef/bot/mozdefbot.py

495 строки
18 KiB
Python
Executable File

#!/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/.
"""mozdef bot using KitnIRC."""
import logging
import threading
from datetime import datetime
import time
import sys
import kitnirc.client
import random
import pyes
import pytz
from dateutil.parser import parse
import netaddr
import pygeoip
import os
from configlib import getConfig,OptionParser
import pika
import json
import select
channelKeys={}
channelKeys['#channelnamegoeshere']="channelkeygoeshere"
greetz=["mozdef bot in da house",
"mozdef here..what's up",
"mozdef has joined the room..no one panic",
"mozdef bot here..nice to see everyone"]
retorts=["why, why soo mean?",
"someone got up on the wrong side of...",
"yo momma arcsight",
"gross arcsight..why you do that?",
"fat chance arcsight",
"arcsight you're such a show off"]
panics=["don't panic",
".. a towel has immense psychological value",
"..but in fact the message was this: 'So Long, and Thanks for All the Fish.'",
"42",
"What I need..is a strong drink and a peer group --Douglas Adams",
"Eddies in the space-time continuum.",
"segmentation fault..SEP"
]
if os.path.isfile('quotes.txt'):
quotes=quotes=open('quotes.txt').readlines()
else:
quotes=['nothing to say..add a quotes.txt file!']
colors = {'red': '\x034\x02',
'normal': '\x03\x02',
'blue': '\x032\x02',
'green': '\x033\x02',
'yellow': '\x038\x02',
}
keywords = {'INFORMATIONAL': colors['green'],
'INFO': colors['green'],
'WARNING': colors['yellow'],
'CRITICAL': colors['red'],
'mozdef': colors['blue'],
}
jlFacePalm=r'''
.--yy+/-````` `s:``.
-.-: :...-::-````:-.:+/:
:/+---:-/-.-:--:s/:oyhys:
.o`.:---.--.-///:o-sdhhhy
`-.::--:---:/-::++: `.--.
`-/::.:/::-.-..::: .
`:/--..-/-.` ``.-: `:o:
.+ss+--...-.````.-:os/`
`-:/oyhyyso++/:/+++/++:o+-
:ydyso+syysso+oso+++osyy+s+
`osyyyo+/+sssoo//+s:--ssyy+so.
-osyhyso//sysoo++s/-/osysss++
/osyhyso+/+oooooosoooosooo+//
+osyyyso+/sosoooo+oo++++//:::
ossyyyso+:osyhddhsso+++//++/:
dyooyyso+/dNNNmhysoo+-:+oo+/:.
mmdyssss+/mmmmysyoo+:/o+sso+/.
dmhhyo+oo/smdssso+///oo:sss++:`
dddhhy++o/:yosso+///ooo-oys++/:
hdddhyso/:/osso+/:/ooo:`/soo+/:
dddhhso/-/oso++/:/ossys+.ooo+/.
ddddho../oo+//::+yhdddys::/o+:`
mdhdhy/.-////:odmmNdmmdhys+-`
mdddyy+--:::/ydmmmmmddddddhs-
'''
def colorify(data):
for i in keywords:
data = data.replace(i, keywords[i]+i+colors['normal'], 1)
return data
#http://code.activestate.com/recipes/576684-simple-threading-decorator/
def run_async(func):
"""
run_async(func)
function decorator, intended to make "func" run in a separate
thread (asynchronously).
Returns the created Thread object
E.g.:
@run_async
def task1():
do_something
@run_async
def task2():
do_something_too
t1 = task1()
t2 = task2()
...
t1.join()
t2.join()
"""
from threading import Thread
from multiprocessing import Process
from functools import wraps
@wraps(func)
def async_func(*args, **kwargs):
func_hl = Thread(target = func, args = args, kwargs = kwargs)
func_hl.start()
return func_hl
return async_func
def toUTC(suspectedDate,localTimeZone="US/Pacific"):
'''make a UTC date out of almost anything'''
utc=pytz.UTC
objDate=None
if type(suspectedDate)==str:
objDate=parse(suspectedDate,fuzzy=True)
elif type(suspectedDate)==datetime:
objDate=suspectedDate
if objDate.tzinfo is None:
objDate=pytz.timezone(localTimeZone).localize(objDate)
objDate=utc.normalize(objDate)
else:
objDate=utc.normalize(objDate)
if objDate is not None:
objDate=utc.normalize(objDate)
return objDate
def getQuote():
aquote='{0} --Mos Def'.format(quotes[random.randint(0,len(quotes)-1)].strip())
return aquote
def isIP(ip):
try:
netaddr.IPNetwork(ip)
return True
except:
return False
def ipLocation(ip):
location=""
try:
gi = pygeoip.GeoIP('GeoLiteCity.dat', pygeoip.MEMORY_CACHE)
geoDict=gi.record_by_addr(str(netaddr.IPNetwork(ip)[0]))
if not geoDict==None:
location=geoDict['country_name']
if geoDict['country_code'] in ('US'):
if geoDict['metro_code']:
location=location + '/{0}'.format(geoDict['metro_code'])
except Exception as e:
location=""
return location
def esSearchFail2ban(begindateUTC,enddateUTC=None):
resultMessages=[]
try:
es=pyes.ES(("http",options.esserver,9200))
nowDatePhrase=datetime.now().strftime("%b %d")
if enddateUTC is None:
enddateUTC=toUTC(nowDatePhrase).isoformat()
qDate=pyes.RangeQuery(qrange=pyes.ESRange('utctimestamp',from_value=begindateUTC,to_value=enddateUTC))
qFail2ban=pyes.MatchQuery("message","fail2ban.actions","phrase")
q=pyes.BoolQuery(must=[qDate,qFail2ban])
results = es.search(query=q)
sys.stderr.write('{0} results found\n'.format(len(results)))
for r in results:
sys.stderr.write('{0}\n'.format(r['message']))
resultMessages.append(r['message'])
return resultMessages
except Exception as e:
self.client.root_logger.error("Exception {0} while searching for fail2ban alerts.".format(e))
return resultMessages
def formatAlert(jsonDictIn):
#defaults
severity='INFO'
summary=''
category=''
if 'severity' in jsonDictIn.keys():
severity=jsonDictIn['severity']
if 'summary' in jsonDictIn.keys():
summary=jsonDictIn['summary']
if 'category' in jsonDictIn.keys():
category=jsonDictIn['category']
return colorify('{0}: {1} {2}'.format(severity,colors['blue']+ category +colors['normal'],summary))
#@run_async
#def consumealerts(client):
# try:
# def alertsCallback(ch, method, properties, bodyin):
# client.root_logger.debug(" [x]event %r:%r" % (method.routing_key, bodyin))
# try:
# jbody=json.loads(bodyin)
# client.msg(options.alertircchannel,formatAlertMessage(jbody))
#
# except Exception as e:
# client.root_logger.error('Exception on message queue callback {0}'.format(e))
# connection = pika.BlockingConnection(pika.ConnectionParameters(host=options.mqserver))
# client.root_logger.info('opening message queue channel')
# channel = connection.channel()
# client.mqconnection=connection
# channel.exchange_declare(exchange=options.alertexchange,type='topic')
# result = channel.queue_declare(exclusive=False)
# queue_name = result.method.queue
# channel.queue_bind(exchange=options.alertexchange, queue=queue_name,routing_key=options.alertqueue)
# channel.basic_consume(alertsCallback,queue=queue_name,no_ack=True)
# client.root_logger.debug('starting consuming')
# channel.start_consuming()
# except IOError:
# #Connection closed by us or something else
# if not connection is None:
# connection.close()
# except pika.exceptions.ConnectionClosed:
# pass
# except Exception as e:
# sys.stderr.write('%r'%e.message)
class alertsListener(threading.Thread):
def __init__(self,client):
threading.Thread.__init__(self)
# A flag to notify the thread that it should finish up and exit
self.kill = False
self.lastRunTime=datetime.now()
self.client=client
self.lastalerts=[]
self.openMQ()
self.mqError=False
self.connection =None
self.channel = None
def alertsCallback(self, ch, method, properties, bodyin):
self.client.root_logger.debug(" [x]event %r:%r" % (method.routing_key, bodyin))
try:
jbody=json.loads(bodyin)
self.client.msg(options.alertircchannel,formatAlert(jbody))
except Exception as e:
self.client.root_logger.error('Exception on message queue callback {0}'.format(e))
@run_async
def openMQ(self):
try:
if self.connection is None and not self.kill:
self.mqError=False
self.connection = pika.BlockingConnection(pika.ConnectionParameters(host=options.mqserver,heartbeat_interval=10))
#give the irc client visibility to our connection state.
self.client.mqconnection=self.connection
self.client.root_logger.info('opening message queue channel')
if self.channel is None:
self.channel = self.connection.channel()
self.channel.exchange_declare(exchange=options.alertexchange,type='topic')
result = self.channel.queue_declare(exclusive=False)
queue_name = result.method.queue
self.channel.queue_bind(exchange=options.alertexchange, queue=queue_name,routing_key=options.alertqueue)
self.client.root_logger.info('INFO consuming message queue {0}'.format(options.alertqueue))
self.client.msg(options.alertircchannel,'consuming message queue {0}'.format(options.alertqueue))
self.channel.basic_consume(self.alertsCallback,queue=queue_name,no_ack=True)
self.channel.start_consuming()
except pika.exceptions.ConnectionClosed as e:
self.client.root_logger.error("MQ Connection closed {0}".format(e))
self.client.msg(options.alertircchannel,"ERROR: Message queue is closed. No alerts for you")
self.mqError=True
try:
self.connection=None
self.channel=None
except:
pass
except AttributeError:
pass
except select.error:
pass
except Exception as e:
self.mqError=True
self.client.root_logger.error("Exception {0} while processing alerts message queue.".format(type(e)))
self.client.msg(options.alertircchannel,"ERROR: Exception {0} while processing alerts message queue".format(e))
try:
if self.connection is not None:
self.connection.close()
except:
pass
finally:
self.connection=None
self.channel=None
def run(self):
while not self.kill:
try:
self.client.root_logger.debug('checking mq connections')
if self.connection is None or self.mqError or not self.connection.is_open:
self.openMQ()
self.client.root_logger.info('opening mq connection')
time.sleep(10)
except Exception as e:
self.client.root_logger.error("Exception {0} while polling alerts message queue health.".format(e))
time.sleep(int(10))
class alertsWorker(threading.Thread):
def __init__(self,client):
threading.Thread.__init__(self)
# A flag to notify the thread that it should finish up and exit
self.kill = False
self.lastRunTime=datetime.now()
self.client=client
self.lastalerts=[]
def run(self):
while not self.kill:
self.checkAlerts()
def checkAlerts(self):
try:
if (datetime.now() - self.lastRunTime).seconds > int(10):
self.client.root_logger.debug("checking for alerts" )
#self.client.msg("#mzdf","checking for alerts...")
#analert='{0}: {1}'.format(datetime.now(),random.randint(0,100))
esresults=esSearchFail2ban(toUTC(self.lastRunTime))
#esresults=esSearchFail2ban(toUTC("Nov 18"))
for analert in esresults:
if analert not in self.lastalerts:
self.lastalerts.append(analert)
self.client.msg(alertChannel,analert)
self.client.root_logger.debug('sent {0}'.format(analert))
if len(self.lastalerts)>100:
self.client.root_logger.debug('removing:{0}'.format(self.lastalerts[0]))
self.lastalerts.remove(self.lastalerts[0])
self.lastRunTime=datetime.now()
else:
#self.client.root_logger.debug("not alert time..sleeping")
time.sleep(int(5))
except Exception as e:
self.client.root_logger.error("Exception {0} while checking for alerts.".format(e))
pass
class mozdefBot():
def __init__(self, ):
# Logging initialization
self.log_handler = logging.StreamHandler()
self.log_formatter = logging.Formatter("%(asctime)s %(message)s")
self.log_handler.setFormatter(self.log_formatter)
self.root_logger = logging.getLogger()
self.root_logger.addHandler(self.log_handler)
self.root_logger.setLevel(logging.INFO)
self.client = kitnirc.client.Client(options.host, options.port)
self.client.root_logger=self.root_logger
self.client.connect(
nick=options.nick,
username=options.username or options.nick,
realname=options.realname or options.username or options.nick,
password=options.password,
ssl=True
)
self.threads=[]
self.mqconnection=None
def run(self):
try:
@self.client.handle('WELCOME')
def join_channels(client, *params):
if not options.join:
return
for chan in options.join.split(","):
if chan in channelKeys.keys():
client.join(chan,channelKeys[chan])
else:
client.join(chan)
#t=alertsWorker(self.client)
t=alertsListener(self.client)
self.threads.append(t)
t.start()
#consumealerts(self.client)
@self.client.handle('LINE')
def line_handler(client,*params):
self.root_logger.debug('linegot:' + line)
@self.client.handle('PRIVMSG')
def priv_handler(client,actor,recipient,message):
self.root_logger.debug('privmsggot:' + message + ' from ' + actor)
if 'ArcSight' in actor or 'jeff' in actor:
if 'BANG' in message:
self.client.msg(recipient,random.choice(retorts))
if "!help" in message:
self.client.msg(recipient,"I just send alerts and taunt arcsight..for now..but try these:")
self.client.msg(recipient,"!quote --get a quote from my buddy Mos Def")
self.client.msg(recipient,"!panic --panic (or not )")
self.client.msg(recipient,"!facepalm --jennifer lawrence face-palm as requested by fox2mike")
self.client.msg(recipient,"!ipinfo --do a geoip lookup on an ip address")
if "!quote" in message:
self.client.msg(recipient,getQuote())
if "!panic" in message:
self.client.msg(recipient,random.choice(panics))
if "!facepalm" in message:
for line in jlFacePalm.split('\n'):
self.client.msg(recipient,line)
if "!ipinfo" in message:
for i in message.split():
if isIP(i):
ip=netaddr.IPNetwork(i)[0]
if ( not ip.is_loopback() and not ip.is_private() and not ip.is_reserved()):
self.client.msg(recipient,"{0} location: {1}".format(i,ipLocation(i)))
else:
self.client.msg(recipient,"{0}: hrm..loopback? private ip?".format(i))
@self.client.handle('JOIN')
def join_handler(client, user,channel,*params):
self.root_logger.debug('%r' % channel)
if user.nick==options.nick:
self.client.msg(channel,colorify(random.choice(greetz)))
self.client.run()
except KeyboardInterrupt:
for t in self.threads:
t.kill = True
self.client.disconnect()
if self.client.mqconnection is not None:
try:
self.client.mqconnection.close()
except:
pass
except Exception as e:
self.client.root_logger.error('bot error..quitting {0}'.format(e))
for t in self.threads:
t.kill = True
self.client.disconnect()
if self.client.mqconnection is not None:
self.client.mqconnection.close()
def initConfig():
#initialize config options
#sets defaults or overrides from config file.
options.host=getConfig('host','irc.somewhere.com',options.configfile)
options.nick=getConfig('nick','mozdefnick',options.configfile)
options.port=getConfig('port',6697,options.configfile)
options.username=getConfig('username','username',options.configfile)
options.realname=getConfig('realname','realname',options.configfile)
options.password=getConfig('password','',options.configfile)
options.join=getConfig('join','#mzdf',options.configfile)
options.esserver=getConfig('esserver','localhost',options.configfile)
options.mqserver=getConfig('mqserver','localhost',options.configfile)
options.alertqueue=getConfig('alertqueue','mozdef.alert',options.configfile)
options.alertexchange=getConfig('alertexchange','alerts',options.configfile)
options.alertircchannel=getConfig('alertircchannel','',options.configfile)
if options.alertircchannel=='':
options.alertircchannel=options.join
if __name__ == "__main__":
parser=OptionParser()
parser.add_option("-c", dest='configfile' , default='', help="configuration file to use")
(options,args) = parser.parse_args()
initConfig()
thebot=mozdefBot()
thebot.run()
# vim: set ts=4 sts=4 sw=4 et: