
* Le wrapper n'envoie plus de mails * Analyzer envoie ses mails lui-même, avec en pièce jointe le contenu du logging. * Analyzer n'envoit rien si les tableaux sont vide, sauf si hargneux, et sur une demi heure * Création de mac_prise_reperage, dont le but est de lister les macs inconnues d'une chambre sur une plage de 24h (script exécuté par cron toutes les heures), et de compter le nombre de minutes pendant lesquelles elles ont été présentes. Si la somme des compteurs pour une chambre dépasse une quantité dans config, on envoie un mail. Pas de logging ici, tout est "limpide". Il faudra bien définir la variable de config sus-citée. * Modification de config, on rajoute les deux variables pour mac_prise_reperage.py
218 lines
8.8 KiB
Python
Executable file
218 lines
8.8 KiB
Python
Executable file
#!/usr/bin/env python
|
||
# -*- coding: utf8 -*-
|
||
|
||
import psycopg2
|
||
import psycopg2.extras
|
||
import sys
|
||
import smtplib
|
||
import time
|
||
|
||
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)
|
||
|
||
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:
|
||
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]:
|
||
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'][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]:
|
||
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:
|
||
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]:
|
||
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:
|
||
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
|
||
|
||
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')
|
||
|
||
data = []
|
||
clefs = pb_comptage_suspect.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)
|
||
output += u"\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 = []
|
||
clefs = pb_comptage_tres_suspect.keys()
|
||
clefs.sort()
|
||
for clef in clefs:
|
||
data.append([clef, ", ".join(pb_comptage_tres_suspect[clef])])
|
||
|
||
output += tableau(data, titres[groupe], largeurs, alignements)
|
||
output += u"\n\n\n"
|
||
|
||
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')
|
||
output += genere_comptage('instant', 'chambre', 'mac')
|
||
output += genere_comptage('heuristique', 'chambre', 'mac')
|
||
output += genere_comptage('journalier', 'chambre', 'mac')
|
||
|
||
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
|
||
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 <spoof-watcher@crans.org>',
|
||
'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'))
|