Быстрый просмотр и триаж журналов Windows .evtx из командной строки — с проверкой полноты чтения.
evtxview построен на rust-based парсере evtx и предназначен для быстрого DFIR-триажа: сводка по EventID, фильтры по времени/ID/подстроке, экспорт в CSV/JSON. Отличается от «просто просмотрщика» встроенной командой --verify, которая доказывает, что прочитаны все записи, а не молчаливая часть файла.
Инструмент вырос из разбора 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 (поProcessGuid→ParentProcessGuid);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-биндинг), ставится автоматически.
pipx ставит инструмент в изолированное окружение и сам добавляет шорткат в PATH:
git clone https://github.com/kotru21/evtx-viewer
cd evtx-viewer
pipx install .После этого команда доступна из любого каталога:
$ evtxview --help
$ evtxview Security.evtx --summarygit 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 --summarypip install -e ".[dev]" # editable + pytest, ruff, mypy1. Сначала — проверить, что файл прочитан целиком:
$ 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-relevant3. Углубиться — процессы, сеть, учётки:
$ 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.exeevtxview 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.exe5. Построить дерево процессов — цепочка «родитель → потомок» из 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 /add6. Разобрать входы — сессии, привилегированные логоны и признаки 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 (ProcessGuid→ParentProcessGuid) |
--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-сущности (< → <). На нестандартно оформленном или битом 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_errorspip 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.