
Привет, Хабр! На связи команда 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) всегда имеют одно и то же значение.

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

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

Здесь самое важное — это размеры последующих двух блоков. Для каждого блока выделяется 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.


Здесь, думаю, пояснений уже не нужно, возьмем расшифрованные данные второго блока (выделены красным) и разместим их в hex-редакторе для более удобного чтения.
На рисунке 6 зеленым цветом выделен UUID файла {5FB5EA10-7DFB-48BC-B1AA-4E7954F5F25F}:
10EAB55F—5FB5EA10(little-endian);FB7D—7DFB(little-endian);BC48—48BC(little-endian);B1AA— сохраняется последовательность;4E7954F5F25F— сохраняется последовательность.
Черным цветом выделена временная метка в формате Windows FILETIME: 396BD3B50B53DC01 — 01DC530BB5D36B39(2025-11-1113:04:27.454956). По всей видимости, это время создания файла и окончания сканирования. Оно нам никак не поможет в дальнейшем.

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

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

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

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

Зеленым цветом выделен расшифрованный первый блок. Желтым — указатель на размер второго блока, читаем наоборот: 0х04AB = 1195 dec. Далее идет второй блок — как видите, он начинается с размера самого блока (обозначен черным).
На рисунке 11 представлен пример. Файл …rypt.exe был извлечен из архива, …rypt.exe:Zone.Identifier указывает на то, что архив был скачан из интернета через браузер и WinRAR скопировал из архива метку Zone.Identifier для потенциально небезопасного файла. Файл readme.txtтакже извлечен из архива, но не получил Zone.Identifier, так как WinRAR счел текстовый файл безопасным. В конце видим файлы, связанные со сканированием.

Связка …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 как неиспользуемые, — разумеется, с сохраненными атрибутами.

Кроме того, есть еще одна проблема: файл, предшествующий событию сканирования, был удален. В таком случае без дополнительных артефактов не понять, на какой файл реагировал Defender.
Теперь рассмотрим, как выглядит таймлайн в $MFT при реальной компрометации компьютерной системы. На рисунках 13, 14 и 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 может быть очень полезным инструментом при расследовании инцидентов ИБ. В то же время он не всегда позволяет получить новые данные — поэтому должен использоваться в качестве подспорья для основного метода исследования.

