Skip to content

Repository files navigation

evtxview

Быстрый просмотр и триаж журналов Windows .evtx из командной строки — с проверкой полноты чтения.

CI Python Platform Tests Linting: Ruff Checked with mypy License

evtxview построен на rust-based парсере evtx и предназначен для быстрого DFIR-триажа: сводка по EventID, фильтры по времени/ID/подстроке, экспорт в CSV/JSON. Отличается от «просто просмотрщика» встроенной командой --verify, которая доказывает, что прочитаны все записи, а не молчаливая часть файла.


Зачем ещё один просмотрщик evtx

Инструмент вырос из разбора ransomware-инцидента, где популярный python-evtx молча прочитал только первый chunk из 30 — 94 записи из 2309 — и едва не увёл расследование в неверную сторону. Урок зашит в дизайн:

Число записей из парсера без ошибки ≠ полнота данных. Парсер может тихо отдать часть файла.

Поэтому --verify — не вспомогательная опция, а центральная функция. Прогоняйте её перед любым серьёзным анализом: она сверяет прочитанные записи с заголовками chunk'ов и ищет пропуски в EventRecordID, ловя потерю данных даже когда счётчик случайно совпал.

Возможности

  • Проверка полноты (--verify) — сверка с заголовками chunk'ов и поиск пропущенных EventRecordID. Различает недочитанный парсером файл (ОБРЕЗКА ЧТЕНИЯ) и записи, которых нет в самом файле (ПРОПУСКИ В ФАЙЛЕ) — выводы для расследования у этих случаев разные.
  • Сводка (--summary) — распределение EventID и диапазон времени; security-relevant EID подсвечены.
  • Единый таймлайн (--timeline) — события из нескольких .evtx сливаются в одну ленту, отсортированную по времени, с колонкой источника. Коррелирует Sysmon/Security/PowerShell в один поток.
  • Пресеты (--preset) — готовые представления под задачу. process-tree строит дерево процессов из Sysmon EID 1 (по ProcessGuidParentProcessGuid); logon-analysis разбирает входы Security (сессии 4624→4634, привилегированные логоны 4672, признаки brute-force по 4625); network группирует соединения Sysmon EID 3 по назначению, выводя необычные порты и исходящий с хоста трафик первыми; rdp сливает LocalSessionManager/RemoteConnectionManager/Security в единую хронологию RDP-активности.
  • Фильтры — по EventID (--eid), по подстроке в сыром XML (--grep), по времени (--after/--before; по умолчанию UTC, можно указать пояс суффиксом +03:00/Z или трактовать наивное время как локальное флагом --tz-filter).
  • Экспорт — CSV и JSON с метаполями (_EventID, _UTC, _Local, _Provider, _Computer, _SourceFile) и всеми полями события. В CSV значения, начинающиеся с =, +, -, @, экранируются: поля событий контролирует атакующий, а Excel исполнил бы такую ячейку как формулу.
  • Устойчивый разборElementTree с namespace/атрибутами/многострочными значениями и декодированием XML-сущностей; fallback на регулярки для битого XML. Работает с форматом UserData (PrintService и др.), не только EventData.
  • Несколько файлов сразу — маски (*.evtx) раскрываются, каждый файл в своей секции.
  • Кросс-платформенность — Windows и Linux, корректный UTF-8 вывод. Цвет — только в терминале и только если не задан NO_COLOR.
  • Настраиваемый конфиг (--config) — hot_eids, поля сводки и тюнинг пресетов [logon]/[network] заданы под Sysmon/Security по умолчанию, но переопределяются TOML-файлом.

Установка

Требуется Python 3.9+. Единственная runtime-зависимость — пакет evtx (rust-биндинг), ставится автоматически.

Вариант 1 — команда evtxview в PATH (рекомендуется)

pipx ставит инструмент в изолированное окружение и сам добавляет шорткат в PATH:

git clone https://github.com/kotru21/evtx-viewer
cd evtx-viewer
pipx install .

После этого команда доступна из любого каталога:

$ evtxview --help
$ evtxview Security.evtx --summary

Вариант 2 — обычный pip

git clone https://github.com/kotru21/evtx-viewer
cd evtx-viewer
pip install .

pip создаёт исполняемый файл evtxview в каталоге скриптов интерпретатора. Если после установки команда evtxview не находится (command not found), этот каталог не в PATH:

  • Windows: обычно %LOCALAPPDATA%\Programs\Python\PythonXX\Scripts (или ...\Scripts рядом с python.exe). Добавьте его в переменную среды PATH — путь пишется в предупреждении pip при установке.
  • Linux/macOS: обычно ~/.local/bin. Добавьте в ~/.bashrc/~/.zshrc: export PATH="$HOME/.local/bin:$PATH".

Универсальный запуск, не зависящий от PATH, доступен всегда:

python -m evtxview Security.evtx --summary

Для разработки

pip install -e ".[dev]"     # editable + pytest, ruff, mypy

Быстрый старт

1. Сначала — проверить, что файл прочитан целиком:

$ evtxview Sysmon.evtx --verify
Sysmon.evtx: chunks=30 заявлено=2309 прочитано=2309 OK

При потере записей:

$ evtxview suspect.evtx --verify
suspect.evtx: chunks=30 заявлено=2309 прочитано=2201 !!! ОБРЕЗКА ЧТЕНИЯ
  парсер вернул меньше записей, чем заявляют заголовки chunk'ов — часть файла не прочитана
  пропущено EventRecordID: 108  [11290, 11291, 11292, …(+105)]  (диапазон 11193..13501)

Если же счётчик сошёлся, а дыры есть, — это не обрезка чтения, а отсутствие записей в самом файле:

$ evtxview rotated.evtx --verify
rotated.evtx: chunks=30 заявлено=2309 прочитано=2309 !!! ПРОПУСКИ В ФАЙЛЕ
  парсер прочитал ровно столько, сколько заявлено, но в диапазоне есть дыры — записи
  отсутствуют в самом файле (ротация журнала или выборочное удаление)

2. Обзор — что вообще в журнале:

$ evtxview Sysmon.evtx --summary
  Всего: 2309 записей
  Диапазон (UTC): 2026-05-11T12:15:49  ..  2026-05-11T13:00:56
  EventID:
        11: 689 <-- security-relevant
         5: 636
         3: 518 <-- security-relevant
         1: 187 <-- security-relevant
         2: 145
        13: 121 <-- security-relevant

3. Углубиться — процессы, сеть, учётки:

$ evtxview Sysmon.evtx --eid 1 --limit 1
2026-05-11 15:20:42  EID     1  Image=C:\Windows\System32\sc.exe CommandLine=C:\Windows\system32\sc.exe start w32time User=NT AUTHORITY\LOCAL SERVICE ParentImage=C:\Windows\System32\services.exe
evtxview Sysmon.evtx --eid 3 --grep spoolsv          # сетевые соединения из спулера
evtxview Sysmon.evtx --eid 1 --grep "net user"       # создание учёток
evtxview Security.evtx --eid 1102 --full             # очистка журнала, все поля
evtxview Sysmon.evtx --eid 1 --csv processes.csv     # экспорт для таймлайна
evtxview *.evtx --verify                              # проверить все файлы на обрезку

4. Свести несколько журналов в единую ленту — коррелировать процессы, вход и сеть по времени:

$ evtxview Sysmon.evtx Security.evtx --timeline --after "2026-05-11 12:57:40" --before "2026-05-11 12:57:42"
2026-05-11 15:57:40  [Sysmon.evtx]    EID     1  Image=C:\Windows\System32\LogonUI.exe ParentImage=C:\Windows\System32\winlogon.exe
2026-05-11 15:57:40  [Security.evtx]  EID  4648  SubjectUserName=VM1-PC$ TargetUserName=vm1 IpAddress=10.8.0.2
2026-05-11 15:57:40  [Security.evtx]  EID  4624  SubjectUserName=VM1-PC$ TargetUserName=vm1 IpAddress=10.8.0.2 LogonType=10
2026-05-11 15:57:41  [Sysmon.evtx]    EID     3  Image=C:\Windows\System32\svchost.exe DestinationIp=10.10.10.20 DestinationPort=3389
2026-05-11 15:57:41  [Sysmon.evtx]    EID     5  Image=C:\Windows\System32\rdpclip.exe

5. Построить дерево процессов — цепочка «родитель → потомок» из Sysmon EID 1 сразу вскрывает активность атакующего:

$ evtxview Sysmon.evtx --preset process-tree
Дерево процессов (Sysmon EID 1): 187 процессов, 25 корней

15:25:10  cmd.exe (3188)  C:\Windows\system32\cmd.exe
├─ 15:25:28  net.exe (568)  net user /domain
├─ 15:26:03  net.exe (3740)  net user john 123123qwe /add
│  └─ 15:26:03  net1.exe (2852)  C:\Windows\system32\net1 user john 123123qwe /add
├─ 15:27:43  net.exe (992)  net localgroup Administrators john /add
└─ 15:27:58  net.exe (888)  net localgroup "Remote Desktop Users" john /add

6. Разобрать входы — сессии, привилегированные логоны и признаки brute-force из Security 4624/4625/4634/4672:

$ evtxview Security.evtx --preset logon-analysis
Анализ входов (Security): 2 успешных, 0 неуспешных

vm1-PC\vm1  (2 вход(ов))
    2026-05-11 15:57:40 -> 15:58:22  (0:42)  LogonType=RemoteInteractive(RDP)  IP=10.8.0.2  [privileged: 4672]
    2026-05-11 15:57:40 -> 15:57:41  (0:00)  LogonType=RemoteInteractive(RDP)  IP=10.8.0.2

Сессии строятся парой 4624→4634 по TargetLogonId (длительность считается из совпавшей пары); вход помечается [privileged: 4672], если по тому же LogonId было выдано специальное право. При наличии неудачных входов (4625) выводится их список и, если по одной учётке или IP набирается 5+ неудач за 5 минут, — предупреждение о вероятном brute-force.

7. Разобрать сетевую активность — соединения Sysmon EID 3, сгруппированные по назначению; необычные порты и исходящие с хоста соединения — первыми:

$ evtxview Sysmon.evtx --preset network
Сетевые соединения (Sysmon EID 3): 518 событий, 36 назначений

10.10.10.20:7070  (47 соедин., 15:17:07..15:18:57)  [необычный порт]
    процессы: AnyDesk.exe
10.10.10.1:22  (6 соедин., 16:00:48..16:00:52)  [необычный порт, исходящее с этого хоста (6)]
    процессы: nmap.exe
10.8.0.2:4444  (1 соедин., 15:24:56..15:24:56)  [необычный порт, исходящее с этого хоста (1)]
    процессы: spoolsv.exe
10.10.10.1:161  (1 соедин., 15:31:42..15:31:42)  [исходящее с этого хоста (1)]
    процессы: advanced_ip_scanner.exe

Группировка — по DestinationIp:Port. «Необычный порт» — не входит в общеизвестные (DNS/RPC/SMB/RDP/…) и не из динамического RPC-диапазона (49152+, обычные callback-порты после негоциации через порт 135 — не помечаются, чтобы не заваливать вывод шумом). «Исходящее с этого хоста» — Initiated=true, то есть соединение инициировал сам хост, а не принял: как раз так выглядит recon и удалённый доступ атакующего на фоне обычного фонового трафика ОС.

8. Собрать хронологию RDP — кто, откуда и когда входил, переподключался и выходил, из трёх источников сразу:

$ evtxview LocalSessionManager.evtx RemoteConnectionManager.evtx Security.evtx --preset rdp
RDP-активность: 23 событий

2026-05-11 15:13:08  аутентификация (1149)     vm1                  10.8.0.2
2026-05-11 15:13:09  логон                     vm1-PC\vm1           10.8.0.2
2026-05-11 15:22:29  аутентификация (1149)     wnTnksI              10.8.0.2
2026-05-11 15:28:28  аутентификация (1149)     john                 10.8.0.2
2026-05-11 15:28:29  логон                     vm1-PC\john          10.8.0.2
2026-05-11 15:28:29  запуск shell              vm1-PC\john          10.8.0.2
2026-05-11 15:28:49  отключение                vm1-PC\john          10.8.0.2
2026-05-11 15:28:58  переподключение           vm1-PC\john          10.8.0.2

Сливает три источника в единую хронологию: TerminalServices-LocalSessionManager (EID 21/22/23/24/25 — логон/shell/логофф/отключение/переподключение сессии), TerminalServices-RemoteConnectionManager (EID 1149 — успешная аутентификация; срабатывает раньше и надёжнее прочих, даже если сессия так и не была создана — как wnTnksI в примере выше, явно аномальное имя учётки без единого сопутствующего события логона), и Security (4624/4625 с LogonType=10 — независимая сверка по журналу входов, неудачные попытки подсвечиваются). Работает, только если все три файла переданы вместе; при наличии лишь части источников использует то, что есть.

Опции

Флаг Назначение
files... Один или несколько .evtx; поддерживаются маски (*.evtx)
--verify Проверка полноты чтения (chunk-заголовки + пропуски EventRecordID); различает обрезку чтения и пропуски в самом файле. Проверяет весь файл — несовместим с --eid/--grep/--after/--before/--tz-filter
--summary Сводка: распределение EventID и диапазон времени
--full Полный дамп всех полей каждого события
--timeline Единая лента из всех файлов, отсортированная по времени (колонка источника)
--preset process-tree Дерево процессов из Sysmon EID 1 (ProcessGuidParentProcessGuid)
--preset logon-analysis Сессии, привилегированные входы и brute-force из Security 4624/4625/4634/4672
--preset network Соединения Sysmon EID 3, сгруппированные по назначению; необычные порты и исходящие с хоста — первыми
--preset rdp Хронология RDP из LSM/RCM/Security: логоны, переподключения, аутентификация, неудачные попытки
--eid 1,3,1102 Фильтр по EventID (через запятую)
--grep СТРОКА Фильтр: подстрока в сыром XML (регистронезависимо)
--after "YYYY-MM-DD HH:MM" События не раньше указанного времени. По умолчанию UTC; можно указать пояс суффиксом ("...+03:00", "...Z")
--before "YYYY-MM-DD HH:MM" События не позже указанного времени. Та же трактовка, что у --after
--tz N Сдвиг локального времени в часах для вывода (по умолчанию +3)
--tz-filter --after/--before без явного пояса трактовать в зоне --tz, а не UTC
--csv FILE Экспорт отобранных событий в CSV. Несовместим с --summary/--preset (они не строят построчную выборку)
--json FILE Экспорт отобранных событий в JSON. Те же ограничения, что у --csv
--limit N Не более N событий в терминале. Без --csv/--json на dump останавливает чтение файла (chunk-errors только по прочитанной части). С экспортом и на --timeline — только печать; CSV/JSON содержат все отобранные события
--config FILE TOML-конфиг: hot_eids, поля сводки, [logon]/[network] (иначе: $EVTXVIEW_CONFIG, ~/.config/evtxview/config.toml, встроенные дефолты)

Проверка полноты

--verify разбирает заголовки chunk'ов напрямую (ElfChnk\x00, шаг 0x10000) и опирается на два независимых источника истины:

  • счётчик — номера записей (0x08/0x10) дают ожидаемое число записей;
  • диапазон — идентификаторы EventRecordID (0x18/0x20) дают ожидаемый непрерывный диапазон.

Прочитанные записи сверяются с обоими, и результат различается по смыслу:

Вердикт Что произошло
OK счётчик сошёлся, дыр нет
ОБРЕЗКА ЧТЕНИЯ прочитано меньше заявленного — потерял парсер
ПРОПУСКИ В ФАЙЛЕ прочитано ровно заявленное, но в диапазоне дыры — записей нет в самом файле (ротация или выборочное удаление)
ЗАГОЛОВКИ ЗАНИЖЕНЫ прочитано больше заявленного — заголовки устарели или повреждены
ПОЛНОТА НЕПРОВЕРЯЕМА счётчики читаемых заголовков сошлись, но часть заголовков chunk'ов нечитаема — сколько записей было в них, неизвестно

Пустые предвыделенные chunk'и (sentinel-заголовок 0xFFFF…FF) и нулевой хвост файла не считаются потерей. Битые chunk'и не роняют разбор — ошибки парсера выводятся как chunk-errors: N, а повреждённые заголовки chunk'ов — как битых заголовков chunk'ов: N.

Скан заголовков не останавливается на первом повреждённом chunk'е, а проходит файл до конца. Это принципиально: раньше проверка магии стояла в условии цикла, и повреждение в середине файла обрывало обход — заявленное число записей считалось только по chunk'ам до повреждения. Эталон, с которым сверяется полнота, сам оказывался обрезан, и если парсер записей спотыкался на тех же chunk'ах, прочитано и заявлено занижались согласованно и совпадали — --verify выдавал ложный OK ровно на том классе файлов, ради которого он существует. Теперь наличие хотя бы одного битого заголовка переводит вердикт в ПОЛНОТА НЕПРОВЕРЯЕМА даже при совпадении счётчиков.

Если сам заголовок chunk'а повреждён и объявляет неправдоподобно широкий диапазон ID (на порядки больше, чем бывает в реальных файлах), --verify не пытается перечислить пропуски в таком диапазоне — это дало бы либо ложный OK, либо попытку выделить память под нереальное число элементов. Вместо этого выводится явное предупреждение о повреждённом заголовке.

--verify всегда проверяет файл целиком: заголовки chunk'ов описывают весь файл, а не отфильтрованную выборку, поэтому проверка полноты в принципе не может быть ограничена по --eid/--grep/--after/--before. Комбинация с любым из этих флагов (в том числе --tz-filter) отклоняется явной ошибкой — раньше фильтры в этом случае молча игнорировались, и --verify тихо проверял весь файл, хотя в выводе печаталась информация о применённом фильтре времени.

Разбор полей

Каждая запись парсится один раз (parse_record) в модель EventRecord. Основной путь — ElementTree: корректно обрабатывает namespace, атрибуты, многострочные значения и декодирует XML-сущности (&lt;<). На нестандартно оформленном или битом XML происходит откат на разбор регулярными выражениями, чтобы не потерять запись целиком.

Конфигурация

По умолчанию инструмент подсвечивает как security-relevant набор EventID, характерный для Sysmon/Security, и выбирает те же провайдер-специфичные поля для однострочной сводки (Image, CommandLine, IpAddress, ...). Через TOML также тюнятся пресеты logon-analysis / network ([logon], [network]). Полезно, если вы работаете преимущественно с другими провайдерами (Exchange, IIS, кастомные приложения) и хотите подсветку под них.

# ~/.config/evtxview/config.toml (или любой путь через --config / $EVTXVIEW_CONFIG)

[highlight]
hot_eids = ["1102", "4624", "4625", "4672"]

[summary]
fields = ["Image", "CommandLine", "TargetUserName"]

[logon]
logon_type_names = { "10" = "RDP" }
brute_force_threshold = 5
brute_force_window = "5m"

[network]
common_ports = ["53", "80", "443", "445", "3389"]

Порядок поиска: --config PATH$EVTXVIEW_CONFIG~/.config/evtxview/config.toml → встроенные дефолты. hot_eids, summary.fields, common_ports, brute_force_threshold и brute_force_window при наличии ключа полностью заменяют соответствующий дефолт; logon_type_names мержится поверх встроенной таблицы (перечисленные ключи добавляют/переопределяют, остальные дефолты сохраняются). Если файл указан явно (флагом или переменной окружения) и не найден или содержит битый TOML — программа завершается с понятной ошибкой, а не молча откатывается на дефолты; путь по умолчанию (~/.config/...), если его нет, просто пропускается.

Кросс-валидация

На критичных артефактах полезно сверять результат с независимым инструментом — EvtxECmd (Eric Zimmerman) или нативным wevtutil. Расхождение в числе записей сразу видно и указывает на проблему в одном из парсеров.

Ограничения и планы

  • Все четыре запланированных пресета готовы: process-tree, logon-analysis, network, rdp.
  • Через TOML конфигурируются hot_eids, поля сводки, а также тюнинг пресетов: logon_type_names (merge), brute_force_threshold / brute_force_window, common_ports (replace). Наборы EID/провайдеров внутри пресетов и RDP-метки пока захардкожены.
  • Чтение: dump и --summary идут потоком (XML не копится в список); --limit без --csv/--json останавливает чтение после N напечатанных событий. --timeline / --preset по-прежнему материализуют отобранные EventRecord в память; буфер строк экспорта (--csv/--json) тоже растёт O(n). --verify по-прежнему лёгкий по телу файла: собирает только EventRecordID при обходе + сверка с заголовками chunk'ов.
  • Breaking API: read_records(path) больше не возвращает (list, errs), а RecordStream — итерируйте XML и смотрите stream.chunk_errors после (полного или частичного) обхода. Повторный for по тому же stream — новый проход парсера со сбросом счётчика.
from evtxview import read_records

stream = read_records("Security.evtx")
for xml in stream:
    ...
n_errs = stream.chunk_errors

Разработка

pip install -e ".[dev]"
pytest            # тесты (юниты разбора + интеграция на реальных .evtx фикстурах)
ruff check .      # линт
mypy src/evtxview # типы

CI (GitHub Actions) прогоняет ruff, mypy и pytest на Windows и Linux (Python 3.9 и 3.12).

Структура

Модуль Ответственность
reader.py Потоковое чтение .evtx (RecordStream) и проверка полноты по заголовкам chunk'ов
record.py Модель EventRecord и разбор полей (ElementTree + regex-fallback)
render.py Форматирование вывода: цвета, сводка, однострочный/полный дамп
presets.py Пресеты анализа (process-tree, logon-analysis, network, rdp)
export.py Экспорт в CSV/JSON
config.py Дефолты hot_eids/полей сводки/[logon]/[network] + загрузка и мерж пользовательского TOML
util.py Кодировка вывода, работа со временем
cli.py Разбор аргументов и оркестрация

Лицензия

MIT — см. LICENSE.

About

Быстрый просмотр и триаж журналов Windows .evtx из командной строки — с проверкой полноты чтения.

Resources

Stars

1 star

Watchers

0 watching

Forks

Used by

Contributors

Languages