Python для форензики: Автоматизация анализа логов и артефактов (Pandas, RegEx)

  • Автор темы Автор темы ADMIN
  • Дата начала Дата начала

ADMIN

Administrator
ADMIN
Регистрация
18 Окт 1989
Сообщения
1,408
Реакции
93
Баллы
48
☑ ДЕПОЗИТ
0$
1773426832734.webp

Syslog с серверов, Security.evtx с контроллеров домена, логи веб-серверов, дампы сетевого трафика, артефакты файловой системы. Каждый источник говорит на своём языке: один пишет timestamp в формате RFC3164 без года, другой - в ISO8601 с часовым поясом, третий вообще хранит события в бинарном XML. А клиент или руководство уже через час ждут ответ: "Откуда зашли? Что украли? Кто виноват?"

Ручной анализ в такой ситуации - путь к профессиональному выгоранию и фатальным ошибкам. Открыть 10-гигабайтный лог в блокноте? Бесполезно. Выгрузить Event Log в CSV через wevtutil и склеивать в Excel? Excel упадёт на миллионе строк, а времени на обработку уйдёт половина смены. К тому же человеческий глаз неизбежно пропустит аномалию, которая прячется между тысячами рутинных записей.

Здесь на помощь приходит автоматизация. И инструмент номер один для неё - Python. Почему именно он, а не PowerShell, не Perl и не набор bash-скриптов? Потому что Python - это экосистема, закрывающая 90% задач DFIR-аналитика:
  • Pandas - таблицы на стероидах. Фильтрация, группировка, объединение миллионов строк за секунды.
  • re - регулярные выражения для выкусывания индикаторов из любого текстового мусора.
  • datetime - работа с временем: нормализация часовых поясов, вычисление интервалов, построение хронологии.
  • json / xml.etree - парсинг современных форматов логов.
  • Специализированные библиотеки: python-evtx для чтения Windows Event Log, python-registry для работы с реестром, pytsk3 для MFT, jinja2 для генерации отчётов, matplotlib для визуализации.
И всё это - с возможностью быстро прототипировать решение в Jupyter Notebook, а затем упаковать в готовый CLI-скрипт, который можно передать коллегам или запускать по расписанию.

Но автоматизация - это не про "запустил скрипт и ушёл пить кофе". Это про перекладывание рутины на машину, чтобы освободить голову для настоящего анализа. Скрипты берут на себя грязную работу: нормализацию форматов, поиск паттернов, подсчёт статистики. Аналитик же сосредотачивается на интерпретации: почему атакующий выбрал именно этот вектор, как развивалась атака во времени, какие ещё хосты могли быть скомпрометированы. Автоматизация не заменяет экспертизу - она даёт ей пространство для манёвра.

Эта статья - практический гайд, построенный на реальных задачах, с которыми сталкивается DFIR-специалист. Мы не будем переписывать документацию и разбирать основы синтаксиса. Мы сразу перейдём к делу: возьмём типовые сценарии анализа и покажем, как закрыть их с помощью Python. В каждом разделе - готовый код, пояснения, на каких граблях можно споткнуться и как их обойти.

Кому это будет полезно:
  • SOC-аналитикам L2/L3, которые тонут в море алертов и хотят автоматизировать первичный разбор.
  • Incident Responder'ам, которым после ночного вызова нужно за пару часов собрать картину происшествия.
  • Forensic Examiner'ам, работающим с образами и артефактами, - покажем, как вытаскивать данные из MFT, реестра, Prefetch.
  • Всем, кто уже знает, что такое event ID 4625 и чем отличается Syslog от Windows Event Log, но ещё не построил супертаймлайн из десяти источников одной командой.

Устраивайся поудобнее

Давай сразу к цифрам. Средний инцидент сегодня - это не один скомпрометированный хост, а цепочка: фишинг, малварь, C2, lateral movement, эксфильтрация. За пару дней атакующий успевает наследить в DNS-логах, firewall, контроллере домена, на рабочих станциях. Собираешь логи со всех источников - и получаешь 50–100 ГБ текста. Вручную открыть такой объём в Notepad++? Ну-ну.

А ещё форматы... Syslog пишет в одном стиле, Windows Event Log - в своём XML-зоопарке, Apache - вообще строка с пробелами. Всё это надо склеить в единую хронологию. А клиент (или начальник) звонит каждые полчаса: «Ну что там? Нашли, откуда зашли?». Time pressure - наше всё. Без автоматизации тут либо сдаваться, либо работать сутками, теряя внимательность.

Поэтому Python - не роскошь, а инструмент выживания. Он берёт на себя грязную работу: нормализацию, поиск, подсчёт, а ты сосредотачиваешься на анализе. И да, это дешевле и гибче, чем любой корпоративный SIEM. Мы тут не против SIEM, но когда нужно быстро наколенном скрипте разобрать свежий дамп - Python вне конкуренции.

1.2. Инструменты: Pandas, re, datetime, json, xml.etree - базовый набор​

Погнали по списку того, что будем таскать из ящика:
  • Pandas - наша рабочая лошадка. Это Excel на стероидах. DataFrame позволяет фильтровать, группировать, объединять тысячи событий за доли секунды. Если ты ещё не знаком с Pandas - считай, ты не видел жизни.
  • re - регулярные выражения. Ими мы будем выкусывать IP, URL, хеши из текстового мусора. Без них никак.
  • datetime - работа со временем. Перевод строк в timestamp, нормализация часовых поясов, вычисление разниц. Всё для timeline.
  • json / xml.etree - многие современные логи (и даже Windows Event Log внутри) хранятся в JSON или XML. Надо уметь это парсить.
  • os, glob - чтобы ходить по папкам, собирать файлы.
  • argparse - для скриптов, которые запускаются из командной строки. Сделаем интерфейс по-человечески.
Кроме стандартных, понадобятся специализированные библиотеки:
  • python-evtx - читаем .evtx файлы напрямую.
  • python-registry - для работы с кустами реестра (но об этом в другой раз).
  • python-ntfs / pytsk3 - для MFT.
  • jinja2 - генерация красивых HTML-отчётов.
  • matplotlib / seaborn - графики.
  • requests - если надо отправить результаты в Elasticsearch или другой REST API.
Устанавливается всё одной командой:
pip install pandas python-evtx python-registry pytsk3 jinja2 matplotlib requests


1.3. Jupyter notebooks vs CLI scripts - когда что использовать​

Тут принцип простой:
  • Jupyter - для разведки. Когда ты впервые видишь новый формат логов, экспериментируешь с регулярками, строишь графики, чтобы понять, где копать. Ячейки позволяют перезапускать части кода, смотреть промежуточные результаты - идеально для исследовательской фазы.
  • CLI-скрипты - для боевого применения. Когда алгоритм отлажен, данные надо обрабатывать регулярно или в пайплайне. Запустил скрипт с параметрами, получил CSV/отчёт, пошёл дальше.
Но есть нюанс: ноутбуки тяжело версионировать в git, они жрут память, если загрузить гигабайтный лог прямо в ячейку. Поэтому для больших объёмов я предпочитаю писать скрипт, а в ноутбуке только отлаживать маленькие выборки.

Совет: используй библиотеку dask для работы с данными, которые не влезают в память. Она имитирует Pandas, но обрабатывает данные по частям. Но об этом позже.



2. Log Normalization​

2.1. Парсинг: Syslog (regex), Windows Event Log (python-evtx), Apache (CLF)​

Первый этап - вытащить структуру из сырых строк. У каждого формата свои приколы.

Syslog​

Классический syslog бывает двух основных форматов: старый (RFC3164) и новый (RFC5424). Старый выглядит так:

Код:
<34>Oct 11 22:14:15 mymachine su: 'su root' failed for lonvick on /dev/pts/8


Новый:

Код:
<34>1 2003-10-11T22:14:15.003Z mymachine.example.com su - - - 'su root' failed

Разбирать проще всего регуляркой, но нужно учитывать варианты. Я обычно делаю так:

Python:
Код:
import re

syslog_pattern = re.compile(
    r'^<(?P<pri>\d+)>(?P<timestamp>\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+(?P<host>\S+)\s+(?P<app>[^:]+):\s+(?P<message>.*)$'
)

Но это для старого. Для нового - другая регулярка. Либо можно попробовать универсальный подход: сначала попытаться распарсить как новый, если не получилось - как старый.

Подводный камень: в старом формате год не указывается, его приходится брать из даты файла или считать, что события за последние 12 месяцев. Ещё бывает, что timestamp включает год в начале - тогда своя регулярка.


Windows Event Log (.evtx)​

Здесь нам поможет библиотека python-evtx. Она умеет читать evtx файлы и выдавать XML каждой записи. Дальше парсим XML.

Пример базового извлечения событий:

Python:
Код:
import Evtx.Evtx as evtx
import xml.etree.ElementTree as ET

def parse_evtx(file_path):
    events = []
    with evtx.Evtx(file_path) as log:
        for record in log.records():
            xml_str = record.xml()
            root = ET.fromstring(xml_str)
            # Пространство имён обычно такое:
            ns = {'ns': 'http://schemas.microsoft.com/win/2004/08/events/event'}
            system = root.find('ns:System', ns)
            event_id = system.find('ns:EventID', ns).text
            time_created = system.find('ns:TimeCreated', ns).get('SystemTime')
            # Данные события могут быть в EventData/Data
            event_data = root.find('ns:EventData', ns)
            data_dict = {}
            if event_data is not None:
                for data in event_data.findall('ns:Data', ns):
                    name = data.get('Name', 'Unknown')
                    data_dict[name] = data.text
            events.append({
                'event_id': event_id,
                'timestamp': time_created,
                'data': data_dict
            })
    return events

Но этот код вытянет все события подряд. На практике нам нужны только определённые EventID (например, 4624, 4625 для логонов). Фильтровать можно либо на этапе парсинга, либо потом через Pandas.

Альтернатива: можно использовать wevtutil для экспорта evtx в XML/CSV, но это внешний инструмент, и он медленный. python-evtx работает быстрее и даёт контроль.


Apache access log (Common Log Format)​

Тут всё проще: поля разделены пробелами, но есть хитрости - в запросе могут быть пробелы, а в кавычках - что угодно. Стандартный combined формат:

Код:
127.0.0.1 - - [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"

Регулярка для такого:

Python:
Код:
apache_pattern = re.compile(
    r'^(?P<ip>\S+) \S+ \S+ \[(?P<timestamp>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) \S+" (?P<status>\d+) \S+ "(?P<referer>[^"]*)" "(?P<user_agent>[^"]*)"$'
)

Поля: ip, timestamp, method, path, status, referer, user_agent. Timestamp потом надо преобразовать в datetime.


2.2. Нормализация: единый формат timestamp, source, event_type, details​

Мы собрали события из разных источников, но они живут в своих структурах. Чтобы объединить их, нужна общая схема. Я предлагаю такую:
ПолеТипОписание
timestampdatetimeВремя события в UTC
sourcestrИдентификатор источника
('syslog', 'evtx', 'apache')
event_typestrТип события
( 'logon_failure', 'http_request')
hoststrХост, на котором произошло событие
rawstrИсходная строка (для отладки)
detailsdict/jsonДополнительные поля
(ip, user, status и т.д.)

Такой подход позволяет хранить всё в одном DataFrame и при необходимости обращаться к details.

Пример приведения записи из syslog к общему формату:

Python:
Код:
def normalize_syslog(parsed):
    return {
        'timestamp': pd.to_datetime(parsed['timestamp']),  # надо ещё год добавить
        'source': 'syslog',
        'event_type': 'syslog_message',
        'host': parsed['host'],
        'raw': parsed['raw'],
        'details': {'app': parsed['app'], 'message': parsed['message']}
}

Для evtx:

Python:
Код:
def normalize_evtx(parsed):
    # пример: событие 4625 (неудачный логон)
    if parsed['event_id'] == '4625':
        event_type = 'logon_failure'
    else:
        event_type = f'win_event_{parsed["event_id"]}'
    return {
        'timestamp': pd.to_datetime(parsed['timestamp']),
        'source': 'evtx',
        'event_type': event_type,
        'host': parsed.get('computer', 'unknown'),
        'raw': '',  # можно не хранить, если лень
        'details': parsed['data']
    }

2.3. Pandas DataFrame: загрузка, типизация, индексация по timestamp​

После того как мы нормализовали все события в список словарей, превращаем его в DataFrame:

Python:
Код:
import pandas as pd

df = pd.DataFrame(normalized_events)
df['timestamp'] = pd.to_datetime(df['timestamp'])  # убедимся, что тип правильный
df.set_index('timestamp', inplace=True)           # делаем индексом для временных рядов

Теперь у нас мощный инструмент фильтрации:

Python:
Код:
# все события за последний час
last_hour = df[df.index > datetime.now() - timedelta(hours=1)]

# все неудачные логоны
logon_failures = df[df['event_type'] == 'logon_failure']

# группировка по источнику и типу
df.groupby(['source', 'event_type']).size()

Pandas автоматически сортирует по индексу, если нужно - сделаем df.sort_index().

Типизация: детали хранятся в словаре, но иногда полезно вытащить конкретные поля в отдельные колонки:

Python:
df['ip'] = df['details'].apply(lambda x: x.get('ip', None))

Но это может быть медленно для больших данных. Лучше сразу при нормализации добавлять важные поля как отдельные колонки, если они нужны часто. Например, для логонов: ip, user, status.

3. Pattern Matching с RegEx​

Регулярки - наше всё, когда нужно выковырять индикаторы из текста. Рассмотрим типовые задачи.

Простая регулярка для IPv4 (без проверки корректности диапазонов):

Python:
ipv4_pattern <span>=</span> re<span>.</span><span>compile</span><span>(</span><span>r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b'</span><span>)</span>
Но она ловит и "999.999.999.999", что невалидно. Можно усложнить:

Python:
ipv4_valid <span>=</span> re<span>.</span><span>compile</span><span>(</span><span>r'\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b'</span><span>)</span>
Применяем к колонке:

Python:
df<span>[</span><span>'ips'</span><span>]</span> <span>=</span> df<span>[</span><span>'raw'</span><span>]</span><span>.</span><span>str</span><span>.</span>findall<span>(</span>ipv4_valid<span>)</span>
Если несколько IP в одной строке, findall вернёт список.

IPv6 сложнее, но есть стандартная регулярка из RFC. Я использую упрощённый вариант:

Python:
ipv6_pattern <span>=</span> re<span>.</span><span>compile</span><span>(</span><span>r'\b(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}\b|\b(?:[a-fA-F0-9]{1,4}:){1,7}:|\b:(?::[a-fA-F0-9]{1,4}){1,7}\b'</span><span>)</span>
Она покрывает большинство случаев, но не все (например, сжатые нули).

После извлечения IP полезно понять, какие из них внутренние, а какие внешние. Модуль ipaddress нам в помощь:

Python:
<span>import</span> ipaddress<br><br><span>def</span> <span>classify_ip</span><span>(</span>ip_str<span>)</span><span>:</span><br> <span>try</span><span>:</span><br> ip <span>=</span> ipaddress<span>.</span>ip_address<span>(</span>ip_str<span>)</span><br> <span>if</span> ip<span>.</span>is_private<span>:</span><br> <span>return</span> <span>'private'</span><br> <span>elif</span> ip<span>.</span>is_global<span>:</span><br> <span>return</span> <span>'public'</span><br> <span>else</span><span>:</span><br> <span>return</span> <span>'other'</span><br> <span>except</span><span>:</span><br> <span>return</span> <span>'invalid'</span><br><br>df<span>[</span><span>'ip_class'</span><span>]</span> <span>=</span> df<span>[</span><span>'ip'</span><span>]</span><span>.</span><span>apply</span><span>(</span>classify_ip<span>)</span>

Регулярка для URL - дело неблагодарное, потому что в логах URL часто обрезаны или экранированы. Простейший вариант:

Python:
url_pattern <span>=</span> re<span>.</span><span>compile</span><span>(</span><span>r'https?://[^\s&lt;&gt;"\'{}|\\^`\[\]]+'</span><span>)</span>
Она ловит http/https ссылки до первого пробела или спецсимвола. Можно расширить на ftp, file и т.д.

Домены вытаскиваем по похожему принципу, но без протокола:

Python:
domain_pattern <span>=</span> re<span>.</span><span>compile</span><span>(</span><span>r'\b(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}\b'</span><span>)</span>
Осторожно: ловит и "localhost", и ".com" отдельно. Лучше фильтровать потом, отбрасывая слишком короткие.

В отчётах часто встречаются обезвреженные индикаторы: hxxp://example[.]com. Чтобы их использовать, нужно привести к нормальному виду:

Python:
<span>def</span> <span>refang</span><span>(</span>ioc<span>)</span><span>:</span><br> <span>return</span> ioc<span>.</span>replace<span>(</span><span>'[.]'</span><span>,</span> <span>'.'</span><span>)</span><span>.</span>replace<span>(</span><span>'hxxp'</span><span>,</span> <span>'http'</span><span>)</span><span>.</span>replace<span>(</span><span>'hxxps'</span><span>,</span> <span>'https'</span><span>)</span>

И наоборот, для публикации отчётов безопасно показывать defang:

Python:
<span>def</span> <span>defang</span><span>(</span>ioc<span>)</span><span>:</span><br> <span>return</span> ioc<span>.</span>replace<span>(</span><span>'.'</span><span>,</span> <span>'[.]'</span><span>)</span><span>.</span>replace<span>(</span><span>'http'</span><span>,</span> <span>'hxxp'</span><span>,</span> <span>1</span><span>)</span>

Хеши - это последовательности hex-символов определённой длины. Регулярки:

Python:
md5_pattern <span>=</span> re<span>.</span><span>compile</span><span>(</span><span>r'\b[a-fA-F0-9]{32}\b'</span><span>)</span><br>sha1_pattern <span>=</span> re<span>.</span><span>compile</span><span>(</span><span>r'\b[a-fA-F0-9]{40}\b'</span><span>)</span><br>sha256_pattern <span>=</span> re<span>.</span><span>compile</span><span>(</span><span>r'\b[a-fA-F0-9]{64}\b'</span><span>)</span>

Всё вместе можно объединить одной, но тогда не различить тип. Лучше искать отдельно.

Применяем к тексту и собираем все хеши:

Python:
<span>def</span> <span>extract_hashes</span><span>(</span>text<span>)</span><span>:</span><br> hashes <span>=</span> <span>{</span><span>}</span><br> hashes<span>[</span><span>'md5'</span><span>]</span> <span>=</span> md5_pattern<span>.</span>findall<span>(</span>text<span>)</span><br> hashes<span>[</span><span>'sha1'</span><span>]</span> <span>=</span> sha1_pattern<span>.</span>findall<span>(</span>text<span>)</span><br> hashes<span>[</span><span>'sha256'</span><span>]</span> <span>=</span> sha256_pattern<span>.</span>findall<span>(</span>text<span>)</span><br> <span>return</span> hashes

Важно: хеши могут пересекаться (например, SHA1 длиннее MD5, но MD5 может быть частью SHA1). Поэтому порядок поиска лучше от большего к меньшему, или использовать границы слов.


SuperTimeline (или суперхронология) - это когда мы собираем все события из всех доступных источников в одну временную линию. Сюда входят не только логи, но и артефакты файловой системы (времена создания/изменения файлов), реестр (последние записанные значения), Prefetch, события из памяти и т.д.

В нашем случае мы ограничимся логами, но подход тот же: нормализуем каждый источник в DataFrame с общим форматом, затем объединяем их.

Если у нас несколько DataFrame с одинаковыми колонками, используем pd.concat:

Python:
df_timeline <span>=</span> pd<span>.</span>concat<span>(</span><span>[</span>df_syslog<span>,</span> df_evtx<span>,</span> df_apache<span>]</span><span>,</span> ignore_index<span>=</span><span>False</span><span>)</span><br>df_timeline<span>.</span>sort_index<span>(</span>inplace<span>=</span><span>True</span><span>)</span>

Если структуры различаются, можно привести их к общему знаменателю, добавив недостающие колонки со значениями None.

Если нужно объединить по ключу (например, по IP), используем merge. Но для timeline чаще просто конкатенация.

Пример: построим количество событий по часам:

Python:
events_per_hour <span>=</span> df_timeline<span>.</span>resample<span>(</span><span>'H'</span><span>)</span><span>.</span>size<span>(</span><span>)</span><br>events_per_hour<span>.</span>plot<span>(</span><span>)</span> <span># matplotlib</span>

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


Python:
<span>import</span> matplotlib<span>.</span>pyplot <span>as</span> plt<br><br>fig<span>,</span> ax <span>=</span> plt<span>.</span>subplots<span>(</span>figsize<span>=</span><span>(</span><span>12</span><span>,</span><span>6</span><span>)</span><span>)</span><br><span>for</span> source <span>in</span> df_timeline<span>[</span><span>'source'</span><span>]</span><span>.</span>unique<span>(</span><span>)</span><span>:</span><br> subset <span>=</span> df_timeline<span>[</span>df_timeline<span>[</span><span>'source'</span><span>]</span> <span>==</span> source<span>]</span><br> <span># scatter plot событий во времени</span><br> ax<span>.</span>scatter<span>(</span>subset<span>.</span>index<span>,</span> <span>[</span>source<span>]</span><span>*</span><span>len</span><span>(</span>subset<span>)</span><span>,</span> label<span>=</span>source<span>,</span> s<span>=</span><span>1</span><span>)</span><br>ax<span>.</span>legend<span>(</span><span>)</span><br>plt<span>.</span>show<span>(</span><span>)</span>

Но scatter с миллионом точек может тормозить. Лучше агрегировать по минутам и строить тепловую карту или ступенчатый график.

Jinja2 позволяет генерировать красивые отчёты из шаблонов. Создаём шаблон report_template.html:

HTML:
<span><span><span>&lt;</span>html</span><span>&gt;</span></span><br><span><span><span>&lt;</span>head</span><span>&gt;</span></span><span><span><span>&lt;</span>title</span><span>&gt;</span></span>Timeline Report<span><span><span>&lt;/</span>title</span><span>&gt;</span></span><span><span><span>&lt;/</span>head</span><span>&gt;</span></span><br><span><span><span>&lt;</span>body</span><span>&gt;</span></span><br><span><span><span>&lt;</span>h1</span><span>&gt;</span></span>Timeline from {{ start_date }} to {{ end_date }}<span><span><span>&lt;/</span>h1</span><span>&gt;</span></span><br><span><span><span>&lt;</span>table</span> <span>border</span><span><span>=</span><span>"</span>1<span>"</span></span><span>&gt;</span></span><br><span><span><span>&lt;</span>tr</span><span>&gt;</span></span><span><span><span>&lt;</span>th</span><span>&gt;</span></span>Timestamp<span><span><span>&lt;/</span>th</span><span>&gt;</span></span><span><span><span>&lt;</span>th</span><span>&gt;</span></span>Source<span><span><span>&lt;/</span>th</span><span>&gt;</span></span><span><span><span>&lt;</span>th</span><span>&gt;</span></span>Event Type<span><span><span>&lt;/</span>th</span><span>&gt;</span></span><span><span><span>&lt;</span>th</span><span>&gt;</span></span>Details<span><span><span>&lt;/</span>th</span><span>&gt;</span></span><span><span><span>&lt;/</span>tr</span><span>&gt;</span></span><br>{% for event in events %}<br><span><span><span>&lt;</span>tr</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>td</span><span>&gt;</span></span>{{ event.timestamp }}<span><span><span>&lt;/</span>td</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>td</span><span>&gt;</span></span>{{ event.source }}<span><span><span>&lt;/</span>td</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>td</span><span>&gt;</span></span>{{ event.event_type }}<span><span><span>&lt;/</span>td</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>td</span><span>&gt;</span></span>{{ event.details }}<span><span><span>&lt;/</span>td</span><span>&gt;</span></span><br><span><span><span>&lt;/</span>tr</span><span>&gt;</span></span><br>{% endfor %}<br><span><span><span>&lt;/</span>table</span><span>&gt;</span></span><br><span><span><span>&lt;/</span>body</span><span>&gt;</span></span><br><span><span><span>&lt;/</span>html</span><span>&gt;</span></span>

В скрипте:

Python:
<span>from</span> jinja2 <span>import</span> Template<br><span>with</span> <span>open</span><span>(</span><span>'report_template.html'</span><span>)</span> <span>as</span> f<span>:</span><br> template <span>=</span> Template<span>(</span>f<span>.</span>read<span>(</span><span>)</span><span>)</span><br><br>html_output <span>=</span> template<span>.</span>render<span>(</span><br> start_date<span>=</span>df_timeline<span>.</span>index<span>.</span><span>min</span><span>(</span><span>)</span><span>,</span><br> end_date<span>=</span>df_timeline<span>.</span>index<span>.</span><span>max</span><span>(</span><span>)</span><span>,</span><br> events<span>=</span>df_timeline<span>.</span>reset_index<span>(</span><span>)</span><span>.</span>to_dict<span>(</span>orient<span>=</span><span>'records'</span><span>)</span><br><span>)</span><br><br><span>with</span> <span>open</span><span>(</span><span>'timeline_report.html'</span><span>,</span> <span>'w'</span><span>)</span> <span>as</span> f<span>:</span><br> f<span>.</span>write<span>(</span>html_output<span>)</span>

Можно добавить графики в виде base64-encoded PNG или использовать Plotly для интерактива.


1773426857940.webp


Теперь, когда теория позади, перейдём к реальным скриптам, которые ты сможешь завтра же применить на практике. Каждый скрипт я постараюсь объяснить построчно, показать пример вывода и указать, где могут вылезти грабли.

Задача: проанализировать файл Security.evtx, выявить все события входа (успешного и неудачного), посчитать количество неудачных попыток по IP и пользователям, построить timeline и выделить возможный брутфорс.

Код:

Python:
<span>#!/usr/bin/env python3</span><br><span># evtx_logon_analysis.py</span><br><br><span>import</span> argparse<br><span>import</span> pandas <span>as</span> pd<br><span>import</span> Evtx<span>.</span>Evtx <span>as</span> evtx<br><span>import</span> xml<span>.</span>etree<span>.</span>ElementTree <span>as</span> ET<br><span>from</span> datetime <span>import</span> datetime<br><br><span>def</span> <span>parse_evtx_logons</span><span>(</span>file_path<span>)</span><span>:</span><br> events <span>=</span> <span>[</span><span>]</span><br> <span>with</span> evtx<span>.</span>Evtx<span>(</span>file_path<span>)</span> <span>as</span> log<span>:</span><br> <span>for</span> record <span>in</span> log<span>.</span>records<span>(</span><span>)</span><span>:</span><br> xml_str <span>=</span> record<span>.</span>xml<span>(</span><span>)</span><br> root <span>=</span> ET<span>.</span>fromstring<span>(</span>xml_str<span>)</span><br> ns <span>=</span> <span>{</span><span>'ns'</span><span>:</span> <span>'http://schemas.microsoft.com/win/2004/08/events/event'</span><span>}</span><br> system <span>=</span> root<span>.</span>find<span>(</span><span>'ns:System'</span><span>,</span> ns<span>)</span><br> event_id <span>=</span> system<span>.</span>find<span>(</span><span>'ns:EventID'</span><span>,</span> ns<span>)</span><span>.</span>text<br> <span>if</span> event_id <span>not</span> <span>in</span> <span>[</span><span>'4624'</span><span>,</span> <span>'4625'</span><span>]</span><span>:</span><br> <span>continue</span><br> time_created <span>=</span> system<span>.</span>find<span>(</span><span>'ns:TimeCreated'</span><span>,</span> ns<span>)</span><span>.</span>get<span>(</span><span>'SystemTime'</span><span>)</span><br> computer <span>=</span> system<span>.</span>find<span>(</span><span>'ns:Computer'</span><span>,</span> ns<span>)</span><span>.</span>text<br> event_data <span>=</span> root<span>.</span>find<span>(</span><span>'ns:EventData'</span><span>,</span> ns<span>)</span><br> data <span>=</span> <span>{</span><span>}</span><br> <span>if</span> event_data <span>is</span> <span>not</span> <span>None</span><span>:</span><br> <span>for</span> d <span>in</span> event_data<span>.</span>findall<span>(</span><span>'ns:Data'</span><span>,</span> ns<span>)</span><span>:</span><br> name <span>=</span> d<span>.</span>get<span>(</span><span>'Name'</span><span>,</span> <span>'unknown'</span><span>)</span><br> data<span>[</span>name<span>]</span> <span>=</span> d<span>.</span>text<br> events<span>.</span>append<span>(</span><span>{</span><br> <span>'timestamp'</span><span>:</span> time_created<span>,</span><br> <span>'event_id'</span><span>:</span> event_id<span>,</span><br> <span>'computer'</span><span>:</span> computer<span>,</span><br> <span>'data'</span><span>:</span> data<br> <span>}</span><span>)</span><br> <span>return</span> events<br><br><span>def</span> <span>main</span><span>(</span><span>)</span><span>:</span><br> parser <span>=</span> argparse<span>.</span>ArgumentParser<span>(</span>description<span>=</span><span>'Analyze Windows Logon Events from .evtx'</span><span>)</span><br> parser<span>.</span>add_argument<span>(</span><span>'evtx'</span><span>,</span> <span>help</span><span>=</span><span>'Path to Security.evtx file'</span><span>)</span><br> parser<span>.</span>add_argument<span>(</span><span>'--output'</span><span>,</span> <span>'-o'</span><span>,</span> default<span>=</span><span>'logon_report.csv'</span><span>,</span> <span>help</span><span>=</span><span>'Output CSV file'</span><span>)</span><br> args <span>=</span> parser<span>.</span>parse_args<span>(</span><span>)</span><br><br> <span>print</span><span>(</span><span><span>f"[*] Parsing </span><span><span>{</span>args<span>.</span>evtx<span>}</span></span><span>..."</span></span><span>)</span><br> raw_events <span>=</span> parse_evtx_logons<span>(</span>args<span>.</span>evtx<span>)</span><br> <span>print</span><span>(</span><span><span>f"[+] Found </span><span><span>{</span><span>len</span><span>(</span>raw_events<span>)</span><span>}</span></span><span> logon events"</span></span><span>)</span><br><br> <span># нормализация</span><br> rows <span>=</span> <span>[</span><span>]</span><br> <span>for</span> ev <span>in</span> raw_events<span>:</span><br> row <span>=</span> <span>{</span><br> <span>'timestamp'</span><span>:</span> pd<span>.</span>to_datetime<span>(</span>ev<span>[</span><span>'timestamp'</span><span>]</span><span>)</span><span>,</span><br> <span>'event_id'</span><span>:</span> ev<span>[</span><span>'event_id'</span><span>]</span><span>,</span><br> <span>'computer'</span><span>:</span> ev<span>[</span><span>'computer'</span><span>]</span><span>,</span><br> <span>'ip'</span><span>:</span> ev<span>[</span><span>'data'</span><span>]</span><span>.</span>get<span>(</span><span>'IpAddress'</span><span>,</span> <span>''</span><span>)</span><span>.</span>replace<span>(</span><span>'::ffff:'</span><span>,</span> <span>''</span><span>)</span><span>,</span> <span># убираем IPv6 mapping</span><br> <span>'user'</span><span>:</span> ev<span>[</span><span>'data'</span><span>]</span><span>.</span>get<span>(</span><span>'TargetUserName'</span><span>,</span> <span>''</span><span>)</span><span>,</span><br> <span>'logon_type'</span><span>:</span> ev<span>[</span><span>'data'</span><span>]</span><span>.</span>get<span>(</span><span>'LogonType'</span><span>,</span> <span>''</span><span>)</span><span>,</span><br> <span>'status'</span><span>:</span> <span>'success'</span> <span>if</span> ev<span>[</span><span>'event_id'</span><span>]</span> <span>==</span> <span>'4624'</span> <span>else</span> <span>'failure'</span><br> <span>}</span><br> rows<span>.</span>append<span>(</span>row<span>)</span><br><br> df <span>=</span> pd<span>.</span>DataFrame<span>(</span>rows<span>)</span><br> df<span>.</span>set_index<span>(</span><span>'timestamp'</span><span>,</span> inplace<span>=</span><span>True</span><span>)</span><br> df<span>.</span>sort_index<span>(</span>inplace<span>=</span><span>True</span><span>)</span><br><br> <span># статистика</span><br> <span>print</span><span>(</span><span>"\n=== Logon Summary ==="</span><span>)</span><br> <span>print</span><span>(</span>df<span>.</span>groupby<span>(</span><span>'status'</span><span>)</span><span>.</span>size<span>(</span><span>)</span><span>)</span><br><br> <span>print</span><span>(</span><span>"\n=== Top 10 Failed Logons by IP ==="</span><span>)</span><br> failed <span>=</span> df<span>[</span>df<span>[</span><span>'status'</span><span>]</span> <span>==</span> <span>'failure'</span><span>]</span><br> <span>print</span><span>(</span>failed<span>[</span><span>'ip'</span><span>]</span><span>.</span>value_counts<span>(</span><span>)</span><span>.</span>head<span>(</span><span>10</span><span>)</span><span>)</span><br><br> <span>print</span><span>(</span><span>"\n=== Top 10 Failed Logons by User ==="</span><span>)</span><br> <span>print</span><span>(</span>failed<span>[</span><span>'user'</span><span>]</span><span>.</span>value_counts<span>(</span><span>)</span><span>.</span>head<span>(</span><span>10</span><span>)</span><span>)</span><br><br> <span># брутфорс-детектор: больше 10 неудач с одного IP за 5 минут</span><br> <span>print</span><span>(</span><span>"\n=== Potential Brute-Force IPs (threshold 10 failures in 5 min) ==="</span><span>)</span><br> <span># используем rolling window</span><br> failed_by_ip <span>=</span> failed<span>.</span>groupby<span>(</span><span>'ip'</span><span>)</span><span>.</span>resample<span>(</span><span>'5T'</span><span>)</span><span>.</span>size<span>(</span><span>)</span><span>.</span>reset_index<span>(</span><span>)</span><br> brute_ips <span>=</span> failed_by_ip<span>[</span>failed_by_ip<span>[</span><span>0</span><span>]</span> <span>&gt;=</span> <span>10</span><span>]</span><span>[</span><span>'ip'</span><span>]</span><span>.</span>unique<span>(</span><span>)</span><br> <span>print</span><span>(</span>brute_ips<span>)</span><br><br> <span># сохраняем CSV</span><br> df<span>.</span>to_csv<span>(</span>args<span>.</span>output<span>)</span><br> <span>print</span><span>(</span><span><span>f"[+] Report saved to </span><span><span>{</span>args<span>.</span>output<span>}</span></span><span>"</span></span><span>)</span><br><br><span>if</span> __name__ <span>==</span> <span>'__main__'</span><span>:</span><br> main<span>(</span><span>)</span>

Пояснения:
  • Используем python-evtx для чтения, фильтруем только 4624 и 4625.
  • Извлекаем IP, имя пользователя, тип входа.
  • Статус определяем по event_id.
  • Для поиска брутфорса группируем неудачные попытки по IP и пересэмплируем по 5 минутам. Если в каком-то пятиминутном окне >=10 попыток - считаем IP подозрительным.
  • Выводим статистику и сохраняем в CSV.
Пример вывода:

Код:
[*] Parsing Security.evtx...<br>[+] Found 12500 logon events<br><br>=== Logon Summary ===<br>status<br>failure 4200<br>success 8300<br><br>=== Top 10 Failed Logons by IP ===<br>192.168.1.100 1500<br>10.0.0.55 800<br>...<br><br>=== Top 10 Failed Logons by User ===<br>administrator 2000<br>user1 500<br>...<br><br>=== Potential Brute-Force IPs (threshold 10 failures in 5 min) ===<br>['192.168.1.100', '10.0.0.55']

Грабли:
  • В поле IpAddress может приходить IPv6-отображение IPv4 (::ffff:192.168.1.1), мы обрезаем.
  • Для старых Windows в данных могут отсутствовать некоторые поля.
  • Время в evtx хранится в UTC, но Windows часто пишет локальное время. Проверь часовой пояс.
Задача: разобрать access.log веб-сервера, выделить IP с большим количеством запросов, найти подозрительные пути (админки, скрипты, сканеры), построить график активности.

Код:

Python:
<span>#!/usr/bin/env python3</span><br><span># apache_analyze.py</span><br><br><span>import</span> argparse<br><span>import</span> re<br><span>import</span> pandas <span>as</span> pd<br><span>import</span> matplotlib<span>.</span>pyplot <span>as</span> plt<br><span>from</span> collections <span>import</span> Counter<br><br>apache_pattern <span>=</span> re<span>.</span><span>compile</span><span>(</span><br> <span>r'^(?P&lt;ip&gt;\S+) \S+ \S+ \[(?P&lt;timestamp&gt;[^\]]+)\] "(?P&lt;method&gt;\S+) (?P&lt;path&gt;\S+) \S+" (?P&lt;status&gt;\d+) \S+ "(?P&lt;referer&gt;[^"]*)" "(?P&lt;user_agent&gt;[^"]*)"$'</span><br><span>)</span><br><br><span>def</span> <span>parse_apache_line</span><span>(</span>line<span>)</span><span>:</span><br> m <span>=</span> apache_pattern<span>.</span>match<span>(</span>line<span>)</span><br> <span>if</span> <span>not</span> m<span>:</span><br> <span>return</span> <span>None</span><br> d <span>=</span> m<span>.</span>groupdict<span>(</span><span>)</span><br> <span># преобразуем timestamp</span><br> ts_str <span>=</span> d<span>[</span><span>'timestamp'</span><span>]</span><br> <span># формат: 10/Oct/2000:13:55:36 -0700</span><br> ts <span>=</span> pd<span>.</span>to_datetime<span>(</span>ts_str<span>,</span> <span>format</span><span>=</span><span>'%d/%b/%Y:%H:%M:%S %z'</span><span>)</span><br> <span>return</span> <span>{</span><br> <span>'timestamp'</span><span>:</span> ts<span>,</span><br> <span>'ip'</span><span>:</span> d<span>[</span><span>'ip'</span><span>]</span><span>,</span><br> <span>'method'</span><span>:</span> d<span>[</span><span>'method'</span><span>]</span><span>,</span><br> <span>'path'</span><span>:</span> d<span>[</span><span>'path'</span><span>]</span><span>,</span><br> <span>'status'</span><span>:</span> <span>int</span><span>(</span>d<span>[</span><span>'status'</span><span>]</span><span>)</span><span>,</span><br> <span>'referer'</span><span>:</span> d<span>[</span><span>'referer'</span><span>]</span><span>,</span><br> <span>'user_agent'</span><span>:</span> d<span>[</span><span>'user_agent'</span><span>]</span><br> <span>}</span><br><br><span>def</span> <span>main</span><span>(</span><span>)</span><span>:</span><br> parser <span>=</span> argparse<span>.</span>ArgumentParser<span>(</span>description<span>=</span><span>'Analyze Apache access log'</span><span>)</span><br> parser<span>.</span>add_argument<span>(</span><span>'logfile'</span><span>,</span> <span>help</span><span>=</span><span>'Path to access.log'</span><span>)</span><br> parser<span>.</span>add_argument<span>(</span><span>'--output'</span><span>,</span> <span>'-o'</span><span>,</span> default<span>=</span><span>'apache_report.csv'</span><span>)</span><br> args <span>=</span> parser<span>.</span>parse_args<span>(</span><span>)</span><br><br> <span>print</span><span>(</span><span><span>f"[*] Parsing </span><span><span>{</span>args<span>.</span>logfile<span>}</span></span><span>..."</span></span><span>)</span><br> events <span>=</span> <span>[</span><span>]</span><br> <span>with</span> <span>open</span><span>(</span>args<span>.</span>logfile<span>,</span> <span>'r'</span><span>,</span> encoding<span>=</span><span>'utf-8'</span><span>,</span> errors<span>=</span><span>'ignore'</span><span>)</span> <span>as</span> f<span>:</span><br> <span>for</span> line <span>in</span> f<span>:</span><br> ev <span>=</span> parse_apache_line<span>(</span>line<span>)</span><br> <span>if</span> ev<span>:</span><br> events<span>.</span>append<span>(</span>ev<span>)</span><br> <span>print</span><span>(</span><span><span>f"[+] Parsed </span><span><span>{</span><span>len</span><span>(</span>events<span>)</span><span>}</span></span><span> lines"</span></span><span>)</span><br><br> df <span>=</span> pd<span>.</span>DataFrame<span>(</span>events<span>)</span><br> df<span>.</span>set_index<span>(</span><span>'timestamp'</span><span>,</span> inplace<span>=</span><span>True</span><span>)</span><br> df<span>.</span>sort_index<span>(</span>inplace<span>=</span><span>True</span><span>)</span><br><br> <span>print</span><span>(</span><span>"\n=== Top 10 IPs by request count ==="</span><span>)</span><br> <span>print</span><span>(</span>df<span>[</span><span>'ip'</span><span>]</span><span>.</span>value_counts<span>(</span><span>)</span><span>.</span>head<span>(</span><span>10</span><span>)</span><span>)</span><br><br> <span>print</span><span>(</span><span>"\n=== Top 10 most requested paths ==="</span><span>)</span><br> <span>print</span><span>(</span>df<span>[</span><span>'path'</span><span>]</span><span>.</span>value_counts<span>(</span><span>)</span><span>.</span>head<span>(</span><span>10</span><span>)</span><span>)</span><br><br> <span># подозрительные пути (словари, скрипты)</span><br> suspicious <span>=</span> <span>[</span><span>'admin'</span><span>,</span> <span>'phpmyadmin'</span><span>,</span> <span>'wp-admin'</span><span>,</span> <span>'mysql'</span><span>,</span> <span>'sql'</span><span>,</span> <span>'backup'</span><span>,</span> <span>'config'</span><span>,</span> <span>'.env'</span><span>]</span><br> pattern <span>=</span> <span>'|'</span><span>.</span>join<span>(</span>suspicious<span>)</span><br> suspicious_hits <span>=</span> df<span>[</span>df<span>[</span><span>'path'</span><span>]</span><span>.</span><span>str</span><span>.</span>contains<span>(</span>pattern<span>,</span> case<span>=</span><span>False</span><span>,</span> na<span>=</span><span>False</span><span>)</span><span>]</span><br> <span>print</span><span>(</span><span><span>f"\n=== Suspicious path hits (</span><span><span>{</span><span>len</span><span>(</span>suspicious_hits<span>)</span><span>}</span></span><span>) ==="</span></span><span>)</span><br> <span>print</span><span>(</span>suspicious_hits<span>[</span><span>'path'</span><span>]</span><span>.</span>value_counts<span>(</span><span>)</span><span>.</span>head<span>(</span><span>10</span><span>)</span><span>)</span><br><br> <span># статусы ошибок</span><br> errors <span>=</span> df<span>[</span>df<span>[</span><span>'status'</span><span>]</span> <span>&gt;=</span> <span>400</span><span>]</span><br> <span>print</span><span>(</span><span><span>f"\n=== HTTP errors (4xx/5xx): </span><span><span>{</span><span>len</span><span>(</span>errors<span>)</span><span>}</span></span><span> ==="</span></span><span>)</span><br> <span>print</span><span>(</span>errors<span>[</span><span>'status'</span><span>]</span><span>.</span>value_counts<span>(</span><span>)</span><span>)</span><br><br> <span># временной график</span><br> hits_per_hour <span>=</span> df<span>.</span>resample<span>(</span><span>'H'</span><span>)</span><span>.</span>size<span>(</span><span>)</span><br> hits_per_hour<span>.</span>plot<span>(</span>figsize<span>=</span><span>(</span><span>12</span><span>,</span><span>4</span><span>)</span><span>,</span> title<span>=</span><span>'Requests per hour'</span><span>)</span><br> plt<span>.</span>tight_layout<span>(</span><span>)</span><br> plt<span>.</span>savefig<span>(</span><span>'requests_per_hour.png'</span><span>)</span><br> <span>print</span><span>(</span><span>"[+] Graph saved to requests_per_hour.png"</span><span>)</span><br><br> df<span>.</span>to_csv<span>(</span>args<span>.</span>output<span>)</span><br> <span>print</span><span>(</span><span><span>f"[+] Report saved to </span><span><span>{</span>args<span>.</span>output<span>}</span></span><span>"</span></span><span>)</span><br><br><span>if</span> __name__ <span>==</span> <span>'__main__'</span><span>:</span><br> main<span>(</span><span>)</span>

Пояснения:
  • Регулярка под combined log format.
  • Извлекаем timestamp с часовым поясом, Pandas его понимает.
  • Считаем топ IP и пути.
  • Ищем подозрительные слова в пути (часто используются сканерами).
  • Строим график числа запросов по часам.
Грабли:
  • В логах могут быть невалидные строки (обрывки), нужно обрабатывать ошибки.
  • Путь может быть закодирован, но мы ищем подстроки, это работает.
  • Если лог огромный, чтение построчно с регуляркой может быть медленным. Можно использовать pandas.read_csv с разделителем пробел, но тогда сложности с кавычками. Для ускорения можно применить re.compile один раз и параллельную обработку (но это уже сложнее).

5.3. Скрипт 3: artifact parser - $MFT timeline с python-ntfs​

Задача: прочитать файл $MFT, извлечь временные метки создания, изменения, записи и доступа для каждого файла, построить timeline активности файловой системы. Это полезно для выявления времени появления вредоносных файлов, их модификаций.

Важно: работа с MFT требует либо прав администратора (чмонтировать диск), либо доступа к сырому файлу $MFT (можно скопировать с помощью инструментов типа rawcopy). Для примера будем использовать библиотеку python-ntfs (GitHub - williballenthin/python-ntfs: Open source Python library for NTFS analysis), которая умеет парсить MFT напрямую.

Установка: pip install python-ntfs

Код:

Python:
<span>#!/usr/bin/env python3</span><br><span># mft_timeline.py</span><br><br><span>import</span> argparse<br><span>import</span> pandas <span>as</span> pd<br><span>from</span> ntfs<span>.</span>mft <span>import</span> MFT<br><span>from</span> ntfs<span>.</span>attributes <span>import</span> ATTR_TYPE<br><span>import</span> os<br><br><span>def</span> <span>parse_mft</span><span>(</span>mft_path<span>)</span><span>:</span><br> records <span>=</span> <span>[</span><span>]</span><br> mft <span>=</span> MFT<span>(</span>mft_path<span>)</span><br> <span>for</span> record <span>in</span> mft<span>.</span>records<span>(</span><span>)</span><span>:</span><br> <span>if</span> record<span>.</span>header<span>.</span>base_record_reference <span>!=</span> <span>0</span><span>:</span><br> <span>continue</span> <span># пропускаем расширенные записи, обычно они дублируют базовые</span><br> <span># Получаем имена файлов из атрибута $FILE_NAME</span><br> fname_attr <span>=</span> <span>None</span><br> <span>for</span> attr <span>in</span> record<span>.</span>attributes<span>(</span><span>)</span><span>:</span><br> <span>if</span> attr<span>.</span><span>type</span> <span>==</span> ATTR_TYPE<span>.</span>FILE_NAME<span>:</span><br> fname_attr <span>=</span> attr<br> <span>break</span><br> <span>if</span> <span>not</span> fname_attr<span>:</span><br> <span>continue</span><br> fn <span>=</span> fname_attr<span>.</span>file_name<span>(</span><span>)</span><br> name <span>=</span> fn<span>.</span>name<br> parent_ref <span>=</span> fn<span>.</span>parent_directory<br> <span># Временные метки (в 100-нс интервалах от 1601-01-01)</span><br> create_time <span>=</span> fn<span>.</span>created_time<br> modify_time <span>=</span> fn<span>.</span>modified_time<br> mft_change_time <span>=</span> fn<span>.</span>mft_changed_time<br> access_time <span>=</span> fn<span>.</span>accessed_time<br> <span># Конвертируем в datetime</span><br> <span>def</span> <span>ntfs_time_to_datetime</span><span>(</span>ntfs_time<span>)</span><span>:</span><br> <span># ntfs_time - 64-bit, количество 100-нс интервалов с 1601-01-01</span><br> <span>if</span> ntfs_time <span>==</span> <span>0</span><span>:</span><br> <span>return</span> <span>None</span><br> <span># Конвертируем в секунды с 1970</span><br> unix_time <span>=</span> <span>(</span>ntfs_time <span>-</span> <span>116444736000000000</span><span>)</span> <span>/</span> <span>10000000</span><br> <span>return</span> pd<span>.</span>to_datetime<span>(</span>unix_time<span>,</span> unit<span>=</span><span>'s'</span><span>)</span><br> records<span>.</span>append<span>(</span><span>{</span><br> <span>'name'</span><span>:</span> name<span>,</span><br> <span>'parent_ref'</span><span>:</span> parent_ref<span>,</span><br> <span>'create'</span><span>:</span> ntfs_time_to_datetime<span>(</span>create_time<span>)</span><span>,</span><br> <span>'modify'</span><span>:</span> ntfs_time_to_datetime<span>(</span>modify_time<span>)</span><span>,</span><br> <span>'mft_change'</span><span>:</span> ntfs_time_to_datetime<span>(</span>mft_change_time<span>)</span><span>,</span><br> <span>'access'</span><span>:</span> ntfs_time_to_datetime<span>(</span>access_time<span>)</span><br> <span>}</span><span>)</span><br> <span>return</span> records<br><br><span>def</span> <span>main</span><span>(</span><span>)</span><span>:</span><br> parser <span>=</span> argparse<span>.</span>ArgumentParser<span>(</span>description<span>=</span><span>'Parse $MFT and generate timeline'</span><span>)</span><br> parser<span>.</span>add_argument<span>(</span><span>'mft'</span><span>,</span> <span>help</span><span>=</span><span>'Path to $MFT file'</span><span>)</span><br> parser<span>.</span>add_argument<span>(</span><span>'--output'</span><span>,</span> <span>'-o'</span><span>,</span> default<span>=</span><span>'mft_timeline.csv'</span><span>)</span><br> args <span>=</span> parser<span>.</span>parse_args<span>(</span><span>)</span><br><br> <span>print</span><span>(</span><span><span>f"[*] Parsing MFT from </span><span><span>{</span>args<span>.</span>mft<span>}</span></span><span>..."</span></span><span>)</span><br> records <span>=</span> parse_mft<span>(</span>args<span>.</span>mft<span>)</span><br> <span>print</span><span>(</span><span><span>f"[+] Loaded </span><span><span>{</span><span>len</span><span>(</span>records<span>)</span><span>}</span></span><span> file records"</span></span><span>)</span><br><br> df <span>=</span> pd<span>.</span>DataFrame<span>(</span>records<span>)</span><br> <span># Создаём "long format" для timeline: каждая запись с одним временем и типом</span><br> timeline <span>=</span> <span>[</span><span>]</span><br> <span>for</span> _<span>,</span> row <span>in</span> df<span>.</span>iterrows<span>(</span><span>)</span><span>:</span><br> <span>for</span> ttype <span>in</span> <span>[</span><span>'create'</span><span>,</span> <span>'modify'</span><span>,</span> <span>'mft_change'</span><span>,</span> <span>'access'</span><span>]</span><span>:</span><br> ts <span>=</span> row<span>[</span>ttype<span>]</span><br> <span>if</span> pd<span>.</span>notnull<span>(</span>ts<span>)</span><span>:</span><br> timeline<span>.</span>append<span>(</span><span>{</span><br> <span>'timestamp'</span><span>:</span> ts<span>,</span><br> <span>'file'</span><span>:</span> row<span>[</span><span>'name'</span><span>]</span><span>,</span><br> <span>'event_type'</span><span>:</span> <span><span>f'$MFT_</span><span><span>{</span>ttype<span>}</span></span><span>'</span></span><br> <span>}</span><span>)</span><br> df_timeline <span>=</span> pd<span>.</span>DataFrame<span>(</span>timeline<span>)</span><br> df_timeline<span>.</span>set_index<span>(</span><span>'timestamp'</span><span>,</span> inplace<span>=</span><span>True</span><span>)</span><br> df_timeline<span>.</span>sort_index<span>(</span>inplace<span>=</span><span>True</span><span>)</span><br><br> <span>print</span><span>(</span><span>"\n=== Timeline summary ==="</span><span>)</span><br> <span>print</span><span>(</span>df_timeline<span>.</span>groupby<span>(</span><span>'event_type'</span><span>)</span><span>.</span>size<span>(</span><span>)</span><span>)</span><br><br> <span># Сохраняем</span><br> df_timeline<span>.</span>to_csv<span>(</span>args<span>.</span>output<span>)</span><br> <span>print</span><span>(</span><span><span>f"[+] Timeline saved to </span><span><span>{</span>args<span>.</span>output<span>}</span></span><span>"</span></span><span>)</span><br><br><span>if</span> __name__ <span>==</span> <span>'__main__'</span><span>:</span><br> main<span>(</span><span>)</span>

Пояснения:
  • Используем библиотеку python-ntfs, она даёт доступ к атрибутам каждой записи.
  • Извлекаем атрибут $FILE_NAME, где хранятся временные метки (в NTFS времени).
  • Конвертируем NTFS-время (число 100-нс интервалов с 1601-01-01) в datetime.
  • Создаём "длинный" формат: для каждого файла и каждого типа времени одна запись.
  • Получаем timeline изменений файловой системы.
Грабли:
  • Библиотека python-ntfs может быть нестабильна на больших MFT (сотни ГБ). Альтернативы: использовать pytsk3 (но там другой API) или читать MFT через analyzeMFT.py (сторонний инструмент) и потом парсить его CSV.
  • MFT может содержать удалённые файлы (они помечены флагом). По умолчанию мы их тоже видим, что полезно.
  • Время доступа (access_time) может быть отключено на системе, тогда оно не обновляется.
Задача: дан текстовый файл (email, отчёт антивируса, лог), нужно автоматически извлечь из него IP, домены, URL, хеши и вывести в структурированном виде (CSV/JSON).

Код:

Python:
<span>#!/usr/bin/env python3</span><br><span># ioc_extractor.py</span><br><br><span>import</span> argparse<br><span>import</span> re<br><span>import</span> json<br><span>import</span> pandas <span>as</span> pd<br><span>from</span> collections <span>import</span> defaultdict<br><br><span># Регулярки</span><br>ipv4_pattern <span>=</span> re<span>.</span><span>compile</span><span>(</span><span>r'\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b'</span><span>)</span><br>domain_pattern <span>=</span> re<span>.</span><span>compile</span><span>(</span><span>r'\b(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}\b'</span><span>)</span><br>url_pattern <span>=</span> re<span>.</span><span>compile</span><span>(</span><span>r'https?://[^\s&lt;&gt;"\'{}|\\^`\[\]]+'</span><span>)</span><br>md5_pattern <span>=</span> re<span>.</span><span>compile</span><span>(</span><span>r'\b[a-fA-F0-9]{32}\b'</span><span>)</span><br>sha1_pattern <span>=</span> re<span>.</span><span>compile</span><span>(</span><span>r'\b[a-fA-F0-9]{40}\b'</span><span>)</span><br>sha256_pattern <span>=</span> re<span>.</span><span>compile</span><span>(</span><span>r'\b[a-fA-F0-9]{64}\b'</span><span>)</span><br><br><span>def</span> <span>extract_iocs</span><span>(</span>text<span>)</span><span>:</span><br> iocs <span>=</span> defaultdict<span>(</span><span>list</span><span>)</span><br> iocs<span>[</span><span>'ipv4'</span><span>]</span> <span>=</span> <span>list</span><span>(</span><span>set</span><span>(</span>ipv4_pattern<span>.</span>findall<span>(</span>text<span>)</span><span>)</span><span>)</span><br> iocs<span>[</span><span>'domain'</span><span>]</span> <span>=</span> <span>list</span><span>(</span><span>set</span><span>(</span>domain_pattern<span>.</span>findall<span>(</span>text<span>)</span><span>)</span><span>)</span><br> iocs<span>[</span><span>'url'</span><span>]</span> <span>=</span> <span>list</span><span>(</span><span>set</span><span>(</span>url_pattern<span>.</span>findall<span>(</span>text<span>)</span><span>)</span><span>)</span><br> iocs<span>[</span><span>'md5'</span><span>]</span> <span>=</span> <span>list</span><span>(</span><span>set</span><span>(</span>md5_pattern<span>.</span>findall<span>(</span>text<span>)</span><span>)</span><span>)</span><br> iocs<span>[</span><span>'sha1'</span><span>]</span> <span>=</span> <span>list</span><span>(</span><span>set</span><span>(</span>sha1_pattern<span>.</span>findall<span>(</span>text<span>)</span><span>)</span><span>)</span><br> iocs<span>[</span><span>'sha256'</span><span>]</span> <span>=</span> <span>list</span><span>(</span><span>set</span><span>(</span>sha256_pattern<span>.</span>findall<span>(</span>text<span>)</span><span>)</span><span>)</span><br> <span># Убираем пересечения: если домен попал в url, не страшно, но можно очистить</span><br> <span>return</span> iocs<br><br><span>def</span> <span>main</span><span>(</span><span>)</span><span>:</span><br> parser <span>=</span> argparse<span>.</span>ArgumentParser<span>(</span>description<span>=</span><span>'Extract IOCs from text file'</span><span>)</span><br> parser<span>.</span>add_argument<span>(</span><span>'input'</span><span>,</span> <span>help</span><span>=</span><span>'Input text file'</span><span>)</span><br> parser<span>.</span>add_argument<span>(</span><span>'--output'</span><span>,</span> <span>'-o'</span><span>,</span> default<span>=</span><span>'iocs.json'</span><span>,</span> <span>help</span><span>=</span><span>'Output JSON file'</span><span>)</span><br> parser<span>.</span>add_argument<span>(</span><span>'--csv'</span><span>,</span> action<span>=</span><span>'store_true'</span><span>,</span> <span>help</span><span>=</span><span>'Output CSV instead of JSON'</span><span>)</span><br> args <span>=</span> parser<span>.</span>parse_args<span>(</span><span>)</span><br><br> <span>with</span> <span>open</span><span>(</span>args<span>.</span><span>input</span><span>,</span> <span>'r'</span><span>,</span> encoding<span>=</span><span>'utf-8'</span><span>,</span> errors<span>=</span><span>'ignore'</span><span>)</span> <span>as</span> f<span>:</span><br> text <span>=</span> f<span>.</span>read<span>(</span><span>)</span><br><br> iocs <span>=</span> extract_iocs<span>(</span>text<span>)</span><br><br> <span>if</span> args<span>.</span>csv<span>:</span><br> <span># превращаем в плоский список для CSV</span><br> rows <span>=</span> <span>[</span><span>]</span><br> <span>for</span> ioc_type<span>,</span> values <span>in</span> iocs<span>.</span>items<span>(</span><span>)</span><span>:</span><br> <span>for</span> val <span>in</span> values<span>:</span><br> rows<span>.</span>append<span>(</span><span>{</span><span>'type'</span><span>:</span> ioc_type<span>,</span> <span>'value'</span><span>:</span> val<span>}</span><span>)</span><br> df <span>=</span> pd<span>.</span>DataFrame<span>(</span>rows<span>)</span><br> df<span>.</span>to_csv<span>(</span>args<span>.</span>output<span>.</span>replace<span>(</span><span>'.json'</span><span>,</span> <span>'.csv'</span><span>)</span><span>,</span> index<span>=</span><span>False</span><span>)</span><br> <span>print</span><span>(</span><span><span>f"[+] CSV saved to </span><span><span>{</span>args<span>.</span>output<span>.</span>replace<span>(</span><span>'.json'</span><span>,</span> <span>'.csv'</span><span>)</span><span>}</span></span><span>"</span></span><span>)</span><br> <span>else</span><span>:</span><br> <span>with</span> <span>open</span><span>(</span>args<span>.</span>output<span>,</span> <span>'w'</span><span>)</span> <span>as</span> f<span>:</span><br> json<span>.</span>dump<span>(</span>iocs<span>,</span> f<span>,</span> indent<span>=</span><span>2</span><span>)</span><br> <span>print</span><span>(</span><span><span>f"[+] JSON saved to </span><span><span>{</span>args<span>.</span>output<span>}</span></span><span>"</span></span><span>)</span><br><br> <span># Статистика</span><br> <span>print</span><span>(</span><span>"\n=== Extracted IOCs ==="</span><span>)</span><br> <span>for</span> k<span>,</span> v <span>in</span> iocs<span>.</span>items<span>(</span><span>)</span><span>:</span><br> <span>print</span><span>(</span><span><span>f"</span><span><span>{</span>k<span>}</span></span><span>: </span><span><span>{</span><span>len</span><span>(</span>v<span>)</span><span>}</span></span><span>"</span></span><span>)</span><br><br><span>if</span> __name__ <span>==</span> <span>'__main__'</span><span>:</span><br> main<span>(</span><span>)</span>

Пояснения:
  • Используем несколько регулярных выражений для разных типов IOC.
  • Для каждого типа удаляем дубликаты через set.
  • Результат можно сохранить в JSON (структурированно по типам) или в CSV (плоская таблица).
  • Легко добавить другие типы (например, email, registry paths).
Грабли:
  • Домены могут пересекаться с URL. В CSV они будут отдельными записями, это нормально.
  • Могут быть ложные срабатывания (например, IPv4 в десятичной записи не ловится). Можно использовать библиотеку ipaddress для валидации после извлечения.
  • Для defanged IOC нужно предварительно refang.
Задача: объединить логи из трёх источников (файрвол, события аутентификации, логи EDR) и найти корреляции. Например: все IP, которые были заблокированы файрволом и одновременно делали неудачные попытки входа. Или все процессы, которые были запущены на хосте сразу после успешного входа с внешнего IP.

Подход: нормализуем каждый источник в DataFrame с общим индексом timestamp, добавляем колонку с IP (если есть), затем объединяем по временному окну.

Упрощённый пример (без полного парсинга, демонстрируем идею):

Python:
<span>#!/usr/bin/env python3</span><br><span># correlate.py</span><br><br><span>import</span> pandas <span>as</span> pd<br><span>import</span> argparse<br><br><span>def</span> <span>load_firewall_logs</span><span>(</span><span>file</span><span>)</span><span>:</span><br> <span># заглушка: читаем CSV с колонками timestamp, ip, action</span><br> df <span>=</span> pd<span>.</span>read_csv<span>(</span><span>file</span><span>,</span> parse_dates<span>=</span><span>[</span><span>'timestamp'</span><span>]</span><span>)</span><br> df<span>.</span>set_index<span>(</span><span>'timestamp'</span><span>,</span> inplace<span>=</span><span>True</span><span>)</span><br> df<span>[</span><span>'source'</span><span>]</span> <span>=</span> <span>'firewall'</span><br> <span>return</span> df<br><br><span>def</span> <span>load_auth_logs</span><span>(</span><span>file</span><span>)</span><span>:</span><br> df <span>=</span> pd<span>.</span>read_csv<span>(</span><span>file</span><span>,</span> parse_dates<span>=</span><span>[</span><span>'timestamp'</span><span>]</span><span>)</span><br> df<span>.</span>set_index<span>(</span><span>'timestamp'</span><span>,</span> inplace<span>=</span><span>True</span><span>)</span><br> df<span>[</span><span>'source'</span><span>]</span> <span>=</span> <span>'auth'</span><br> <span>return</span> df<br><br><span>def</span> <span>load_endpoint_logs</span><span>(</span><span>file</span><span>)</span><span>:</span><br> df <span>=</span> pd<span>.</span>read_csv<span>(</span><span>file</span><span>,</span> parse_dates<span>=</span><span>[</span><span>'timestamp'</span><span>]</span><span>)</span><br> df<span>.</span>set_index<span>(</span><span>'timestamp'</span><span>,</span> inplace<span>=</span><span>True</span><span>)</span><br> df<span>[</span><span>'source'</span><span>]</span> <span>=</span> <span>'endpoint'</span><br> <span>return</span> df<br><br><span>def</span> <span>main</span><span>(</span><span>)</span><span>:</span><br> parser <span>=</span> argparse<span>.</span>ArgumentParser<span>(</span><span>)</span><br> parser<span>.</span>add_argument<span>(</span><span>'--firewall'</span><span>,</span> required<span>=</span><span>True</span><span>)</span><br> parser<span>.</span>add_argument<span>(</span><span>'--auth'</span><span>,</span> required<span>=</span><span>True</span><span>)</span><br> parser<span>.</span>add_argument<span>(</span><span>'--endpoint'</span><span>,</span> required<span>=</span><span>True</span><span>)</span><br> parser<span>.</span>add_argument<span>(</span><span>'--window'</span><span>,</span> default<span>=</span><span>'5min'</span><span>,</span> <span>help</span><span>=</span><span>'Correlation window'</span><span>)</span><br> args <span>=</span> parser<span>.</span>parse_args<span>(</span><span>)</span><br><br> df_fw <span>=</span> load_firewall_logs<span>(</span>args<span>.</span>firewall<span>)</span><br> df_auth <span>=</span> load_auth_logs<span>(</span>args<span>.</span>auth<span>)</span><br> df_ep <span>=</span> load_endpoint_logs<span>(</span>args<span>.</span>endpoint<span>)</span><br><br> <span># объединяем все</span><br> df_all <span>=</span> pd<span>.</span>concat<span>(</span><span>[</span>df_fw<span>,</span> df_auth<span>,</span> df_ep<span>]</span><span>)</span><br> df_all<span>.</span>sort_index<span>(</span>inplace<span>=</span><span>True</span><span>)</span><br><br> <span># теперь ищем IP, которые есть в firewall (action=block) и в auth (failure)</span><br> <span># допустим, в firewall есть колонка ip, в auth тоже ip</span><br> <span># берём временное окно: события из разных источников, произошедшие в течение window</span><br> <span># Простой способ: сгруппировать по IP и проверить пересечение типов событий</span><br> <span># Но это не учитывает временную близость. Лучше использовать rolling window с merge_asof.</span><br><br> <span># Пример: найти все пары событий firewall (block) и auth (failure) с одного IP, произошедшие не позднее 5 минут друг от друга</span><br> fw_blocks <span>=</span> df_fw<span>[</span>df_fw<span>[</span><span>'action'</span><span>]</span> <span>==</span> <span>'block'</span><span>]</span><span>[</span><span>[</span><span>'ip'</span><span>]</span><span>]</span><span>.</span>reset_index<span>(</span><span>)</span><br> auth_fails <span>=</span> df_auth<span>[</span>df_auth<span>[</span><span>'status'</span><span>]</span> <span>==</span> <span>'failure'</span><span>]</span><span>[</span><span>[</span><span>'ip'</span><span>]</span><span>]</span><span>.</span>reset_index<span>(</span><span>)</span><br><br> <span># используем merge_asof для поиска ближайших по времени</span><br> <span># сначала сортируем</span><br> fw_blocks<span>.</span>sort_values<span>(</span><span>'timestamp'</span><span>,</span> inplace<span>=</span><span>True</span><span>)</span><br> auth_fails<span>.</span>sort_values<span>(</span><span>'timestamp'</span><span>,</span> inplace<span>=</span><span>True</span><span>)</span><br> <span># убираем дубликаты индексов</span><br> result <span>=</span> pd<span>.</span>merge_asof<span>(</span><br> fw_blocks<span>,</span> auth_fails<span>,</span><br> on<span>=</span><span>'timestamp'</span><span>,</span> by<span>=</span><span>'ip'</span><span>,</span><br> direction<span>=</span><span>'nearest'</span><span>,</span><br> tolerance<span>=</span>pd<span>.</span>Timedelta<span>(</span>args<span>.</span>window<span>)</span><br> <span>)</span><br> <span># result содержит строки из fw_blocks с добавленным timestamp из auth_fails, если нашли в пределах окна</span><br> correlated <span>=</span> result<span>.</span>dropna<span>(</span>subset<span>=</span><span>[</span><span>'timestamp_y'</span><span>]</span><span>)</span><br> <span>print</span><span>(</span><span><span>f"Found </span><span><span>{</span><span>len</span><span>(</span>correlated<span>)</span><span>}</span></span><span> correlations between firewall blocks and auth failures within </span><span><span>{</span>args<span>.</span>window<span>}</span></span><span>"</span></span><span>)</span><br> <span>print</span><span>(</span>correlated<span>[</span><span>[</span><span>'ip'</span><span>,</span> <span>'timestamp_x'</span><span>,</span> <span>'timestamp_y'</span><span>]</span><span>]</span><span>)</span><br><br><span>if</span> __name__ <span>==</span> <span>'__main__'</span><span>:</span><br> main<span>(</span><span>)</span>

Пояснения:
  • Загружаем три нормализованных DataFrame.
  • Используем merge_asof для поиска событий из разных источников, близких по времени (с одного IP).
  • Выводим корреляции.
Грабли:
  • merge_asof требует сортировки по времени.
  • Толерантность задаётся как pd.Timedelta.
  • Если событий много, может быть много совпадений. Нужно продумать логику: ищем все пары, или только если блокировка произошла после неудач? Направление можно задать параметром direction.
  • В реальных данных IP может быть NAT, тогда корреляция будет неточной.

После того как мы получили обогащённые данные, часто нужно отправить их в Elasticsearch для визуализации или дальнейшего анализа. Есть несколько способов:
  • Использовать официальный клиент elasticsearch-py.
  • Записать в CSV и загрузить через Logstash.
  • Использовать bulk API.
Пример отправки DataFrame в Elasticsearch индекс:

Python:
<span>from</span> elasticsearch <span>import</span> Elasticsearch<span>,</span> helpers<br><span>import</span> pandas <span>as</span> pd<br><br><span>def</span> <span>df_to_es</span><span>(</span>df<span>,</span> index_name<span>,</span> es_host<span>=</span><span>'localhost:9200'</span><span>)</span><span>:</span><br> es <span>=</span> Elasticsearch<span>(</span><span>[</span>es_host<span>]</span><span>)</span><br> actions <span>=</span> <span>[</span><span>]</span><br> <span>for</span> i<span>,</span> row <span>in</span> df<span>.</span>iterrows<span>(</span><span>)</span><span>:</span><br> doc <span>=</span> row<span>.</span>to_dict<span>(</span><span>)</span><br> doc<span>[</span><span>'_index'</span><span>]</span> <span>=</span> index_name<br> doc<span>[</span><span>'_id'</span><span>]</span> <span>=</span> <span><span>f"</span><span><span>{</span>i<span>}</span></span><span>"</span></span> <span># уникальный ID</span><br> actions<span>.</span>append<span>(</span>doc<span>)</span><br> helpers<span>.</span>bulk<span>(</span>es<span>,</span> actions<span>)</span>

Осторожно: если DataFrame большой, лучше отправлять чанками. Также нужно настроить mapping под типы данных (особенно timestamp).

Мы уже рассматривали базовый шаблон. Усложним: добавим графики в формате base64 и интерактивные таблицы (например, с помощью DataTables).

Генерация графика в base64:

Python:
<span>import</span> matplotlib<span>.</span>pyplot <span>as</span> plt<br><span>import</span> io<br><span>import</span> base64<br><br><span>def</span> <span>fig_to_base64</span><span>(</span>fig<span>)</span><span>:</span><br> buf <span>=</span> io<span>.</span>BytesIO<span>(</span><span>)</span><br> fig<span>.</span>savefig<span>(</span>buf<span>,</span> <span>format</span><span>=</span><span>'png'</span><span>)</span><br> buf<span>.</span>seek<span>(</span><span>0</span><span>)</span><br> <span>return</span> base64<span>.</span>b64encode<span>(</span>buf<span>.</span>read<span>(</span><span>)</span><span>)</span><span>.</span>decode<span>(</span><span>'utf-8'</span><span>)</span>

В шаблоне:

HTML:
<span><span><span>&lt;</span>img</span> <span>src</span><span><span>=</span><span>"</span>data:image/png;base64,{{ plot_base64 }}<span>"</span></span> <span>/&gt;</span></span>
Использование DataTables: подключим CSS/JS в шаблоне и добавим атрибуты к таблице.

Шаблон report_template.html:

HTML:
<span><span><span>&lt;</span>html</span><span>&gt;</span></span><br><span><span><span>&lt;</span>head</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>title</span><span>&gt;</span></span>DFIR Report<span><span><span>&lt;/</span>title</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>link</span> <span>rel</span><span><span>=</span><span>"</span>stylesheet<span>"</span></span> <span>type</span><span><span>=</span><span>"</span>text/css<span>"</span></span> <span>href</span><span><span>=</span><span>"</span>https://cdn.datatables.net/1.10.25/css/jquery.dataTables.css<span>"</span></span><span>&gt;</span></span><br> <span><span><span>&lt;</span>script</span> <span>src</span><span><span>=</span><span>"</span>https://code.jquery.com/jquery-3.6.0.min.js<span>"</span></span><span>&gt;</span></span><span></span><span><span><span>&lt;/</span>script</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>script</span> <span>src</span><span><span>=</span><span>"</span>https://cdn.datatables.net/1.10.25/js/jquery.dataTables.js<span>"</span></span><span>&gt;</span></span><span></span><span><span><span>&lt;/</span>script</span><span>&gt;</span></span><br><span><span><span>&lt;/</span>head</span><span>&gt;</span></span><br><span><span><span>&lt;</span>body</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>h1</span><span>&gt;</span></span>Incident Timeline Report<span><span><span>&lt;/</span>h1</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>h2</span><span>&gt;</span></span>Generated on {{ now }}<span><span><span>&lt;/</span>h2</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>img</span> <span>src</span><span><span>=</span><span>"</span>data:image/png;base64,{{ plot_base64 }}<span>"</span></span> <span>/&gt;</span></span><br> <span><span><span>&lt;</span>h2</span><span>&gt;</span></span>Events ({{ events|length }})<span><span><span>&lt;/</span>h2</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>table</span> <span>id</span><span><span>=</span><span>"</span>events<span>"</span></span> <span>class</span><span><span>=</span><span>"</span>display<span>"</span></span><span>&gt;</span></span><br> <span><span><span>&lt;</span>thead</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>tr</span><span>&gt;</span></span><span><span><span>&lt;</span>th</span><span>&gt;</span></span>Timestamp<span><span><span>&lt;/</span>th</span><span>&gt;</span></span><span><span><span>&lt;</span>th</span><span>&gt;</span></span>Source<span><span><span>&lt;/</span>th</span><span>&gt;</span></span><span><span><span>&lt;</span>th</span><span>&gt;</span></span>Type<span><span><span>&lt;/</span>th</span><span>&gt;</span></span><span><span><span>&lt;</span>th</span><span>&gt;</span></span>Details<span><span><span>&lt;/</span>th</span><span>&gt;</span></span><span><span><span>&lt;/</span>tr</span><span>&gt;</span></span><br> <span><span><span>&lt;/</span>thead</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>tbody</span><span>&gt;</span></span><br> {% for ev in events %}<br> <span><span><span>&lt;</span>tr</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>td</span><span>&gt;</span></span>{{ ev.timestamp }}<span><span><span>&lt;/</span>td</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>td</span><span>&gt;</span></span>{{ ev.source }}<span><span><span>&lt;/</span>td</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>td</span><span>&gt;</span></span>{{ ev.event_type }}<span><span><span>&lt;/</span>td</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>td</span><span>&gt;</span></span>{{ ev.details }}<span><span><span>&lt;/</span>td</span><span>&gt;</span></span><br> <span><span><span>&lt;/</span>tr</span><span>&gt;</span></span><br> {% endfor %}<br> <span><span><span>&lt;/</span>tbody</span><span>&gt;</span></span><br> <span><span><span>&lt;/</span>table</span><span>&gt;</span></span><br> <span><span><span>&lt;</span>script</span><span>&gt;</span></span><span><span><br> <span>$</span><span>(</span>document<span>)</span><span>.</span><span>ready</span><span>(</span> <span>function</span> <span>(</span><span>)</span> <span>{</span><br> <span>$</span><span>(</span><span>'#events'</span><span>)</span><span>.</span><span>DataTable</span><span>(</span><span>)</span><span>;</span><br> <span>}</span> <span>)</span><span>;</span><br> </span></span><span><span><span>&lt;/</span>script</span><span>&gt;</span></span><br><span><span><span>&lt;/</span>body</span><span>&gt;</span></span><br><span><span><span>&lt;/</span>html</span><span>&gt;</span></span>

В Python:

Python:
<span>from</span> jinja2 <span>import</span> Template<br><span>from</span> datetime <span>import</span> datetime<br><br><span>with</span> <span>open</span><span>(</span><span>'report_template.html'</span><span>)</span> <span>as</span> f<span>:</span><br> template <span>=</span> Template<span>(</span>f<span>.</span>read<span>(</span><span>)</span><span>)</span><br><br>plot_base64 <span>=</span> fig_to_base64<span>(</span>plt<span>.</span>gcf<span>(</span><span>)</span><span>)</span> <span># после построения графика</span><br>html <span>=</span> template<span>.</span>render<span>(</span><br> now<span>=</span>datetime<span>.</span>now<span>(</span><span>)</span><span>.</span>strftime<span>(</span><span>'%Y-%m-%d %H:%M:%S'</span><span>)</span><span>,</span><br> plot_base64<span>=</span>plot_base64<span>,</span><br> events<span>=</span>df_timeline<span>.</span>reset_index<span>(</span><span>)</span><span>.</span>to_dict<span>(</span>orient<span>=</span><span>'records'</span><span>)</span><br><span>)</span><br><span>with</span> <span>open</span><span>(</span><span>'final_report.html'</span><span>,</span> <span>'w'</span><span>)</span> <span>as</span> f<span>:</span><br> f<span>.</span>write<span>(</span>html<span>)</span>

Получается полноценный интерактивный отчёт.


Автоматизация - это не магия. Это просто молоток, которым можно забивать гвозди, а можно - себе по пальцам. Мы рассмотрели пять скриптов, которые покрывают 80% рутинных задач. Но если запустить их на сырых данных без понимания, что внутри, - результат будет таким же сырым. Поэтому прежде чем жать Enter, всегда задавай себе три вопроса:
  1. Что я ищу? (Если не знаешь - скрипт найдёт кучу мусора.)
  2. Откуда данные? (Может, часы в логах переведены неправильно, и весь timeline поедет.)
  3. Что я буду делать с результатом? (Просто сохранить в CSV и забыть? Или передать клиенту?)
В каждом скрипте мы срезали углы. Например, в парсере evtx мы не обрабатывали все возможные варианты структуры EventData - бывает, что данные лежат в Binary, а не в Data. Регулярка для IP не ловит адреса в десятичном виде (2130706433), хотя это тоже localhost. В MFT-парсере мы пропустили расширенные записи и файлы без имени. Это нормально для быстрого расследования, но если ты готовишь улики для суда - придётся дорабатывать.

Золотое правило: всегда оставляй в отчёте раздел «Ограничения метода». Это не стыдно, это профессионально. Судьям и заказчикам важна прозрачность.
Автоматизация должна облегчать жизнь, а не превращать её в ад поддержки кода. Я видел ребят, которые пишут мега-скрипты на 5000 строк, а через полгода сами в них не могут разобраться. Не будь как они.
  • Документируй. Хотя бы пару строк в начале каждого модуля: что делает, какие входные данные, пример вызова.
  • Версионируй. Git - наше всё. Даже если ты один, история изменений спасает, когда случайно удалил рабочую версию.
  • Тестируй на маленьких данных. Не лезь сразу с гигабайтным логом - сначала отрежь 100 строк и проверь, что регулярка не падает.
И ещё: не пытайся объять необъятное. Если скрипт начинает тормозить на 10 ГБ логов - возможно, пора не оптимизировать его до бесконечности, а просто распараллелить или использовать dask. А может, и вовсе признать, что для такого объёма нужен настоящий SIEM, а не твой ноутбук.

DFIR не стоит на месте. Вчера были логи Apache, сегодня - контейнеры, Kubernetes, облачные audit-логи (AWS CloudTrail, Azure AD). Форматы меняются, объёмы растут. Но подход остаётся: нормализация, обогащение, визуализация. Python с его экосистемой (Pandas, Dask, PySpark) позволяет масштабироваться.

Мой тебе совет: не замыкайся на одном инструменте. Скрипты - это классно, но иногда быстрее написать запрос в Elastic или использовать готовый плагин для Volatility. Держи под рукой арсенал и выбирай под задачу.
 
Независимый арбитраж: В случае возникновения споров или несоответствия товара описанию, сервис выступает в качестве арбитра, принимая объективное решение на основе заранее оговоренных условий.
Назад
Сверху