LINUX.ORG.RU

Анализ логов Astaro gateway


1

1

Примечание: штука довольно бесполезная в силу узкозаточенности, интересны в основном предложения по улучшению. ВОзможно, стоит перенести во флудилку.

Вчера просматривал старую почту и случайно наткнулся на вот эту тему: Автоматический анализ логов фаервола Там я просил помочь мне написать утилиту для сбора нужной статистики из файла логов Astaro Gateway. Был многократно послан с обвинениями в любви к халяве и тему закрыл, однако обещал написать такой скрипт самостоятельно и выложить сюда. Про тему я давно забыл, но, раз обещал и теперь вспомнил, выполняю. Так как та тема давно уже закрыта, выкладываю его здесь. Скрипт на питоне, в максимально простом структурном стиле, с довольно подробными комментариями - вдруг кому пригодится. В дальнейшем планирую сделать уже с использованием ООП что-нибудь более-менее универсальное для анализа аналогичных логов: например, оно из командной строки будет принимать имена нужных полей и выдавать статистику по ним, чтобы можно было не хачить код, а воспользоваться готовым решением. Как думаете, это актуально вообще?

Вот скрипт:

#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
Скрипт анализирует лог-файл в формате Astaro gateway
и на выходе пишет в файл статистику по:
- количеству подключений с каждого исходного адреса на каждый конечный;
- общее количество всех подключений с каждого исходного адреса.
"""

import socket       # Для резолва имени. Пример: socket.gethostbyaddr("1.1.1.1")
import sys
import re


### Секция анализа файла ###

filename = sys.argv[1]   # Имя файла для анализа
log = open(filename)

# Регулярное выражение для поиска ip-адреса:
regexp1 = re.compile(r"[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}")

src_dict = {}       # Основной словарь (адресов источника).
                    # Состоит из вложенных словарей (адресов назначения),
                    # значения ключей которых являются списками из [отрезолвленное_имя, счётчик_вхождений (сколько раз встречался данный dstip для данного src_ip)]
dst_dict = {}       # Словарь входящих адресов и их отрезолвленных имён.

for line in log:
    if "srcip=" in line:    # Убеждаемся, что строка подходит.
        needed_line = line.split("srcip=")[1]  # Отбрасываем лишнюю часть строки (всё, что до "srcip=", нам не нужно)
        if regexp1.search(needed_line) is not None:  # Убеждаемся, что в оставшейся части есть совпадения с регулярным выражением.
            ips = regexp1.findall(needed_line)      # Создаём временный список из пары srcip и dstip. ips[0] - это srcip, ips[1] - dstip
            if ips[0] in src_dict.keys():           # Пытаемся найти srcip среди ключей основного словаря
                if ips[1] in src_dict[ips[0]].keys():   # Пытаемся найти dstip среди ключей вложенного словаря
                    src_dict[ips[0]][ips[1]][1] += 1     # Добавляем единицу к счётчику вхождений во вложенном словаре
                else:
                    if ips[1] in dst_dict.keys():       # Проверяем, отрезолвлено ли уже имя.
                        src_dict[ips[0]][ips[1]] = [dst_dict[ips[1]], 1] # Если да, то добавляем его во вложенный словарь и
                                                                         # задаём начальное значение счётчика.
                    else:                               # Если имя ещё не отрезолвлено:
                        try:
                            name = socket.gethostbyaddr(ips[1])[0] # Резолвим имя
                        except socket.herror:
                            name = "Not Resolved"
                        dst_dict[ips[1]] = name             # добавляем его в словарь dst_dict
                        src_dict[ips[0]][ips[1]] = [dst_dict[ips[1]], 1]    # и создаём запись в src_dict
            else:                                   # Если такого srcip ещё нет:
                if ips[1] not in dst_dict.keys():        # Если dstip ещё не отрезолвлен,
                    try:
                        name = socket.gethostbyaddr(ips[1])[0] # Резолвим имя
                    except socket.herror:
                        name = "Not Resolved"
                    dst_dict[ips[1]] = name             # и добавляем в словарь dst_ip
                src_dict[ips[0]] = {ips[1]: [dst_dict[ips[1]], 1]}  # Добавляем srcip вместе с dstip, его именем и счётчиком вхождений.
        else:
            pass                                      # Если в строке нет совпадений с регулярным выражением, идём к следующей.
    else:
        pass            # Если в строке нет "srcip=", идём к следующей.


### Секция записи результата ###

outfile = open("statistics_of_%s" % filename, 'w')

for srcip in src_dict.keys():   # Для каждого srcip
    counter_full = 0            # Счётчик общего количества всех соединений для данного srcip
    lines_list = []         # Список строк. Нужно, чтобы перед записью можно было отсортировать.
    for dstip in src_dict[srcip].keys():    # Обрабатываем все dstip для данного srcip
        counter = src_dict[srcip][dstip][1] # Счётчик соединений для данного dstip
        line = "%s -> %s (%s): " % (srcip, dstip, src_dict[srcip][dstip][0])
        counter_full = counter_full + counter   # Увеличиваем значение общего счётчика подключений для данного srcip.
        lines_list.append((line, counter))      # Добавляем строку и количество подключений в список
    sorted_lines_list = sorted(lines_list, key=lambda x: x[::-1], reverse=True)   # Сортируем по значению счётчика, по убывающей.
    for l in sorted_lines_list:
        m = l[0] + str(l[1]) + "\n"      # Собираем каждую строку.
        outfile.write(m)         # Пишем каждую строку с dstip в файл.
    outfile.write("-" * 40 + "\n")  
    outfile.write("Всего подключений с %s: %d\n\n\n" % (srcip, counter_full))   # Пишем общее количество соединений для srcip.

outfile.close()

Вот пример результата работы на коротеньком тестовом куске лога:

192.168.100.11 -> 8.8.8.8 (google-public-dns-a.google.com): 3
192.168.100.11 -> 213.170.92.166 (Not Resolved): 3
----------------------------------------
Всего подключений с 192.168.100.11: 6


192.168.100.13 -> 8.8.8.8 (google-public-dns-a.google.com): 3
----------------------------------------
Всего подключений с 192.168.100.13: 3


192.168.1.22 -> 91.195.170.37 (ast37.novatel.ru): 1
----------------------------------------
Всего подключений с 192.168.1.22: 1


192.168.0.11 -> 91.195.170.37 (ast37.novatel.ru): 5
192.168.0.11 -> 213.170.92.166 (Not Resolved): 4
192.168.0.11 -> 22.195.170.37 (Not Resolved): 3
----------------------------------------
Всего подключений с 192.168.0.11: 12




ЗЫ: У меня под спойлеры засунуть код не получается, так что извиняюсь за простыни текста..

UPD: Исправил код (забыл добавить сортировку по количеству соединений с каждого ip) и пример результата (для наглядности).

Ответ на: комментарий от anonymous

Блин. Перечитал своё сообщение - что-то меня переклинило. Вместо «структурном» читать «процедурном». Стиле. Специально написал в процедурном стиле, чтобы для начинающего было очевидно, что да как, чтобы было всё последовательно. Я сам не профи, но могу и в функциональном стиле написать, и классы ООП использовать, вот только нафига, если задача узкоспециализированная? Вот когда буду делать универсальный парсер, тогда и заморочусь классами. А пока выложил простейший, сделанный на коленке вариант, выполняющий свою задачу. И вопрос заключался не в оценке моего стиля программирования, а в том, что полезного можно заставить делать такой парсер. Что он должен уметь?

Я это вижу примерно так: парсер должен в командной строке принимать обязательные ключи - названия полей, отвечающих в логе за source ip и destination ip - и необязательные. Например, поля, обозначающие порты, таймстемпы (в виде регекспа), тип пакета, или вообще просто произвольные поля, количество которых он будет считать и выводить в итоге. Вот когда допишу окончательную версию с учётом ваших предложений, если таковые появятся, тогда и будете критиковать СТИЛЬ, а пока не надо - давайте по выполняемым скриптом задачам.

Drevoborod ()
Ответ на: комментарий от kukara4

Хм.. Ну мне так понятнее, из чего я исхожу, что другому чайнику тоже будет так понятнее. А обычно я комменчу те строчки, которые кажутся мне неочевидными, чтобы не путаться в собственном коде.

И опять речь про стиль, в котором написан скрипт, а не про то, что он умеет/должен уметь!

Drevoborod ()
Ответ на: комментарий от kukara4

Да может еще кому нужно. Только непонятно, как с таким заголовком этот кто-то найдет этот скрипт. Разве что будет гуглить русскими словами, что довольно невероятно.

stevejobs ★★★★☆ ()
Ответ на: комментарий от stevejobs

Ну тогда и комментарии надо на английском писать, а я, зная, что «стиль - говно», не стал так делать специально, чтобы не позориться. То есть я не очень-то жду, что его кто-то найдёт. Ну ок, я понял, эта тема никому не интересна, так что заморачиваться написанием полноценного парсера не буду. По крайней мере, выкладывать его здесь - точно :)

Drevoborod ()
Ответ на: комментарий от Drevoborod

Да тут полно тем с 2 комментариями, которые тем не менее стабильно гуглятся в гугле, которые нужны куче людей, которые никогда здесь не смогут оставить комментарий (через некоторое время возможность комментирования закрывается). Такшто ящетаю имеет смысл постить сюда хорошие вещи, чтобы они остались в веках.

stevejobs ★★★★☆ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.