[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:
parent
baf4aa3645
commit
df49d0b6dc
6 changed files with 305 additions and 90 deletions
|
@ -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()
|
||||
|
|
|
@ -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']}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
156
surveillance/mac_prises/mac_prise_analyzer.py
Executable file
156
surveillance/mac_prises/mac_prise_analyzer.py
Executable 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
|
54
surveillance/mac_prises/mac_prise_holder.py
Executable file
54
surveillance/mac_prises/mac_prise_holder.py
Executable 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))
|
|
@ -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" <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
|
||||
python $SCRIPT $SWITCHS
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue