[mac_prises] On envoie plus qu'un email par jour, qui contient un résumé qualitatif, et des détails en pièce jointe.

This commit is contained in:
Pierre-Elliott Bécue 2013-05-03 01:48:00 +02:00
parent 46bf7e47d6
commit 0e84b5e02d
6 changed files with 199 additions and 231 deletions

View file

@ -12,9 +12,14 @@ from config import mac_prise
from affich_tools import tableau
sys.path.append('/usr/scripts/lc_ldap')
import lc_ldap
import collections
ldap = lc_ldap.lc_ldap_local()
conn = psycopg2.connect(user='crans', database='mac_prises')
conn.set_session(autocommit = True)
cur = conn.cursor(cursor_factory = psycopg2.extras.DictCursor)
membres_actifs = ldap.search('(|(droits=Cableur)(droits=Nounou)(droits=Apprenti)(droits=Bureau))')
chambres_ma = []
for membre_actif in membres_actifs:
@ -31,20 +36,12 @@ for club in clubs:
except:
pass
conn = psycopg2.connect(user='crans', database='mac_prises')
conn.set_session(autocommit = True)
cur = conn.cursor(cursor_factory = psycopg2.extras.DictCursor)
requete = "SELECT * FROM signales WHERE date >= timestamp 'now' - interval '1 day';"
cur.execute(requete)
signales = cur.fetchall()
longueur = { 'mac': (24, 3),
'chambre': (10, 7),
}
titres = { 'mac':(u'mac', u'chambres'),
'chambre': (u'chambre', u'macs'),
}
longueur = (24, 7)
titres = (u'mac', u'chambres')
alignements = ('c', 'c')
def lin(x, y, z):
@ -53,87 +50,168 @@ def lin(x, y, z):
"""
return (float(x)/float(y)-1.0)/(float(z)-1.0)
def genere_comptage(duree, groupe, associes):
def genere_comptage(duree):
"""
Grosse fonction de hack pour ne pas écrire six fois le même formatage de sortie bidon.
Prend en argument :
* duree, qui peut valoir 'instant', 'heuristique', ou 'journalier', permet d'importer
les variables de config qui vont bien
* groupe, qui précise selon quoi on groupe (mac ou chambre)
* associes, qui contient l'autre champ
"""
global date
global Logs
pb_comptage_suspect = {}
output = ""
requete = "SELECT array_to_string(array_agg(DISTINCT date), ', ') AS dates , %(groupe)s, array_to_string(array_agg(DISTINCT %(associes)s), ', ') AS %(associes)ss, COUNT(DISTINCT %(associes)s) AS nb_%(associes)ss_distinctes, COUNT(%(associes)s) AS nb_%(associes)ss, COUNT(DISTINCT date) as nb_dates_distinctes, COUNT(DISTINCT %(groupe)s) as nb_%(groupe)ss_distinctes FROM correspondance WHERE date >= timestamp 'now' - interval '%(delay)s' GROUP BY %(groupe)s;" % { 'groupe': groupe, 'associes': associes, 'delay': mac_prise.delay[duree] }
requete = "SELECT array_to_string(array_agg(DISTINCT date), ', ') AS dates , mac, array_to_string(array_agg(DISTINCT chambre), ', ') AS chambres, COUNT(DISTINCT chambre) AS nb_chambres_distinctes, COUNT(chambre) AS nb_chambres, COUNT(DISTINCT date) as nb_dates_distinctes, COUNT(DISTINCT mac) as nb_macs_distinctes FROM correspondance WHERE date >= timestamp 'now' - interval '%(delay)s' GROUP BY mac;" % {'delay': mac_prise.delay[duree]}
cur.execute(requete)
fetched = cur.fetchall()
for entry in fetched:
if groupe == "mac":
machines = ldap.search('(macAddress=%s)' % entry[groupe])
if len(machines) > 0:
if isinstance(machines[0], lc_ldap.machineWifi):
continue
else:
machines = ldap.search('(macAddress=%s)' % entry['mac'])
if len(machines) > 0:
if isinstance(machines[0], lc_ldap.machineWifi):
continue
else:
continue
if entry['nb_'+associes+'s_distinctes'] >= mac_prise.suspect[duree][groupe]:
Logs.append(u"Recherche par %s, entrée suspecte : %s -> %s : %s distinctes sur %s dates distinctes\n" % (groupe, entry[groupe], entry[associes+'s'], entry['nb_'+associes+'s'], entry['nb_dates_distinctes']))
liste_associes = entry[associes+'s'].split(', ')
if entry['nb_chambres_distinctes'] >= mac_prise.suspect[duree]:
Logs.append(u"Recherche par %s, entrée suspecte : %s -> %s : %s distinctes sur %s dates distinctes\n" % ('mac', entry['mac'], entry['chambres'], entry['nb_chambres'], entry['nb_dates_distinctes']))
liste_chambres = entry['chambres'].split(', ')
# On calcule la "probabilité" qu'un truc ne soit pas clair concernant la chambre/mac
rapport = lin(entry['nb_'+associes+'s'], entry['nb_dates_distinctes'], float(entry['nb_'+associes+'s_distinctes']))
if rapport >= mac_prise.rapport_suspect[duree][groupe]:
Logs.append(u"Entrée ajoutée au tableau %s pour la recherche par %s, car rapport supérieur au seuil.\n\n" % (duree, groupe))
pb_comptage_suspect[entry[groupe]] = (liste_associes, rapport, mac_prise.rapport_suspect[duree][groupe])
rapport = lin(entry['nb_chambres'], entry['nb_dates_distinctes'], float(entry['nb_chambres_distinctes']))
if rapport >= mac_prise.rapport_suspect[duree]:
Logs.append(u"Entrée ajoutée au tableau %s, car rapport supérieur au seuil.\n\n" % (duree))
pb_comptage_suspect[entry['mac']] = (liste_chambres, rapport, mac_prise.rapport_suspect[duree])
else:
Logs.append(u"Entrée rejetée du tableau %s pour la recherche par %s, car rapport inférieur au seuil : (%s).\n\n" % (duree, groupe, rapport))
Logs.append(u"Entrée rejetée du tableau %s, car rapport inférieur au seuil : (%s).\n\n" % (duree, rapport))
else:
pass
if len(pb_comptage_suspect) > 0:
output += mac_prise.titre_suspect[duree][groupe] + "\n"
# On prend la longueur de la plus longue valeur, on s'assure que cette longueur fait celle de la légende, plus un entier de marge
longueur_max = max([len(", ".join(a[0])) for a in pb_comptage_suspect.values()] + [longueur[associes][1]]) + 4
largeurs = (longueur[groupe][0], longueur_max, 11, 9)
titre = (titres[groupe][0], titres[groupe][1], "rapport", "seuil")
alignement = (alignements[0], alignements[1], 'c', 'c')
for clef, valeur in pb_comptage_suspect.items():
requete = "INSERT INTO spotted (mac, type, chambre, date, rapport, seuil) VALUES(%s, %s, %s, %s, %s, %s);"
cur.execute(requete, (clef, duree, ", ".join(valeur[0]), date, valeur[1], valeur[2]))
return None
def summary():
"""
Fonction résumant les entrées "spotted" des dernières 24h
"""
output = u""
Logs = u""
requete = "SELECT * FROM spotted WHERE date >= timestamp 'now' - interval '1 day' ORDER BY date ASC;"
cur.execute(requete)
fetched = cur.fetchall()
liste_triee = collections.defaultdict(dict)
for entry in fetched:
chbres = entry['chambre'].split(', ')
chbres.sort()
chbres = tuple(chbres)
if liste_triee[entry['mac']].has_key(entry['type']):
if liste_triee[entry['mac']][entry['type']].has_key(chbres):
liste_triee[entry['mac']][entry['type']][chbres].append((entry['date'], entry['rapport'], entry['seuil']))
else:
liste_triee[entry['mac']][entry['type']][chbres] = []
else:
liste_triee[entry['mac']][entry['type']] = {}
for mac, ltype in liste_triee.items():
data = []
output += u"Il y a eu une activité suspecte concernant la mac %s :\n\n" % mac
Logs += u"Activité pour %s :\n" % mac
for dtype, dataset in ltype.items():
number = 0
chambres = set()
for chbres, dataframe in dataset.items():
chambres.update(chbres)
number += len(dataframe)
data += [[dtype, ", ".join(list(chbres)), donnee[0], donnee[1], donnee[2]] for donnee in dataframe]
output += u'Sur le relevé %s, la mac est apparue %s fois. Elle est apparue dans les chambres suivantes : %s.\n' % (dtype, number, ", ".join(list(chambres)))
output += u'Consulter les logs (en PJ) pour plus d\'informations.\n\n'
maxlen = max([len(a[1]) for a in data]) + 4
titres = (u'relevé', u'chambres', u'date', u'rapport', u'seuil')
alignements = ('c', 'c', 'c', 'c', 'c')
largeurs = (12, maxlen, 20, 10, 10)
Logs += tableau(data, titres, largeurs, alignements)
Logs += u"\n\n"
return output, Logs
def reperage_mac_inconnue():
"""
Fonction de repérage d'une mac qui ne devrait pas être
dans telle chambre, sur une plage de 24h, suivant un
paramètre de config pour le nombre d'occurrences.
"""
output = u""
probleme = {}
requete = "SELECT chambre, mac, COUNT(mac) as nb_min FROM correspondance WHERE date >= timestamp 'now' - interval '24 hours' GROUP BY chambre, mac ORDER BY chambre ASC;"
cur.execute(requete)
fetched = cur.fetchall()
liste_parsee = collections.defaultdict(dict)
for entry in fetched:
liste_parsee[entry['chambre']][entry['mac']] = int(entry['nb_min'])
for chambre in liste_parsee.keys():
if chambre in chambres_ma + chambres_clubs:
continue
for mac in liste_parsee[chambre].keys():
try:
proprio_associe = ldap.search('macAddress=%s' % mac)[0].proprio()
if str(proprio_associe['chbre'][0]).lower() == chambre.lower():
garbage = liste_parsee[chambre].pop(mac)
except:
pass
number = sum(liste_parsee[chambre].values())
if number >= mac_prise.max_inconnues_par_jour:
probleme[chambre] = (liste_parsee[chambre].keys(), number)
if len(probleme) > 0:
output += mac_prise.titre_mac_inconnue+"\n"
longueur_max = max([len(", ".join(a[0])) for a in probleme.values()] + [len("macs")]) + 2
largeurs = (len('chambre') + 2, longueur_max, len('compteur') + 2, len('seuil') + 2)
data = []
clefs = pb_comptage_suspect.keys()
clefs = probleme.keys()
clefs.sort()
for clef in clefs:
data.append([clef, ", ".join(pb_comptage_suspect[clef][0]), pb_comptage_suspect[clef][1], pb_comptage_suspect[clef][2]])
output += tableau(data, titre, largeurs, alignement)
data.append([clef, ", ".join(probleme[clef][0]), probleme[clef][1], mac_prise.max_inconnues_par_jour])
output += tableau(data, ('chambre', 'macs', 'compteur', 'seuil'), largeurs, ('c', 'c', 'c', 'c'))
output += u"\n\n\n"
return output
if __name__ == '__main__':
output = u"Détection de spoof potentiel\n\n\n"
coupure = len(output)
date = time.strftime('%F %T')
if len(sys.argv) >= 2:
output = u''
Logs = u''
if '--summary' in sys.argv or 'summary' in sys.argv:
# On envoie le résumé
output += u' *Résumé des apparitions de mac dans plusieurs chambres*\n\n'
texte, Logs = summary()
output += texte
output += u"\n\n"
if '--reperage' in sys.argv or 'reperage' in sys.argv:
# On envoie le repérage
output += u' *Repérage de spoof potentiel par comptage*\n\n'
output += reperage_mac_inconnue()
Logs = [u"Logs du script mac_prise_analyzer\n\n\n"]
output += genere_comptage('instant', 'mac', 'chambre')
output += genere_comptage('heuristique', 'mac', 'chambre')
output += genere_comptage('journalier', 'mac', 'chambre')
if time.localtime().tm_min % 30 == 0 and mac_prise.hargneux:
hargneux = True
else:
hargneux = False
if len(output) == coupure and not hargneux:
sys.exit(0)
message = """From: %(from)s
Logs = Logs.encode('UTF-8')
output = output.encode('UTF-8')
message = """From: %(from)s
To: %(to)s
Subject: %(subject)s
Subject: Résumé quotidien : mac_prises.
Content-Type: multipart/mixed; boundary="_424234545aaff-ffca234efff-556adceff5646_"
--_424234545aaff-ffca234efff-556adceff5646_
@ -141,9 +219,6 @@ Content-Type: text/plain; charset="UTF-8"
%(contenu)s
--
Script d'analyse mac_prise (en test)
--_424234545aaff-ffca234efff-556adceff5646_
Content-Type: text/plain; charset="UTF-8"
Content-Disposition: attachment; filename="logs_analyse"
@ -151,17 +226,46 @@ Content-Disposition: attachment; filename="logs_analyse"
%(logs)s
--_424234545aaff-ffca234efff-556adceff5646_--
"""
corps = message % { 'from': 'Spoofing watcher <spoof-watcher@crans.org>',
'to': 'test@lists.crans.org',
'subject': 'Analyse du spoofing',
'contenu': output,
'logs': "".join(Logs),
}
corps = message % { 'from': 'Spoofing watcher <spoof-watcher@crans.org>',
'to': 'test@lists.crans.org',
'subject': 'Résumé journalier ',
'contenu': output,
'logs': Logs,
}
mail = smtplib.SMTP('localhost')
mailfrom = 'spoof-watcher@crans.org'
mailto = 'test@lists.crans.org'
mail.sendmail(mailfrom, mailto, corps.encode('utf-8'))
mail = smtplib.SMTP('localhost')
mailfrom = 'spoof-watcher@crans.org'
mailto = 'test@lists.crans.org'
mail.sendmail(mailfrom, mailto, corps)
else:
Logs = [u"Logs du script mac_prise_analyzer\n\n\n"]
genere_comptage('instantanne')
genere_comptage('moyen')
genere_comptage('journalier')
if time.localtime().tm_min % 30 == 0 and mac_prise.hargneux:
hargneux = True
else:
hargneux = False
if hargneux:
message = """From: %(from)s
To: %(to)s
Subject: %(subject)s
Content-Type: text/plain; charset="UTF-8"
%(logs)s
"""
corps = message % { 'from': 'Spoofing watcher <spoof-watcher@crans.org>',
'to': 'test@lists.crans.org',
'subject': 'Logs de mac_prises_analyzer',
'logs': "".join(Logs),
}
mail = smtplib.SMTP('localhost')
mailfrom = 'spoof-watcher@crans.org'
mailto = 'test@lists.crans.org'
mail.sendmail(mailfrom, mailto, corps.encode('utf-8'))