parse_auth_log.py 11.6 KB
Newer Older
1
#! /usr/bin/env python
2
# -*- coding: utf-8 -*-
3 4

###############################################################################
5
# parse_auth_log.py : Détecte les problèmes d'authentifications
6 7
###############################################################################
# The authors of this code are
8
# Jérémie Dimino <dimino@crans.org>
9
#
10
# Copyright (C) 2006 Jérémie Dimino
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
# All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
###############################################################################

import sys

sys.path.append('/usr/scripts/gestion')

from ldap_crans import crans_ldap
33
from annuaires_pg import chbre_prises, reverse
34 35 36 37 38 39 40 41 42
from affich_tools import cprint
import sys, datetime

base = None
# Correspondance prise -> chambre
prise_chbre = {}
aujourdhui = datetime.date.today()

def trouve_chambre(bat, prise):
43
    """ Trouve la chambre associée à une prise """
44

glondu's avatar
glondu committed
45
    if prise in ('????', 'EXT', 'CRA'):
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
        return prise

    if not prise_chbre.has_key(bat):
        prise_chbre[bat] = reverse(bat)

    chbre = prise_chbre[bat].get(prise) or prise_chbre[bat].get(prise+"-")
    if chbre:
        chbre = chbre[0]
    else:
        cprint('La prise %s%s est inconnue' % (bat, prise), 'rouge')
        chbre = "????"

    return chbre


def trouve_prise(chbre):
62
    """ Trouve la prise associée à une chambre """
63

glondu's avatar
glondu committed
64
    if chbre in ('EXT', '????', 'CRA'):
65 66 67
        return chbre
    else:
        bat = chbre[0].lower()
68
        prise = bat + chbre_prises(bat, chbre[1:])
69 70 71 72 73 74
        if prise[-1] == '-':
            prise = prise[:-1]
        return prise


def __calcul_max(table):
75
    """ Calcule les différents maxima (voir parse_auth_log pour plus de détails) """
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95

    nb_m = 0
    nb_p = 0
    for prise, erreur_prise in table.items():
        nb = 0
        nb_max = 0
        for mac, erreur_mac in erreur_prise['macs'].items():
            n = len(erreur_mac['lignes'])
            erreur_mac['nombre'] = n
            nb += n
            nb_max += n
        erreur_prise['nombre'] = nb
        erreur_prise['nombre_max'] = nb_max
        nb_m = max(nb_m, nb_max)
        nb_p = max(nb_p, nb)

    return nb_m, nb_p


def __ajoute_ligne(errs, ligne, prise, mac, info=None):
96
    """ Ajoute une ligne dans le dico pour la prise et la mac donnée """
97 98 99 100 101 102 103 104 105 106 107 108 109 110

    erreur_prise = errs.get(prise)
    if not erreur_prise:
        erreur_prise = { 'macs': {} }
        errs[prise] = erreur_prise

    erreur_mac = erreur_prise['macs'].get(mac)
    if not erreur_mac:
        erreur_mac = { 'lignes': [], 'info': info }
        erreur_prise['macs'][mac] = erreur_mac

    erreur_mac['lignes'].append(ligne)


111
# Correspondance 'noms de mois' -> n°
112 113 114 115
__mois_num = { 'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
               'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12 }

def parse_auth_log(fichier_log='/var/log/freeradius/radius_auth.log', jour=None):
116
    """ Retourne la liste des problèmes de connexions: retourne deux dicos, la première étant pour les macs qui n'ont pas été trouvées dans la base et la seconde pour les connexions qui ont eu lieu à partir d'une mauvaise prise
117 118

les deux dictionnaires ont la structure suivante:
119 120 121
- nombre_mac_max: nombre maximal d'erreurs trouvées pour une mac
- nombre_prise_max: nombre maximal d'erreurs trouvées pour une prise
- prises: dico associant à une prise la listes des erreurs à partir de cette prise:
122
  - nombre: nombre total d'erreurs survenues sur la prise
123 124
  - nombre_max: nombre maximal d'erreurs trouvéss pour une mac
  - macs: dico associant à une mac la liste des erreurs à partir de cette mac
125
    - nombre: le nombre d'erreurs survenues pour cette mac sur cette prise
126 127
    - lignes: la liste des lignes du fichier de logs où la mac apparaît sur cette prise
    - info: informations sur le propriétaire (vaut None pour la première table)
128 129 130
      - machine: la machine en question
      - prise: la prise de sa chambre
      - chambre: sa chambre
131
      - prop: le propriétaire
132

133
Si jour est spécifié (de type datetime.date), toutes les connexions avant cette dates seront ignorées.
134 135 136 137 138 139 140 141
"""

    try:
        log = open(fichier_log)
    except:
        cprint(u"Impossible d'ouvrir le fichier %s" % fichier_log, 'rouge')
        sys.exit(1)

142
    # Les macs non trouvées
143 144
    errs_inconnue = {}

145
    # Les pc connectés sur la mauvaise prise
146 147
    errs_prise = {}

148
    # Les recherches déjà effectuées sur les macs
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
    mac_machine = {}

    annee = None
    mois = None

    for ligne in log:
        champs = ligne.split()
        prise = champs[5]
        mac = champs[7]

        if jour != None:
            nouveau_mois = __mois_num[champs[0]]

            if annee == None:
                if nouveau_mois > aujourdhui.month:
164
                    # Là on suppose qu'il s'agit de l'année dernière
165 166 167 168
                    annee = aujourdhui.year - 1
                else:
                    annee = aujourdhui.year
            elif mois > nouveau_mois:
169
                # Là on suppose qu'on est passé à l'année suivante
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
                annee += 1

            mois = nouveau_mois

            if datetime.date(annee, mois, int(champs[1])) < jour:
                continue

        info_mac = mac_machine.get(mac)
        if info_mac:
            machine = info_mac['machine']
        else:
            machine = base.search('mac=' + mac)['machine']
            info_mac = { 'machine': machine }
            mac_machine[mac] = info_mac

        if machine == []:
            __ajoute_ligne(errs_inconnue, ligne, prise, mac)

        else:
            info = info_mac.get('info_prop')
            if not info:
                prop = machine[0].proprietaire()
                chbre = prop.chbre()
                info = { 'machine': machine[0],
                         'prise': trouve_prise(chbre),
                         'chambre': chbre,
                         'prop': prop }
                info_mac['info_prop'] = info

            if prise != info['prise']:
                __ajoute_ligne(errs_prise, ligne, prise, mac, info)

    log.close()

    nb_inconnue_m, nb_inconnue_p = __calcul_max(errs_inconnue)
    nb_prise_m, nb_prise_p = __calcul_max(errs_prise)

    return { 'prises': errs_inconnue,
             'nombre_mac_max': nb_inconnue_m,
             'nombre_prise_max': nb_inconnue_p }, \
           { 'prises': errs_prise,
             'nombre_mac_max': nb_prise_m,
             'nombre_prise_max': nb_prise_p }


def affiche_erreurs(errs, message, min_mac=1, min_prise=1):
216
    """ Affiche les infos contenues dans un dico renvoyé par parse_auth_log """
217 218 219 220 221 222 223 224 225 226 227 228 229 230

    if errs['nombre_mac_max'] >= min_mac and errs['nombre_prise_max'] >= min_prise:

        cprint(message, 'bleu')

        for prise, erreur_prise in errs['prises'].items():

            if erreur_prise['nombre'] >= min_prise and erreur_prise['nombre_max'] >= min_mac:

                chambre = prise[0].upper() + trouve_chambre(prise[0], prise[1:])
                cprint(u'  prise %s (chambre %s)' % (prise, chambre), 'gras')

                for mac, erreur_mac in erreur_prise['macs'].items():
                    if erreur_mac['nombre'] >= min_mac:
231 232 233 234
                        ligne = erreur_mac['lignes'][-1].strip().split()
                        ligne[5] = '%8s' % ligne[5].replace("adm.crans.org-", "")
                        ligne = ligne[0:3] + ligne[5:]
                        print '    %s (x%d)' % (' '.join(ligne), erreur_mac['nombre'])
235 236 237
                        info = erreur_mac['info']
                        if info != None:
                            prop = info['prop']
238 239
                            cprint(u'      -> machine de : %-16s %5s (prise %s) <%s>' %
                                   (prop.Nom()[:16], info['chambre'], info['prise'], prop.email()))
240 241 242 243 244 245 246 247 248 249 250


def __usage(err=''):
    """ Message d'erreur """

    if err : cprint(err, 'rouge')
    cprint(u"Tapez %s -h pour plus d'informations" % sys.argv[0].split('/')[-1].split('.')[0])
    sys.exit(2)


def __param_entier(opt, val):
251
    """ Récupère un entier passé en ligne de commande """
252 253 254 255

    try:
        return int(val)
    except:
256
        __usage(u'La valeur du paramètre %s est incorecte (doit être un entier positif)' % opt)
257 258 259 260 261 262


def __aide():
    """ Aide """

    cprint(u"""Usage: %s [OPTIONS]
263
Parse les logs d'authentifications et affiche les erreurs trouvées (macs inconnues ou connexion d'une machine sur une prise autre que celle du propriétaire).
264 265 266

Options:
  -h, --help                affiche cette aide
267 268 269
  -l, --log <fichier>       fichier de log à parser (par défaut: /var/log/freeradius/radius_auth.log)
  -m, --min-mac <nombre>    nombre minimal d'occurrences d'une mac sur une prise pour qu'elle soit reportée
  -n, --min-prise <nombre>  nombre minimal d'erreurs sur une prise pour qu'elle soit reportée
270
  -i, --inconnue            n'affiche que les erreurs de mac inconnues
271
  -p, --prise               n'affiche que les connexions sur une prise autre que celle du propriétaire
272 273
  -d, --date <date>         ignorer totalement les lignes avant cette date

274
<date> peut être:
275
- une date absolue sous la forme AAAAMMJJ
276 277
- une date relative à aujourd'hui sous la forme +<nombre de jour>
Par exemple pour ne considérer que les deux dernier jours: +2
278

279
Rapporter toutes anomalies à <dimino@crans.org>.""" % sys.argv[0].split('/')[-1].split('.')[0])
280 281 282 283 284 285 286 287 288 289 290
    sys.exit(0)


if __name__ == '__main__':

    fichier_log = '/var/log/freeradius/radius_auth.log'
    min_mac = 1
    min_prise = 1
    # Info que l'on veut afficher
    aff_inconnue = True
    aff_prise = True
291
    # Date de départ
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
    jour = None

    import getopt, time

    try:
        options, arg = getopt.getopt(sys.argv[1:], 'hipl:m:n:d:', [ 'help', 'log=', 'min-mac=', 'min-prise=', 'inconnue', 'prise', 'date='])
    except getopt.error, msg:
        __usage(unicode(msg))

    for opt, val in options:
        if opt in [ '-h', '--help' ]:
            __aide()
        elif opt in [ '-l', '--log-file' ]:
            fichier_log = val
        elif opt in [ '-m', '--min-mac' ]:
            min_mac = __param_entier(opt, val)
        elif opt in [ '-n', '--min-prise' ]:
            min_prise = __param_entier(opt, val)
        elif opt in [ '-i', '--inconnue' ]:
            aff_prise = 0
        elif opt in [ '-p', '--prise' ]:
            aff_inconnue = 0
        elif opt in [ '-d', '--date' ]:
            if val[0] == '+':
                jour = aujourdhui - datetime.timedelta(__param_entier(opt, val[1:]))
            else:
                try:
                    jour = datetime.date(*(time.strptime(val, "%Y%m%d")[0:3]))
                except:
321
                    __usage(u'La valeur du paramètre %s est incorecte (doit être de la forme AAAAMMJJ)' % opt)
322 323 324 325 326 327 328 329 330
        else:
            cprint(u'Option inconnue: %s' % opt, 'rouge')
            __usage()

    base = crans_ldap()

    errs_inconnue, errs_prise = parse_auth_log(fichier_log, jour)

    if aff_inconnue:
331
        affiche_erreurs(errs_inconnue, u"Pour les connexions suivantes la mac n'a pas été trouvée dans la base:", min_mac, min_prise)
332 333

    if aff_prise:
334
        affiche_erreurs(errs_prise, u"Les connexions suivantes ont eu lieu sur une prise autre que celle du proriétaire:", min_mac, min_prise)