diff --git a/gestion/config.py b/gestion/config.py index bf4645ac..efbe2065 100644 --- a/gestion/config.py +++ b/gestion/config.py @@ -332,6 +332,16 @@ puissions corriger le problème. Les membres actifs du Crans""" % email.Header.make_header([("Déménagement non déclaré", "utf8")]) class mac_prise: + # Pour spammer, mettre à true. + hargneux = True + + # Si pour une chambre donnée, il y a plus de 300 entrées filaires + # n'appartenant pas à l'adhérent propriétaire de la mac, on prévient. + max_inconnues_par_jour = 300 + + # Titre... + titre_mac_inconnue = u"Repérage de macs potentiellement non désirées dans les chambres suivantes." + # Pour la recherche dans postgres delay = { 'instant': '2 min', 'heuristique': '30 min', diff --git a/surveillance/mac_prises/mac_prise_analyzer.py b/surveillance/mac_prises/mac_prise_analyzer.py index b4c2f7dc..c00aaa4f 100755 --- a/surveillance/mac_prises/mac_prise_analyzer.py +++ b/surveillance/mac_prises/mac_prise_analyzer.py @@ -5,6 +5,7 @@ import psycopg2 import psycopg2.extras import sys import smtplib +import time sys.path.append('/usr/scripts/gestion') from config import mac_prise @@ -60,38 +61,46 @@ def genere_comptage(duree, groupe, associes): pb_comptage_suspect = {} pb_comptage_tres_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 , %(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] } cur.execute(requete) fetched = cur.fetchall() for entry in fetched: # Si c'est la chambre d'un membre actif ou d'un club, on droppe. if groupe == 'chambre': - if entry[groupe] in chambres_ma + chambres_clubs: + if entry[groupe] in chambres_ma: + Logs.append(u"Chambre dropée, car appartenant à un membre actif : %s\n\n" % entry[groupe]) + continue + elif entry[groupe] in chambres_clubs: + Logs.append(u"Chambre dropée, car appartenant à un club : %s\n\n" % entry[groupe]) continue # Sinon, on vérifie si le local est bien rempli. if entry['nb_'+associes+'s_distinctes'] >= mac_prise.tres_suspect[duree][groupe]: - liste_associes = entry[associes+'s'].split(',') + Logs.append(u"Recherche par %s, entrée très suspecte : %s -> %s \n" % (groupe, entry[groupe], entry[associes+'s'])) + liste_associes = entry[associes+'s'].split(', ') # On retire les machines associées à l'adhérent possédant la chambre if groupe == 'chambre': for i in liste_associes: try: proprio_associe = ldap.search('macAddress=%s' % i)[0].proprio() - if str(proprio_associe['chbre']).lower() == entry[groupe]: + if str(proprio_associe['chbre'][0]).lower() == entry[groupe]: liste_associes.remove(i) except: pass if len(liste_associes) < mac_prise.tres_suspect[duree][groupe]: + Logs.append(u"Entrée rejetée : la plupart des %s appartiennent au propriétaire de la %s\n\n" % (associes, groupe)) continue # Toujours un problème ? On ajoute au dico + Logs.append(u"Entrée ajoutée au tableau %s pour la recherche par %s.\n\n" % (duree, groupe)) pb_comptage_tres_suspect[entry[groupe]] = liste_associes # Même chose avec un seuil plus faible elif entry['nb_'+associes+'s_distinctes'] >= mac_prise.suspect[duree][groupe]: - liste_associes = entry[associes+'s'].split(',') + Logs.append(u"Recherche par %s, entrée suspecte : %s -> %s \n" % (groupe, entry[groupe], entry[associes+'s'])) + liste_associes = entry[associes+'s'].split(', ') if groupe == 'chambre': for i in liste_associes: @@ -102,14 +111,16 @@ def genere_comptage(duree, groupe, associes): except: pass if len(liste_associes) < mac_prise.suspect[duree][groupe]: + Logs.append(u"Entrée rejetée : la plupart des %s appartiennent au propriétaire de la %s\n\n" % (associes, groupe)) continue # On calcul 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]) else: - pass + Logs.append(u"Entrée rejetée du tableau %s pour la recherche par %s, car rapport inférieur au seuil.\n\n" % (duree, groupe)) else: pass @@ -148,10 +159,14 @@ def genere_comptage(duree, groupe, associes): return output + + if __name__ == '__main__': output = u"Détection de spoof potentiel\n\n\n" coupure = len(output) + 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') @@ -159,7 +174,45 @@ if __name__ == '__main__': output += genere_comptage('heuristique', 'chambre', 'mac') output += genere_comptage('journalier', 'chambre', 'mac') - if len(output) == coupure: + 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) - print output.encode('utf-8') + message = """From: %(from)s +To: %(to)s +Subject: %(subject)s +Content-Type: multipart/mixed; boundary="_424234545aaff-ffca234efff-556adceff5646_" + +--_424234545aaff-ffca234efff-556adceff5646_ +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" + +%(logs)s + +--_424234545aaff-ffca234efff-556adceff5646_-- + +""" + + corps = message % { 'from': 'Spoofing watcher ', + 'to': 'test@lists.crans.org', + 'subject': 'Analyse du spoofing', + 'contenu': output, + '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')) diff --git a/surveillance/mac_prises/mac_prise_reperage.py b/surveillance/mac_prises/mac_prise_reperage.py new file mode 100755 index 00000000..4f449546 --- /dev/null +++ b/surveillance/mac_prises/mac_prise_reperage.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + +import psycopg2 +import psycopg2.extras +import sys +import smtplib + +sys.path.append('/usr/scripts/gestion') +from config import mac_prise +from affich_tools import tableau +sys.path.append('/usr/scripts/lc_ldap') +import lc_ldap + +ldap = lc_ldap.lc_ldap_admin() + +membres_actifs = ldap.search('(|(droits=Cableur)(droits=Nounou)(droits=Apprenti)(droits=Bureau))') +chambres_ma = [] +for membre_actif in membres_actifs: + try: + chambres_ma.append(str(membre_actif['chbre'][0]).lower()) + except: + pass + +clubs = ldap.search('cid=*') +chambres_clubs = [] +for club in clubs: + try: + chambres_clubs.append(str(club['chbre'][0]).lower()) + except: + pass + +conn = psycopg2.connect(user='crans', database='mac_prises') +cur = conn.cursor(cursor_factory = psycopg2.extras.DictCursor) + +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. + + Sans doute le truc le plus important, sera en tête du mail + """ + + output = u"" + probleme = {} + requete = "SELECT date, chambre, mac FROM correspondance WHERE date >= timestamp 'now' - interval '24 hours' ORDER BY chambre ASC;" + cur.execute(requete) + fetched = cur.fetchall() + liste_parsee = {} + + for entry in fetched: + if liste_parsee.has_key(entry['chambre']): + if liste_parsee[entry['chambre']].has_key(entry['mac']): + liste_parsee[entry['chambre']][entry['mac']] += 1 + else: + liste_parsee[entry['chambre']][entry['mac']] = 1 + else: + liste_parsee[entry['chambre']] = {} + liste_parsee[entry['chambre']][entry['mac']] = 1 + + 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 = probleme.keys() + clefs.sort() + for clef in clefs: + 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'Repérage de spoof potentiel par comptage' + coupure = len(output) + + output += reperage_mac_inconnue() + + if len(output) == coupure and not mac_prise.hargneux: + sys.exit(0) + + message = """From: %(from)s +To: %(to)s +Subject: %(subject)s +Content-Type: text/plain, charset="UTF-8" + +%(contenu)s + +-- +Script d'analyse mac_prise (en test) + +""" + + corps = message % { 'from': 'Spoofing watcher ', + 'to': 'test@lists.crans.org', + 'subject': 'Analyse horaire du spoofing', + 'contenu': output, + } + + mail = smtplib.SMTP('localhost') + mailfrom = 'spoof-watcher@crans.org' + mailto = 'test@lists.crans.org' + mail.sendmail(mailfrom, mailto, corps.encode('utf-8')) diff --git a/surveillance/mac_prises/mac_prise_wrapper.sh b/surveillance/mac_prises/mac_prise_wrapper.sh index 4459da91..c196f3ab 100755 --- a/surveillance/mac_prises/mac_prise_wrapper.sh +++ b/surveillance/mac_prises/mac_prise_wrapper.sh @@ -9,19 +9,4 @@ SWITCHS=$(/usr/bin/host -l adm.crans.org sable.adm.crans.org | /usr/bin/awk '/^b # Lancement du listage des macs en parallèle python $SCRIPT $SWITCHS -CORPS=$(python $ANALYZER) -LENGTH=$(echo $CORPS | wc -c) -if [ $LENGTH -ge 10 ]; then -( -cat < -Subject: Analyse du spoofing -Content-Type: text/plain; charset="UTF-8" - -${CORPS} --- -Script d'analyse en test -EOF -) | /usr/sbin/sendmail -t -fi; +python $ANALYZER