diff --git a/gestion/annuaires_pg.py b/gestion/annuaires_pg.py index ed55c151..63906d2e 100755 --- a/gestion/annuaires_pg.py +++ b/gestion/annuaires_pg.py @@ -5,9 +5,9 @@ import psycopg2 try: if __name__ == 'annuaires_pg_test': - conn = psycopg2.connect("user=crans dbname=switchs host=localhost") + conn = psycopg2.connect(user='crans', database='switchs', host='localhost') else: - conn = psycopg2.connect("user=crans dbname=switchs host=pgsql.adm.crans.org") + conn = psycopg2.connect(user='crans', database='switchs', host='pgsql.adm.crans.org') # Population de la tâble avec les bâtiments cur = conn.cursor() diff --git a/gestion/config.py b/gestion/config.py index 749b73d4..bf4645ac 100644 --- a/gestion/config.py +++ b/gestion/config.py @@ -331,6 +331,42 @@ puissions corriger le problème. --\u0020 Les membres actifs du Crans""" % email.Header.make_header([("Déménagement non déclaré", "utf8")]) +class mac_prise: + # Pour la recherche dans postgres + delay = { 'instant': '2 min', + 'heuristique': '30 min', + 'journalier': '1 day', + } + + # Contient trois dictionnaire. Le paramètre mac signifie "combien de chambres doivent voir la même mac pour que ça soit suspect" + # Le paramètre chambre signifie "combien de macs doivent traverser une même chambre pour que ça soit suspect" + suspect = { 'instant':{'mac': 2, 'chambre': 2}, + 'heuristique':{'mac': 3, 'chambre': 2}, + 'journalier':{'mac': 3, 'chambre': 2}, + } + + # Contient trois dictionnaire. Le paramètre mac signifie "combien de chambres doivent voir la même mac pour que ça soit suspect" + # Le paramètre chambre signifie "combien de macs doivent traverser une même chambre pour que ça soit suspect" + tres_suspect = { 'instant':{'mac': 3, 'chambre': 3}, + 'heuristique':{'mac': 4, 'chambre': 3}, + 'journalier':{'mac': 4, 'chambre': 3}, + } + + # Le point central des analyses. + rapport_suspect = { 'instant':{'mac': 0.51, 'chambre': 0.51}, + 'heuristique':{'mac': 0.57, 'chambre': 0.55}, + 'journalier':{'mac': 0.61, 'chambre': 0.58}, + } + + titre_suspect = { 'instant':{'mac': u"Macs se baladant un peu trop entre les chambres (instantanné)", 'chambre': u"Chambres avec un peu trop de macs (instantanné)"}, + 'heuristique':{'mac': u"Macs se baladant un peu trop entre les chambres (délai moyen)", 'chambre': u"Chambres avec un peu trop de macs (délai moyen)"}, + 'journalier':{'mac': u"Macs s'étant peut-être un peu trop baladées aujourd'hui", 'chambre': u"Chambres avec un peu trop de macs sur un jour"}, + } + + titre_tres_suspect = { 'instant':{'mac': u"/!\ Spoof potentiel des macs suivantes dans les chambres ci-après (instantanné)", 'chambre': u"/!\ Les chambres ci-après contiennent étrangement trop de macs inconnues ! (instantanné)"}, + 'heuristique':{'mac': u"/!\ Spoof potentiel des macs suivantes dans les chambres ci-après (délai moyen)", 'chambre': u"/!\ Les chambres ci-après contiennent étrangement trop de macs inconnues ! (délai moyen)"}, + 'journalier':{'mac': u"/!\ Spoof probable des macs suivantes dans les chambres ci-après aujourd'hui !!", 'chambre': u"/!\ Les chambres suivantes contiennent trop de macs inconnues (sur un jour)"}, + } # Classe pour les paramètres du firewall # ########################################## @@ -645,10 +681,10 @@ debit_max_gratuit = 1000000 ## Vlan accueil et isolement ## ############################### accueil_route = { - '138.231.136.1':{'tcp':['80','443']}, - '138.231.136.67':{'tcp':['80','443']}, - '138.231.136.98':{'tcp':['20','21','80','111','1024:65535'],'udp':['69','1024:65535']}, - '138.231.136.130':{'tcp':['80','443']} + '138.231.136.1':{'tcp':['80','443'],'hosts':['intranet.crans.org']}, + '138.231.136.67':{'tcp':['80','443'],'hosts':['www.crans.org', 'wiki.crans.org', 'wifi.crans.org']}, + '138.231.136.98':{'tcp':['20','21','80','111','1024:65535'],'udp':['69','1024:65535'], 'hosts':['ftp.crans.org']}, + '138.231.136.130':{'tcp':['80','443'],'hosts':['intranet2.crans.org']} } diff --git a/surveillance/mac_prises/mac_prise.py b/surveillance/mac_prises/mac_prise.py index 3f0f96e6..8e617dfd 100755 --- a/surveillance/mac_prises/mac_prise.py +++ b/surveillance/mac_prises/mac_prise.py @@ -10,7 +10,8 @@ from commands import getstatusoutput sys.path.append('/usr/scripts/gestion') import annuaires_pg -import psycopg2 +import time + # nécessite apparemment que l'objet conn soit bien créé lors de l'exec # de annuaires_pg, il faut être root (ou dans je ne sais quel groupe) @@ -18,7 +19,7 @@ import psycopg2 # (plante lamentablement quand j'essaye avec mon compte sur vo, sous # ipython. Mais si je sudo ipython, ça marche... -def liste_prises_macs(switch): +def liste_chambres_macs(switch): u''' Fonction générant un dictionnaire (macs) contenant pour chaque prise une liste des macs qui y sont actives. @@ -33,28 +34,32 @@ def liste_prises_macs(switch): liste_chbres = [] macs = {} - for i in data: - if i == '': - continue - else: - port = int(i.replace('STATISTICS-MIB::hpSwitchPortFdbAddress.', '').split('.')[0]) - mac = data[i].replace(' ', '').lower().replace('"', '') - if not re.match('([0-9a-f]{2}){6}', mac): - mac = mac.encode('hex').lower() - mac = "%s:%s:%s:%s:%s:%s" % (mac[0:2], mac[2:4], mac[4:6], mac[6:8], mac[8:10], mac[10:12]) - uplink = annuaires_pg.uplink_prises[bat] - prise = num_switch*100+port - if prise in uplink: + if data: + for port in data: + if port == '': continue - - chbre = chbre_prises(bat, prise) - - if chbre in liste_chbres: - macs[chbre].append(mac+'\n') else: - macs[chbre] = [] - macs[chbre].append(mac+'\n') - liste_chbres.append(chbre) + mac = data[port] + uplink = annuaires_pg.uplink_prises[bat] + prise = num_switch*100+port + if prise in uplink: + continue + + result = annuaires_pg.reverse(bat, prise) + if result: + chbre = bat+result[0] + if chbre in liste_chbres: + macs[chbre].extend(mac) + else: + macs[chbre] = [] + macs[chbre].extend(mac) + liste_chbres.append(chbre) + del chbre + else: + # On droppe, c'est des bornes wifi ou autres. + pass + else: + print "Pas de données pour %s" % (switch) return macs def walk(host, oid): @@ -67,8 +72,21 @@ def walk(host, oid): received = __exec('snmpwalk -Ox -v 1 -c public %s %s' % (host, oid)).split('\n') result = {} for ligne in received: - pport, pmac = ligne.split('Hex-STRING: ') - result[pport] = pmac + try: + pport, pmac = ligne.split('Hex-STRING: ') + + port = int(pport.replace('STATISTICS-MIB::hpSwitchPortFdbAddress.', '').split('.')[0]) + mac = pmac.replace(' ', '').lower().replace('"', '') + if not re.match('([0-9a-f]{2}){6}', mac): + mac = mac.encode('hex').lower() + mac = "%s:%s:%s:%s:%s:%s" % (mac[0:2], mac[2:4], mac[4:6], mac[6:8], mac[8:10], mac[10:12]) + + if not result.has_key(port): + result[port] = [mac] + else: + result[port].append(mac) + except: + print "Ligne moisie : %s de l'hôte : %s" % (ligne, host) return result @@ -85,18 +103,14 @@ def __exec(cmd): if __name__ == '__main__': switchs = sys.argv[1:] + date = time.strftime('%F %T') for switch in switchs: - macs = liste_prises_macs(switch) - - split = switch.replace('.adm.crans.org', '').split('-') - bat, num_switch = split[0][-1], int(split[1][0]) - - pgsql = psycopg2.connect(database="mac_prises", user="crans") - curseur = pgsql.cursor() + macs = liste_chambres_macs(switch) -# if not os.path.isdir("bat%s/%d"%(bat, num_switch)): -# os.makedirs("bat%s/%d"%(bat, num_switch)) + print macs + +# fichier = +# for chambre in macs.keys(): +# for mac in macs[chambre]: # -# for chbre in macs: -# with open('bat%s/%d/%s%03d.macs'%(bat, num_switch, bat, prise), 'w') as f: -# f.writelines(sorted(macs[prise])) +# curseur.execute(requete, (date, chambre, mac)) diff --git a/surveillance/mac_prises/mac_prise_analyzer.py b/surveillance/mac_prises/mac_prise_analyzer.py new file mode 100755 index 00000000..807ed9a8 --- /dev/null +++ b/surveillance/mac_prises/mac_prise_analyzer.py @@ -0,0 +1,156 @@ +#!/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) + +longueur = { 'mac': (24, 3), + 'chambre': (10, 7), + } +titres = { 'mac':(u'mac', u'chambres'), + 'chambre': (u'chambre', u'macs'), + } +alignements = ('c', 'c') + +def lin(x, y, z): + """ + Calcul linéaire d'un rapport + """ + return (float(x)/float(y)-1.0)/(float(z)-1.0)*100 + +def genere_comptage(duree, groupe, associes): + """ + 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 + """ + 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] } + 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: + 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(',') + + # On retire les machines associées à l'adhérent possédant la chambre + if groupe == 'chambre': + for i in liste_associes: + proprio_associe = ldap.search('macAddress=%s' % i)[0].proprio() + if str(proprio_associe['chbre']).lower() == entry[groupe]: + liste_associes.remove(i) + if len(liste_associes) < mac_prise.tres_suspect[duree][groupe]: + continue + + # Toujours un problème ? On ajoute au dico + 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(',') + + if groupe == 'chambre': + for i in liste_associes: + try: + proprio_associe = ldap.search('macAddress=%s' % i)[0].proprio() + if str(proprio_associe['chbre'][0]).lower() == entry[groupe]: + liste_associes.remove(i) + except: + pass + if len(liste_associes) < mac_prise.suspect[duree][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]: + pb_comptage_suspect[entry[groupe]] = liste_associes + else: + pass + 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)) for a in pb_comptage_suspect.values()] + [longueur[associes][1]]) + 4 + largeurs = (longueur[groupe][0], longueur_max) + + data = [] + for clef, valeurs in pb_comptage_suspect.items(): + data.append([clef, ", ".join(valeurs)]) + + output += tableau(data, titres[groupe], largeurs, alignements) + output += "\n\n\n" + + if len(pb_comptage_tres_suspect) > 0: + output += mac_prise.titre_tres_suspect[duree][groupe]+"\n" + + longueur_max = max([len(", ".join(a)) for a in pb_comptage_tres_suspect.values()] + [longueur[associes][1]]) + 4 + largeurs = (longueur[groupe][0], longueur_max) + + data = [] + for clef, valeurs in pb_comptage_tres_suspect.items(): + data.append([clef, ", ".join(valeurs)]) + + output += tableau(data, titres[groupe], largeurs, alignements) + output += "\n\n\n" + + return output + +if __name__ == '__main__': + output = u"Détection de spoof potentiel\n\n\n" + coupure = len(output) + + output += genere_comptage('instant', 'mac', 'chambre') + output += genere_comptage('heuristique', 'mac', 'chambre') + output += genere_comptage('journalier', 'mac', 'chambre') + output += genere_comptage('instant', 'chambre', 'mac') + output += genere_comptage('heuristique', 'chambre', 'mac') + output += genere_comptage('journalier', 'chambre', 'mac') + + if len(output) == coupure: + sys.exit(0) + + print output diff --git a/surveillance/mac_prises/mac_prise_holder.py b/surveillance/mac_prises/mac_prise_holder.py new file mode 100755 index 00000000..4bca766e --- /dev/null +++ b/surveillance/mac_prises/mac_prise_holder.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + +import sys +import psycopg2 +import mac_prise +from threading import Thread +import time + +class ThreadWithReturnValue(Thread): + """ + Classe de threading qui récupère le "return" d'une fonction, et le renvoie avec + output() + """ + def __init__(self, group=None, target=None, name=None, + args=(), kwargs={}, Verbose=None): + Thread.__init__(self, group, target, name, args, kwargs, Verbose) + self._return = None + + def run(self): + if self._Thread__target is not None: + self._return = self._Thread__target(*self._Thread__args, + **self._Thread__kwargs) + def output(self): + Thread.join(self) + return self._return + +if __name__ == '__main__': + """ + On envoie sur n threads les n arguments, puis on récupère les sorties + et on les enregistre dans une base postgresql + """ + switches = sys.argv[1:] + date = time.strftime('%F %T') + threads = {} + output = {} + for switch in switches: + threads[switch] = ThreadWithReturnValue(target=mac_prise.liste_chambres_macs, args=(switch,)) + threads[switch].start() + + # On change de boucle, car il faut absolument que tous les threads aient démarré, histoire qu'on + # parallélise vraiment ! + for switch in switches: + output[switch] = threads[switch].output() + connecteur = psycopg2.connect(database="mac_prises", user="crans") + connecteur.set_session(autocommit=True) + curseur = connecteur.cursor() + + requete = "INSERT INTO correspondance (date, chambre, mac) VALUES (%s, %s, %s);" + + for switch in output: + for chambre in output[switch]: + for mac in output[switch][chambre]: + curseur.execute(requete, (date, chambre, mac)) diff --git a/surveillance/mac_prises/mac_prise_wrapper.sh b/surveillance/mac_prises/mac_prise_wrapper.sh index f4692dc4..dd06fc3f 100755 --- a/surveillance/mac_prises/mac_prise_wrapper.sh +++ b/surveillance/mac_prises/mac_prise_wrapper.sh @@ -1,54 +1,9 @@ #!/bin/sh -set -ex - -GIT_DIR=/usr/scripts/surveillance/mac_prises/output/ -SCRIPT=/usr/scripts/surveillance/mac_prises/mac_prise.py - -# Nombre de changements de mac sur une prise avant mail -WARNING=2 - -MAILTO=nobody@crans.org - -if ! [ -d $GIT_DIR ]; then - echo -n "Création du répertoire \`$GIT_DIR'..." - mkdir $GIT_DIR - echo " Fait." -fi - -cd $GIT_DIR - -if ! [ -d $GIT_DIR/.git ]; then - git init -fi +SCRIPT=/usr/scripts/surveillance/mac_prises/mac_prise_holder.py # Récupération de la liste des switchs -SWITCHS=$(/usr/bin/host -l adm.crans.org | /usr/bin/awk '/^bat[abcghijpm]-/{print $1}') - -# Nettoyage du contenu du répertoire, avec ignore-unmatch pour éviter le plantage en -# cas de répertoire vide. -/usr/bin/git rm -r -q --ignore-unmatch ./* +SWITCHS=$(/usr/bin/host -l adm.crans.org sable.adm.crans.org | /usr/bin/awk '/^bat[abcghijpm]-/{print $1}') # Lancement du listage des macs en parallèle -/usr/bin/parallel -j 1000 python $SCRIPT -- $SWITCHS - -# Ajout de tous les fichiers (à faire avant le diff, pour que les nouveaux fichiers soient pris en compte) -/usr/bin/git add * - -# Récupération de statistiques -# numstat renvoie le nombre de lignes ajoutées, le nombre de lignes supprimées et le nom du fichier -# on ajoute les deux premières variables et on classe par nombre de modifs -/usr/bin/git diff --cached --numstat | /usr/bin/awk '{print $1+$2 " " $3}' | sort -rn | ( while read num file; do - if [ $num -ge $WARNING ]; then - echo $file - else - break - fi -done ) | xargs /usr/bin/git diff --cached | mail -a 'From: "Eye in the sky" ' -s "Surveillance macs/prises" $MAILTO - -/usr/bin/git commit -m "Updated mac list" > /dev/null - -# Garbage collection toutes les 10 minutes -if [ $(expr $(date +%M) % 10) -eq 0 ]; then - /usr/bin/git gc --aggressive -q -fi +python $SCRIPT $SWITCHS