Привет, Хабр! На связи команда uFactor. В этой статье рассмотрим полезный способ поиска ВПО, который может быть дополнением к анализу компьютерных систем при расследовании инцидентов информационной безопасности. Увы, этот способ не всегда эффективен: следы активности Windows Defender, рассматриваемой далее, имеют свойство полностью удаляться — именно поэтому способ может использоваться только в качестве дополнения к основному методу исследования.

Многие используют в своих расследованиях анализ журналов событий Event ID 1116, Event ID 1117 Windows Defender, а также записи и файлы из карантина. Мы же поговорим о событиях сканирования файлов Windows Defender, которые пользователь скачивает, либо которые по иной причине появились в компьютерной системе.

Артефакты, указывающие на сканирование Windows Defender, хранятся в виде файлов, например: .\ProgramData\Microsoft\WindowsDefender\Scans\History\Results\Resource\{8C9221F8-4026-473E-A0B6-3D6981F102F0}. В журнале Microsoft-Windows-WindowsDefender вы не увидите событий сканирования, если файл не получил статус вредоносного ПО. Имена файлов имеют вид UUID версии 4 согласно стандарту RFC 4122. Эти имена не несут в себе полезной информации, в отличие, например, от имен UUID версии 1 (анализирующихся в нашей статье «Компьютерная криминалистика. Временные метки и туннелирование NTFS»). Содержимое файлов зашифровано алгоритмом RC4 со следующим ключом:

0x1E,0x87,0x78,0x1B,0x8D,0xBA,0xA8,0x44,0xCE,0x69,0x70,0x2C,0x0C, 0x78,0xB7,0x86,0xA3,0xF6,0x23,0xB7,0x38,0xF5,0xED,0xF9,0xAF,0x83, 0x53,0x0F,0xB3,0xFC,0x54,0xFA,0xA2,0x1E,0xB9,0xCF,0x13,0x31,0xFD, 0x0F,0x0D,0xA9,0x54,0xF6,0x87,0xCB,0x9E,0x18,0x27,0x96,0x97,0x90, 0x0E,0x53,0xFB,0x31,0x7C,0x9C,0xBC,0xE4,0x8E,0x23,0xD0,0x53,0x71, 0xEC,0xC1,0x59,0x51,0xB8,0xF3,0x64,0x9D,0x7C,0xA3,0x3E,0xD6,0x8D, 0xC9,0x04,0x7E,0x82,0xC9,0xBA,0xAD,0x97,0x99,0xD0,0xD4,0x58,0xCB, 0x84,0x7C,0xA9,0xFF,0xBE,0x3C,0x8A,0x77,0x52,0x33,0x55,0x7D,0xDE, 0x13,0xA8,0xB1,0x40,0x87,0xCC,0x1B,0xC8,0xF1,0x0F,0x6E,0xCD,0xD0, 0x83,0xA9,0x59,0xCF,0xF8,0x4A,0x9D,0x1D,0x50,0x75,0x5E,0x3E,0x19, 0x18,0x18,0xAF,0x23,0xE2,0x29,0x35,0x58,0x76,0x6D,0x2C,0x07,0xE2, 0x57,0x12,0xB2,0xCA,0x0B,0x53,0x5E,0xD8,0xF6,0xC5,0x6C,0xE7,0x3D, 0x24,0xBD,0xD0,0x29,0x17,0x71,0x86,0x1A,0x54,0xB4,0xC2,0x85,0xA9, 0xA3,0xDB,0x7A,0xCA,0x6D,0x22,0x4A,0xEA,0xCD,0x62,0x1D,0xB9,0xF2, 0xA2,0x2E,0xD1,0xE9,0xE1,0x1D,0x75,0xBE,0xD7,0xDC,0x0E,0xCB,0x0A, 0x8E,0x68,0xA2,0xFF,0x12,0x63,0x40,0x8D,0xC8,0x08,0xDF,0xFD,0x16, 0x4B,0x11,0x67,0x74,0xCD,0x0B,0x9B,0x8D,0x05,0x41,0x1E,0xD6,0x26, 0x2E,0x42,0x9B,0xA4,0x95,0x67,0x6B,0x83,0x98,0xDB,0x2F,0x35,0xD3, 0xC1,0xB9,0xCE,0xD5,0x26,0x36,0xF2,0x76,0x5E,0x1A,0x95,0xCB,0x7C, 0xA4,0xC3,0xDD,0xAB,0xDD,0xBF,0xF3,0x82,0x53

Это известная история, но давайте напомним, как расшифровывать такие файлы. Файл содержит информацию, которая может оказаться полезной, и делится на три блока. Расшифровка, как и шифрование, происходит по блокам. Сначала расшифровывается первый блок, из него процесс получает длины (размеры) для двух других блоков. Затем расшифровываются второй и третий блоки.

На рисунке 1 представлен фрагмент файла {5FB5EA10-7DFB-48BC-B1AA-4E7954F5F25F}, данные в нем зашифрованы. Красным цветом выделен первый блок. Его размер всегда 60 байт, первые 48 байт (D3–0F) и последние 4 байта (3E10D35E) всегда имеют одно и то же значение.

Рисунок 1. Фрагмент файла «{5FB5EA10-7DFB-48BC-B1AA-4E7954F5F25F}»
Рисунок 1. Фрагмент файла «{5FB5EA10-7DFB-48BC-B1AA-4E7954F5F25F}»

Давайте расшифруем первый блок. Для этого воспользуемся, например, ресурсом DevTools.

Рисунок 2. Расшифровка первого блока при помощи DevTools
Рисунок 2. Расшифровка первого блока при помощи DevTools

Зеленым цветом выделен первый блок, который мы расшифровываем, черным — фрагмент ключа 1E87781B8DBAA844CE69702C0C78B786A3F623B738F5EDF9AF83530FB3FC54FAA21EB9CF1331FD0F0DA954F687CB9E18279697900E53FB317C9CBCE48E23D05371ECC15951B8F3649D7CA33ED68DC9047E82C9BAAD9799D0D458CB847CA9FFBE3C8A775233557DDE13A8B14087CC1BC8F10F6ECDD083A959CFF84A9D1D50755E3E191818AF23E2293558766D2C07E25712B2CA0B535ED8F6C56CE73D24BDD0291771861A54B4C285A9A3DB7ACA6D224AEACD621DB9F2A22ED1E9E11D75BED7DC0ECB0A8E68A2FF1263408DC808DFFD164B116774CD0B9B8D05411ED6262E429BA495676B8398DB2F35D3C1B9CED52636F2765E1A95CB7CA4C3DDABDDBFF38253 для расшифровки. Красным цветом обозначен результат расшифровки. Давайте рассмотрим его в hex-редакторе.

Рисунок 3. Расшифрованный первый блок
Рисунок 3. Расшифрованный первый блок

Здесь самое важное — это размеры последующих двух блоков. Для каждого блока выделяется DWORD для указания размера (см. рисунок 3):

  • Зеленый цвет — размер второго блока, на практике встречается всегда 30 hex (48 dec), читается little-endian, то есть 0х00000030. Разумеется, начало второго блока — с 61-го байта.

  • Красный цвет — размер третьего блока, 2D5A hex (11610 dec), читается little-endian, то есть 0х00002D5A. Начало третьего блока — со 109-го байта (61 + 48).

Возвращаемся к файлу {5FB5EA10-7DFB-48BC-B1AA-4E7954F5F25F}`: на рисунке 4 выделен второй блок, копируем его байты и расшифровываем с помощью DevTools.

Рисунок 4. Второй блок данных зашифрованного файла «{5FB5EA10-7DFB-48BC-B1AA-4E7954F5F25F}»
Рисунок 4. Второй блок данных зашифрованного файла «{5FB5EA10-7DFB-48BC-B1AA-4E7954F5F25F}»
Рисунок 5. Расшифровка второго блока при помощи DevTools
Рисунок 5. Расшифровка второго блока при помощи DevTools

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

На рисунке 6 зеленым цветом выделен UUID файла {5FB5EA10-7DFB-48BC-B1AA-4E7954F5F25F}:

  • 10EAB55F5FB5EA10 (little-endian);

  • FB7D7DFB (little-endian);

  • BC4848BC (little-endian);

  • B1AA — сохраняется последовательность;

  • 4E7954F5F25F — сохраняется последовательность.

Черным цветом выделена временная метка в формате Windows FILETIME: 396BD3B50B53DC0101DC530BB5D36B39(2025-11-1113:04:27.454956). По всей видимости, это время создания файла и окончания сканирования. Оно нам никак не поможет в дальнейшем.

Рисунок 6. Расшифрованный второй блок
Рисунок 6. Расшифрованный второй блок

Теперь рассмотрим заключительный блок, его фрагмент представлен на рисунке 7.

Рисунок 7. Фрагмент третьего блока данных в файле «{5FB5EA10-7DFB-48BC-B1AA-4E7954F5F25F}»
Рисунок 7. Фрагмент третьего блока данных в файле «{5FB5EA10-7DFB-48BC-B1AA-4E7954F5F25F}»

Копируем байты и расшифровываем.

Рисунок 8. Расшифровка третьего блока
Рисунок 8. Расшифровка третьего блока

Думаю, рисунок 8 комментировать не нужно. Далее открываем полученный результат в hex-редакторе.

Рисунок 9. Фрагмент расшифрованных данных
Рисунок 9. Фрагмент расшифрованных данных

На рисунке 9 зеленым выделены временные метки в формате Windows FILETIME, остальная часть — какие-то паттерны. Стоит отметить: если Defender определил, что файл является ВПО, то в этой части будут записаны сведения — атрибуция и путь к файлу.

По сути, для нас нет ничего полезного в этих файлах, и в нашем дальнейшем исследовании их содержимое никак не поможет. На самом деле полезная информация, в том числе путь к файлу, к которому применяется сканирование, хранится в другом файле:.\ProgramData\Microsoft\WindowsDefender\Scans\History\Store<filename. Файл также зашифрован, но имеет два блока. Первый блок подобен рассмотренному нами выше, но содержит только длину второго блока. Обратимся к файлу с именем C21B59DA7D4B3B5BD570A20BBB540353.

Рисунок 10. Расшифрованный файл «C21B59DA7D4B3B5BD570A20BBB540353»
Рисунок 10. Расшифрованный файл «C21B59DA7D4B3B5BD570A20BBB540353»

Зеленым цветом выделен расшифрованный первый блок. Желтым — указатель на размер второго блока, читаем наоборот: 0х04AB = 1195 dec. Далее идет второй блок — как видите, он начинается с размера самого блока (обозначен черным).

На рисунке 11 представлен пример. Файл …rypt.exe был извлечен из архива, …rypt.exe:Zone.Identifier указывает на то, что архив был скачан из интернета через браузер и WinRAR скопировал из архива метку Zone.Identifier для потенциально небезопасного файла. Файл readme.txtтакже извлечен из архива, но не получил Zone.Identifier, так как WinRAR счел текстовый файл безопасным. В конце видим файлы, связанные со сканированием.

Рисунок 11. Таймлайн событий $MFT, связанных со сканированием файла «…rypt.exe»
Рисунок 11. Таймлайн событий $MFT, связанных со сканированием файла «…rypt.exe»

Связка …Scans\History\Store и …Scans\History\Results\Resource встречается очень редко, поэтому мы не заостряем на этом внимания. Однако имейте в виду: если вы видите в MFT такую связку, а предшествующие файлы ничем не примечательны, значит, скорее всего, файл был удален. В таком случае нужно расшифровать файл из …Scans\History\Store и посмотреть информацию там.

Ниже представлен скрипт для расшифровки файлов: на входе — папка с зашифрованными файлами, на выходе — папка для расшифрованных.

Скрипт для расшифровки файлов
#!/usr/bin/env python3

"""

Windows Defender Quarantine Decryptor - FINAL VERSION

"""

 

import os

import struct

from pathlib import Path

from Crypto.Cipher import ARC4

 

DEFENDER_RC4_KEY = bytes([

    0x1E, 0x87, 0x78, 0x1B, 0x8D, 0xBA, 0xA8, 0x44, 0xCE, 0x69, 0x70, 0x2C, 0x0C,

    0x78, 0xB7, 0x86, 0xA3, 0xF6, 0x23, 0xB7, 0x38, 0xF5, 0xED, 0xF9, 0xAF, 0x83,

    0x53, 0x0F, 0xB3, 0xFC, 0x54, 0xFA, 0xA2, 0x1E, 0xB9, 0xCF, 0x13, 0x31, 0xFD,

    0x0F, 0x0D, 0xA9, 0x54, 0xF6, 0x87, 0xCB, 0x9E, 0x18, 0x27, 0x96, 0x97, 0x90,

    0x0E, 0x53, 0xFB, 0x31, 0x7C, 0x9C, 0xBC, 0xE4, 0x8E, 0x23, 0xD0, 0x53, 0x71,

    0xEC, 0xC1, 0x59, 0x51, 0xB8, 0xF3, 0x64, 0x9D, 0x7C, 0xA3, 0x3E, 0xD6, 0x8D,

    0xC9, 0x04, 0x7E, 0x82, 0xC9, 0xBA, 0xAD, 0x97, 0x99, 0xD0, 0xD4, 0x58, 0xCB,

    0x84, 0x7C, 0xA9, 0xFF, 0xBE, 0x3C, 0x8A, 0x77, 0x52, 0x33, 0x55, 0x7D, 0xDE,

    0x13, 0xA8, 0xB1, 0x40, 0x87, 0xCC, 0x1B, 0xC8, 0xF1, 0x0F, 0x6E, 0xCD, 0xD0,

    0x83, 0xA9, 0x59, 0xCF, 0xF8, 0x4A, 0x9D, 0x1D, 0x50, 0x75, 0x5E, 0x3E, 0x19,

    0x18, 0x18, 0xAF, 0x23, 0xE2, 0x29, 0x35, 0x58, 0x76, 0x6D, 0x2C, 0x07, 0xE2,

    0x57, 0x12, 0xB2, 0xCA, 0x0B, 0x53, 0x5E, 0xD8, 0xF6, 0xC5, 0x6C, 0xE7, 0x3D,

    0x24, 0xBD, 0xD0, 0x29, 0x17, 0x71, 0x86, 0x1A, 0x54, 0xB4, 0xC2, 0x85, 0xA9,

    0xA3, 0xDB, 0x7A, 0xCA, 0x6D, 0x22, 0x4A, 0xEA, 0xCD, 0x62, 0x1D, 0xB9, 0xF2,

    0xA2, 0x2E, 0xD1, 0xE9, 0xE1, 0x1D, 0x75, 0xBE, 0xD7, 0xDC, 0x0E, 0xCB, 0x0A,

    0x8E, 0x68, 0xA2, 0xFF, 0x12, 0x63, 0x40, 0x8D, 0xC8, 0x08, 0xDF, 0xFD, 0x16,

    0x4B, 0x11, 0x67, 0x74, 0xCD, 0x0B, 0x9B, 0x8D, 0x05, 0x41, 0x1E, 0xD6, 0x26,

    0x2E, 0x42, 0x9B, 0xA4, 0x95, 0x67, 0x6B, 0x83, 0x98, 0xDB, 0x2F, 0x35, 0xD3,

    0xC1, 0xB9, 0xCE, 0xD5, 0x26, 0x36, 0xF2, 0x76, 0x5E, 0x1A, 0x95, 0xCB, 0x7C,

    0xA4, 0xC3, 0xDD, 0xAB, 0xDD, 0xBF, 0xF3, 0x82, 0x53

])

 

def rc4_decrypt(data):

    cipher = ARC4.new(DEFENDER_RC4_KEY)

    return cipher.decrypt(data)

 

def decrypt_quarantine_file(input_path, output_path):

    try:

        print(f"🔓 Обработка: {input_path.name}")

        

        # 1. ОТКРЫВАЕМ ФАЙЛ

        with open(input_path, 'rb') as f:

            all_data = f.read()

        

        file_size = len(all_data)

        print(f"   📊 Размер файла: {file_size} байт")

        

        if file_size < 60:

            print(f"   ❌ Файл слишком мал")

            return False

        

        # 2. РАСШИФРОВЫВАЕМ БЛОК 0 (60 байт)

        block0_encrypted = all_data[:60]

        block0_decrypted = rc4_decrypt(block0_encrypted)

        

        print(f"   🔑 Блок 0 расшифрован: {len(block0_decrypted)} байт")

        

        # 3. ПОЛУЧАЕМ ДЛИНЫ БЛОКОВ ИЗ ЗАГОЛОВКА

        # Байты 40-43: длина блока 1

        # Байты 44-47: длина блока 2

        if len(block0_decrypted) < 48:

            print(f"   ❌ Заголовок слишком мал")

            return False

        

        # Байты 40-43 (0x28-0x2B): длина блока 1

        block1_size = struct.unpack('<I', block0_decrypted[40:44])[0]

        # Байты 44-47 (0x2C-0x2F): длина блока 2  

        block2_size = struct.unpack('<I', block0_decrypted[44:48])[0]

        

        print(f"   📁 Длина блока 1: {block1_size} байт")

        print(f"   📁 Длина блока 2: {block2_size} байт")

        

        # 4. ПРОВЕРЯЕМ РАЗМЕРЫ

        total_needed = 60 + block1_size + block2_size

        if file_size < total_needed:

            print(f"   ❌ Файл маловат: {file_size} < {total_needed}")

            return False

        

        # 5. РАСШИФРОВЫВАЕМ БЛОК 1 (начинается с байта 61)

        block1_start = 60

        block1_end = block1_start + block1_size

        block1_encrypted = all_data[block1_start:block1_end]

        block1_decrypted = rc4_decrypt(block1_encrypted)

        print(f"   🔑 Блок 1 расшифрован: {len(block1_decrypted)} байт")

        

        # 6. РАСШИФРОВЫВАЕМ БЛОК 2 (начинается после блока 1)

        block2_start = block1_end

        block2_end = block2_start + block2_size

        block2_encrypted = all_data[block2_start:block2_end]

        block2_decrypted = rc4_decrypt(block2_encrypted)

        print(f"   🔑 Блок 2 расшифрован: {len(block2_decrypted)} байт")

        

        # 7. ОБЪЕДИНЯЕМ ВСЕ 3 БЛОКА И ЗАПИСЫВАЕМ (ИЗМЕНЕНИЕ ТУТ)

        final_data = block0_decrypted + block1_decrypted + block2_decrypted

        with open(output_path, 'wb') as f:

            f.write(final_data)

        

        print(f"   ✅ Записан: {len(final_data)} байт (все 3 блока)")

        return True

        

    except Exception as e:

        print(f"   ❌ Ошибка: {e}")

        return False

 

def main():

    print("🔒 Windows Defender Quarantine Decryptor - FINAL")

    print("=" * 50)

    

    try:

        from Crypto.Cipher import ARC4

    except ImportError:

        print("❌ pip install pycryptodome")

        return

    

    input_dir = input("Путь к зашифрованным файлам: ").strip()

    output_dir = input("Путь для сохранения: ").strip()

    

    if not input_dir or not output_dir:

        print("❌ Укажи оба пути!")

        return

    

    input_path = Path(input_dir)

    output_path = Path(output_dir)

    output_path.mkdir(parents=True, exist_ok=True)

    

    files = [f for f in input_path.iterdir() if f.is_file()]

    

    if not files:

        print("❌ Файлов нет!")

        return

    

    print(f"\n📁 Найдено файлов: {len(files)}")

    print("⏳ Начинаю расшифровку...\n")

    

    success = 0

    for file_path in files:

        output_file = output_path / file_path.name

        if decrypt_quarantine_file(file_path, output_file):

            success += 1

        print()

    

    print(f"✅ Готово! Обработано: {success}/{len(files)}")

 

if name == "__main__":

    main()

Давайте теперь вернемся к основной теме. С файлами, хранящимися в папке …Scans\History\Results\Resource, также есть проблемы: они хранятся в ней не все время и со временем полностью удаляются. Но можно увидеть большое количество файлов, помеченных в $MFT как неиспользуемые, — разумеется, с сохраненными атрибутами.

Рисунок 12. Удаленные файлы сканирования (неиспользуемые)
Рисунок 12. Удаленные файлы сканирования (неиспользуемые)

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

Теперь рассмотрим, как выглядит таймлайн в $MFT при реальной компрометации компьютерной системы. На рисунках 13, 14 и 15 представлены фрагменты из $MFT. Выделенные файлы были загружены злоумышленником при помощи ВПО.

Рисунок 13. Фрагменты из $MFT
Рисунок 13. Фрагменты из $MFT
Рисунок 14. Фрагменты из $MFT
Рисунок 14. Фрагменты из $MFT
Рисунок 15. Фрагменты из $MFT
Рисунок 15. Фрагменты из $MFT

Ниже представлен скрипт для поиска событий, связанных со сканированием файлов. Работает скрипт с файлом CSV, извлеченным из $MFT при помощи утилиты MFTECmd.exe (Eric Zimmerman's tools). События записываются в JSON-файл. По умолчанию записываются 5 предыдущих событий, это количество можно изменить.

Скрипт для поиска событий, связанных со сканированием
import csv

import os

import json

from datetime import datetime

def read_csv_with_nul_fix(file_path):

    """Чтение CSV файла с исправлением нулевых байтов"""

    with open(file_path, 'r', encoding='utf-8', errors='replace') as f:

        content = f.read()

    content = content.replace('\x00', ' ')

    lines = content.splitlines()

    reader = csv.DictReader(lines)

    return list(reader)

def parse_datetime(date_str):

    """Парсинг даты из строки в datetime объект"""

    if not date_str or date_str == 'N/A':

        return None

    try:

        # Формат: 2024-09-27 14:53:48.4646241

        return datetime.strptime(date_str.split('.')[0], '%Y-%m-%d %H:%M:%S')

    except:

        return None

 def sort_data_by_date(reader):

    """Сортировка данных по дате Created0x10"""

    indexed_data = []

       for i, row in enumerate(reader):

        date_str = row.get('Created0x10', '')

        dt = parse_datetime(date_str)

        indexed_data.append((i, dt, date_str, row))

# Сортируем по дате (None значения в конец)

    indexed_data.sort(key=lambda x: x[1] or datetime.min)

      return indexed_data

 def search_defender_artifacts_sorted(indexed_data):

    """Поиск артефактов Windows Defender в отсортированных данных"""

     results = []

     for sorted_index, (original_index, dt, date_str, row) in enumerate(indexed_data):

        parent_path = str(row.get('ParentPath', '')).strip()

# Основной путь Defender

        if 'ProgramData\\Microsoft\\Windows Defender\\Scans\\History\\Results\\Resource' in parent_path:

            results.append({

                'sorted_index': sorted_index,

                'original_index': original_index,

                'datetime': date_str,

                'type': 'DEFENDER_SCAN',

                'parent_path': row.get('ParentPath', ''),

                'file_name': row.get('FileName', ''),

                'file_size': row.get('FileSize', ''),

                'in_use': row.get('InUse', ''),

                'extension': row.get('Extension', ''),

                'created_0x10': row.get('Created0x10', ''),

                'last_modified_0x10': row.get('LastModified0x10', ''),

                'last_access_0x10': row.get('LastAccess0x10', '')

            })

return results

def get_previous_events(indexed_data, target_sorted_index, count=5):

    """Получение предыдущих событий в правильном порядке (от более поздних к более ранним)"""

    previous_events = []

    start = max(0, target_sorted_index - count)

# Собираем события от start до target_sorted_index - 1

    for i in range(start, target_sorted_index):

        original_index, dt, date_str, row = indexed_data[i]

        event = {

            'sorted_index': i,

            'original_index': original_index,

            'datetime': date_str,

            'parent_path': row.get('ParentPath', ''),

            'file_name': row.get('FileName', ''),

            'file_size': row.get('FileSize', ''),

            'in_use': row.get('InUse', ''),

            'extension': row.get('Extension', ''),

            'time_diff_seconds': None

        }

# Вычисляем разницу во времени с целевым событием

        target_dt = indexed_data[target_sorted_index][1]

        if dt and target_dt:

            time_diff = (target_dt - dt).total_seconds()

            event['time_diff_seconds'] = time_diff

          previous_events.append(event)

# Сортируем previous_events по sorted_index в убывающем порядке (от более поздних к более ранним)

    previous_events.sort(key=lambda x: x['sorted_index'], reverse=True)

    return previous_events

def save_to_json(results, indexed_data, output_path, events_before=5):

    """Сохранение результатов в JSON файл"""

output_data = {

        'analysis_info': {

            'analysis_date': datetime.now().isoformat(),

            'csv_file': os.path.basename(output_path).replace('_defender_analysis.json', '.csv'),

            'total_records_processed': len(indexed_data),

            'defender_scans_found': len(results),

            'events_before_scan': events_before,

            'time_period': {

                'first_record': indexed_data[0][2] if indexed_data else None,

                'last_record': indexed_data[-1][2] if indexed_data else None

            }

        },

        'defender_scans': []

    }

# Добавляем информацию о каждом сканировании

    for result in results:

        scan_data = {

            'scan_info': {

                'sorted_index': result['sorted_index'],

                'original_index': result['original_index'],

                'datetime': result['datetime'],

                'guid': result['file_name'],

                'parent_path': result['parent_path'],

                'file_size': result['file_size'],

                'in_use': result['in_use'],

                'extension': result['extension']

            },

            'previous_events': get_previous_events(indexed_data, result['sorted_index'], events_before)

        }

# Добавляем временную разницу с предыдущим событием

        if result['sorted_index'] > 0:

            prev_dt = indexed_data[result['sorted_index'] - 1][1]

            current_dt = indexed_data[result['sorted_index']][1]

            if prev_dt and current_dt:

                time_diff = (current_dt - prev_dt).total_seconds()

                scan_data['time_since_previous_event_seconds'] = time_diff

         output_data['defender_scans'].append(scan_data)

  # Добавляем статистику

    if len(results) > 1:

        time_intervals = []

        for i in range(1, len(results)):

            current_dt = parse_datetime(results[i]['datetime'])

            prev_dt = parse_datetime(results[i-1]['datetime'])

            if current_dt and prev_dt:

                interval_hours = (current_dt - prev_dt).total_seconds() / 3600

                time_intervals.append(interval_hours)

           if time_intervals:

            first_scan_dt = parse_datetime(results[0]['datetime'])

            last_scan_dt = parse_datetime(results[-1]['datetime'])

           output_data['statistics'] = {

                'total_scan_period_hours': (last_scan_dt - first_scan_dt).total_seconds() / 3600 if first_scan_dt and last_scan_dt else 0,

                'average_interval_hours': sum(time_intervals) / len(time_intervals),

                'min_interval_hours': min(time_intervals),

                'max_interval_hours': max(time_intervals),

                'total_scans': len(results),

                'first_scan': results[0]['datetime'],

                'last_scan': results[-1]['datetime']

            }

# Сохраняем в файл

    with open(output_path, 'w', encoding='utf-8') as f:

        json.dump(output_data, f, ensure_ascii=False, indent=2)

return output_path

def main():

    file_path = input("Введите полный путь к CSV файлу: ").strip()

  if not os.path.exists(file_path):

        print(f"Ошибка: Файл '{file_path}' не найден!")

        return

 # Запрашиваем количество предыдущих событий для анализа

    try:

        events_before = int(input("Введите количество предыдущих событий для анализа (по умолчанию 5): ").strip() or "5")

        if events_before < 0:

            print("Количество событий должно быть неотрицательным. Установлено значение по умолчанию: 5")

            events_before = 5

    except ValueError:

        print("Некорректный ввод. Установлено значение по умолчанию: 5")

        events_before = 5

     print(f"Будет анализироваться {events_before} предыдущих событий перед каждым сканированием.")

    # Автоматическое определение пути для JSON

    base_name = os.path.splitext(file_path)[0]

    output_json_path = f"{base_name}_defender_analysis.json"

      try:

        # Читаем данные

        reader = read_csv_with_nul_fix(file_path)

        print(f"Файл загружен. Строк: {len(reader)}")

        # Сортируем по дате

        indexed_data = sort_data_by_date(reader)

           # Ищем артефакты Defender

        results = search_defender_artifacts_sorted(indexed_data)

            if not results:

            # Создаем JSON с информацией, что сканирования не найдены

            no_results_data = {

                'analysis_info': {

                    'analysis_date': datetime.now().isoformat(),

                    'csv_file': os.path.basename(file_path),

                    'total_records_processed': len(indexed_data),

                    'defender_scans_found': 0,

                    'events_before_scan': events_before,

                    'status': 'NO_DEFENDER_SCANS_FOUND',

                    'time_period': {

                        'first_record': indexed_data[0][2] if indexed_data else None,

                        'last_record': indexed_data[-1][2] if indexed_data else None

                    }

                }

            }

           with open(output_json_path, 'w', encoding='utf-8') as f:

                json.dump(no_results_data, f, ensure_ascii=False, indent=2)

           print(f"Сканирования Defender не найдены. Создан пустой отчет: {output_json_path}")

            return

              # Сохраняем результаты в JSON

        saved_path = save_to_json(results, indexed_data, output_json_path, events_before)

        print(f"\nАнализ завершен!")

        print(f"📊 Обработано записей: {len(indexed_data)}")

        print(f"🔍 Найдено сканирований: {len(results)}")

        print(f"📈 Анализируется событий до сканирования: {events_before}")

        print(f"💾 JSON отчет сохранен: {saved_path}")

       # Краткая информация о первом и последнем сканировании

        if len(results) > 0:

            print(f"📅 Первое сканирование: {results[0]['datetime']}")

            print(f"📅 Последнее сканирование: {results[-1]['datetime']}")

        if len(results) > 1:

            first_dt = parse_datetime(results[0]['datetime'])

            last_dt = parse_datetime(results[-1]['datetime'])

            if first_dt and last_dt:

                total_hours = (last_dt - first_dt).total_seconds() / 3600

                print(f"⏱️  Период сканирований: {total_hours:.2f} часов")

        except Exception as e:

        print(f"Ошибка: {e}")

        import traceback

        traceback.print_exc()

 if name == "__main__":

    main()

В завершение еще раз отметим, что анализ событий Windows Defender может быть очень полезным инструментом при расследовании инцидентов ИБ. В то же время он не всегда позволяет получить новые данные — поэтому должен использоваться в качестве подспорья для основного метода исследования.