[mac_prises] En place, avec script d'analyse.

Je commence à en avoir marre des gens qui spoofent. J'ai donc pris
en charge l'écriture de mac_prises cette nuit.
Je le mets en test vers la mailing list test@lists.crans.org, avis
aux amateurs.
This commit is contained in:
Pierre-Elliott Bécue 2013-02-01 06:15:34 +01:00
parent baf4aa3645
commit df49d0b6dc
6 changed files with 305 additions and 90 deletions

View file

@ -5,9 +5,9 @@ import psycopg2
try: try:
if __name__ == 'annuaires_pg_test': if __name__ == 'annuaires_pg_test':
conn = psycopg2.connect("user=crans dbname=switchs host=localhost") conn = psycopg2.connect(user='crans', database='switchs', host='localhost')
else: 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 # Population de la tâble avec les bâtiments
cur = conn.cursor() cur = conn.cursor()

View file

@ -331,6 +331,42 @@ puissions corriger le problème.
--\u0020 --\u0020
Les membres actifs du Crans""" % email.Header.make_header([("Déménagement non déclaré", "utf8")]) 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 # # Classe pour les paramètres du firewall #
########################################## ##########################################
@ -645,10 +681,10 @@ debit_max_gratuit = 1000000
## Vlan accueil et isolement ## ## Vlan accueil et isolement ##
############################### ###############################
accueil_route = { accueil_route = {
'138.231.136.1':{'tcp':['80','443']}, '138.231.136.1':{'tcp':['80','443'],'hosts':['intranet.crans.org']},
'138.231.136.67':{'tcp':['80','443']}, '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']}, '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']} '138.231.136.130':{'tcp':['80','443'],'hosts':['intranet2.crans.org']}
} }

View file

@ -10,7 +10,8 @@ from commands import getstatusoutput
sys.path.append('/usr/scripts/gestion') sys.path.append('/usr/scripts/gestion')
import annuaires_pg import annuaires_pg
import psycopg2 import time
# nécessite apparemment que l'objet conn soit bien créé lors de l'exec # 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) # 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 # (plante lamentablement quand j'essaye avec mon compte sur vo, sous
# ipython. Mais si je sudo ipython, ça marche... # ipython. Mais si je sudo ipython, ça marche...
def liste_prises_macs(switch): def liste_chambres_macs(switch):
u''' u'''
Fonction générant un dictionnaire (macs) contenant pour chaque prise une Fonction générant un dictionnaire (macs) contenant pour chaque prise une
liste des macs qui y sont actives. liste des macs qui y sont actives.
@ -33,28 +34,32 @@ def liste_prises_macs(switch):
liste_chbres = [] liste_chbres = []
macs = {} macs = {}
for i in data: if data:
if i == '': for port in data:
continue if port == '':
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:
continue continue
chbre = chbre_prises(bat, prise)
if chbre in liste_chbres:
macs[chbre].append(mac+'\n')
else: else:
macs[chbre] = [] mac = data[port]
macs[chbre].append(mac+'\n') uplink = annuaires_pg.uplink_prises[bat]
liste_chbres.append(chbre) 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 return macs
def walk(host, oid): 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') received = __exec('snmpwalk -Ox -v 1 -c public %s %s' % (host, oid)).split('\n')
result = {} result = {}
for ligne in received: for ligne in received:
pport, pmac = ligne.split('Hex-STRING: ') try:
result[pport] = pmac 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 return result
@ -85,18 +103,14 @@ def __exec(cmd):
if __name__ == '__main__': if __name__ == '__main__':
switchs = sys.argv[1:] switchs = sys.argv[1:]
date = time.strftime('%F %T')
for switch in switchs: for switch in switchs:
macs = liste_prises_macs(switch) macs = liste_chambres_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()
# if not os.path.isdir("bat%s/%d"%(bat, num_switch)): print macs
# os.makedirs("bat%s/%d"%(bat, num_switch))
# fichier =
# for chambre in macs.keys():
# for mac in macs[chambre]:
# #
# for chbre in macs: # curseur.execute(requete, (date, chambre, mac))
# with open('bat%s/%d/%s%03d.macs'%(bat, num_switch, bat, prise), 'w') as f:
# f.writelines(sorted(macs[prise]))

View file

@ -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

View file

@ -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))

View file

@ -1,54 +1,9 @@
#!/bin/sh #!/bin/sh
set -ex SCRIPT=/usr/scripts/surveillance/mac_prises/mac_prise_holder.py
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
# Récupération de la liste des switchs # Récupération de la liste des switchs
SWITCHS=$(/usr/bin/host -l adm.crans.org | /usr/bin/awk '/^bat[abcghijpm]-/{print $1}') SWITCHS=$(/usr/bin/host -l adm.crans.org sable.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 ./*
# Lancement du listage des macs en parallèle # Lancement du listage des macs en parallèle
/usr/bin/parallel -j 1000 python $SCRIPT -- $SWITCHS 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" <root@crans.org>' -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