- 1. Введение
- 2. Руководство для обычных пользователей
- 3. Руководство для разработчиков
- 4. Ссылки по теме
1. Введение
1.1 Обзор системы
RDPmon представляет собой открытое решение для мониторинга RDP-подключений, разработанное компанией Cameyo для защиты от атак методом перебора (brute-force). Система предназначена для мониторинга и анализа попыток подключения к серверам Windows через протокол удалённого рабочего стола (Remote Desktop Protocol).[^1][^2][^3]
Основные возможности RDPmon:
- Мониторинг всех RDP-подключений в реальном времени
- Обнаружение атак методом перебора паролей
- Отслеживание активных и завершённых сессий пользователей
- Анализ запущенных процессов в RDP-сессиях
- Цветовая индикация уровня угрозы (зелёный, жёлтый, красный)
- Полная конфиденциальность данных (всё остаётся локально)
Проблемы, решаемые RDPmon:
Согласно исследованиям Cameyo, облачные машины Windows с открытым портом 3389 подвергаются более чем 150,000 попыток входа в неделю. RDPmon адресует критическую потребность в мониторинге RDP-безопасности для облачных инфраструктур.[^2][^4][^1]
1.2 Архитектура решения
RDPmon построен на основе многоуровневой архитектуры, включающей следующие компоненты:

Архитектура RDPmon — диаграмма компонентов системы
Основные компоненты:
- Windows Service (RdpMon.exe) — центральная служба, обеспечивающая непрерывный мониторинг
- GUI Application — пользовательский интерфейс для просмотра и управления данными
- Мониторинговые модули:
- ConnectMon — мониторинг RDP-подключений
- SessionMon — отслеживание пользовательских сессий
- ProcessMon — мониторинг запущенных процессов
- База данных LiteDB — локальное хранение всех данных мониторинга
- Интеграция с Windows API:
- Windows Event Log (события безопасности)
- Windows Terminal Services API (информация о сессиях)
Принципы работы:
- Служба работает в фоновом режиме с правами SYSTEM
- GUI подключается к службе для отображения данных в реальном времени
- Все данные остаются локально в файле LiteDB
- Система использует Windows ETW для получения событий безопасности
1.3 Новые возможности
В рамках модернизации RDPmon были спроектированы следующие расширения функциональности:
Контекстное меню для записей:
- Блокировать IP через Windows Firewall
- Копировать IP-адрес в буфер обмена
- Отправить информацию об атаке в AbuseIPDB[^5][^6][^7][^8]
Новые вкладки интерфейса:
- Settings — централизованные настройки системы
- Reports — генерация и просмотр отчётов
Расширенные настройки безопасности:
- Белый список IP-адресов (исключения)
- Чёрный список IP-адресов (автоматическая и ручная блокировка)
- Настройка количества неудачных попыток для блокировки
- Интеграция с AbuseIPDB API для проверки репутации IP
- Автоматическая блокировка на основе Confidence of Abuse %
2. Руководство для обычных пользователей
2.1 Установка и первоначальная настройка
2.1.1 Системные требования
Операционная система:
- Windows 10 версии 1903 и выше
- Windows 11 (все версии)
- Windows Server 2019/2022
Программные требования:
- .NET Framework 4.6.1 или выше
- Права администратора для установки службы
- Доступ к интернету для интеграции с AbuseIPDB (опционально)
Системные ресурсы:
- ОЗУ: минимум 100 МБ, рекомендуется 256 МБ
- Дисковое пространство: 50 МБ + место для базы данных (растёт по мере накопления данных)
- CPU: минимальное влияние на производительность системы
2.1.2 Процесс установки
- Загрузка RdpMon.exe с официального репозитория GitHub
- Первый запуск — система предложит установить службу автоматически
- Подтверждение установки службы “RDP Monitor”
- Автоматический запуск службы после установки
- Запуск GUI для начала работы с интерфейсом
Важные особенности:
- При первом запуске требуются права администратора для установки службы
- Служба устанавливается автоматически и не требует дополнительной настройки
- Удаление службы:
rdpmon.exe -uninst
2.1.3 Первый запуск и автоматическая установка службы
При первом запуске RdpMon.exe выполняется автоматическая проверка установки службы. Если служба не обнаружена, появляется диалог подтверждения установки. После согласия пользователя:
- Система запрашивает повышение привилегий (UAC)
- Устанавливается служба Windows “RdpMon”
- Служба автоматически запускается
- Открывается главное окно приложения
- Начинается сбор данных о RDP-подключениях
2.2 Работа с основным интерфейсом
2.2.1 Вкладка Connections (Подключения)
Основная вкладка для мониторинга RDP-подключений отображает агрегированную информацию по IP-адресам:
Столбцы таблицы:
- IP Address — адрес источника подключений
- Fail Count — количество неудачных попыток входа
- Success Count — количество успешных подключений
- First Time — время первого обнаруженного подключения
- Last Time — время последнего зафиксированного события
- Duration — продолжительность активности (для текущих атак показывается “ongoing”)
- Logins — список пользователей, с которыми пытались подключиться
Цветовая индикация:
- 🟢 Зелёный — легитимные подключения (есть успешные входы, мало неудачных попыток)
- 🟡 Жёлтый — подозрительная активность (несколько неудачных попыток)
- 🔴 Красный — атаки методом перебора (10+ неудачных попыток подряд)
Фильтры отображения:
- Показать атаки (красные записи)
- Показать легитимные подключения (зелёные записи)
- Показать неопределённые (жёлтые записи)
2.2.2 Вкладка Sessions (Сессии)
Детальная информация о пользовательских сессиях:
Информация о сессиях:
- Session ID — уникальный идентификатор WTS сессии
- User — имя пользователя
- State — статус сессии (Active, Disconnected, Ended)
- Started — время начала сессии
- Ended — время завершения (для активных показывается “ongoing”)
- Address — IP-адрес клиента
Дополнительная функциональность:
- Просмотр процессов, запущенных в выбранной сессии
- Возможность теневого подключения к активным сессиям (Shadow)
- Фильтрация системных процессов
2.2.3 Новая вкладка Settings (Настройки)
Централизованная панель настроек системы безопасности:
Основные настройки:
- Белый список IP — доверенные адреса, исключённые из блокировки
- Чёрный список IP — заблокированные адреса
- Failures для блокировки — количество неудачных попыток для автоматической блокировки
- Failures для AbuseIPDB — порог для отправки сообщения о злоумышленнике
Настройки AbuseIPDB:
- API Key — ключ для доступа к AbuseIPDB API
- Проверять IP после X failures — автоматическая проверка репутации
- Confidence of Abuse % — процент уверенности для классификации как вредоносный
2.2.4 Новая вкладка Reports (Отчёты)
Система генерации отчётов для анализа безопасности:
Типы отчётов:
- Сводка по атакам — статистика за выбранный период
- Топ атакующих IP — наиболее активные злоумышленники
- Географическое распределение — анализ по странам (при интеграции с GeoIP)
- Временные паттерны — активность атак по времени
Настройки экспорта:
- Формат: CSV, PDF, HTML
- Период: за день, неделю, месяц, произвольный
- Автоматическая отправка по email (в перспективе)
2.3 Использование контекстного меню
2.3.1 Блокировка IP-адресов
Щелчок правой кнопкой мыши по записи в таблице подключений открывает контекстное меню с опциями:
“Блокировать IP”:
- Создаётся правило в Windows Firewall
- Блокируется весь трафик с данного IP на порт 3389 (TCP/UDP)
- IP добавляется в чёрный список системы
- Записывается событие в лог системы
Особенности блокировки:
- Правила создаются для входящих подключений
- Действуют на всех сетевых профилях (Domain, Private, Public)
- Автоматическое именование правил: “RdpMon.BlackList TCP/UDP”
2.3.2 Копирование IP-адресов
“Копировать IP” — быстрое копирование IP-адреса в буфер обмена для:
- Дальнейшего анализа в внешних системах
- Добавления в конфигурации сетевого оборудования
- Передачи коллегам для расследования инцидента
2.3.3 Отправка сообщений в AbuseIPDB
“Отправить информацию о атаке в AbuseIPDB”:
Автоматическая отправка отчёта о вредоносной активности включает:
- IP-адрес злоумышленника
- Категории атаки (по классификации AbuseIPDB)[^5]
- Комментарий с деталями атаки (количество попыток, период)
- Автоматическую классификацию как “Brute Force” (категория 18)
Требования:
- Настроенный API ключ AbuseIPDB
- Подключение к интернету
- Соблюдение лимитов API (1000 запросов/день для бесплатного аккаунта)
2.4 Настройка параметров безопасности
2.4.1 Управление белым списком IP
Белый список защищает доверенные IP-адреса от автоматической блокировки:
Случаи использования:
- Офисные IP-адреса администраторов
- VPN-серверы компании
- Адреса систем мониторинга
- Партнёрские организации
Управление списком:
- Добавление через интерфейс Settings
- Импорт из файла (формат: один IP на строку)
- Поддержка диапазонов IP (CIDR нотация)
- Автоматическое исключение localhost (127.0.0.1)
2.4.2 Управление чёрным списком IP
Чёрный список содержит заблокированные IP-адреса:
Источники пополнения:
- Ручная блокировка через контекстное меню
- Автоматическая блокировка при превышении лимита попыток
- Импорт внешних списков угроз
- Синхронизация с AbuseIPDB
Функции управления:
- Просмотр всех заблокированных IP
- Снятие блокировки (разблокировка)
- Просмотр истории блокировок
- Экспорт списка для резервного копирования
2.4.3 Настройка количества попыток для блокировки
Настройка “Failures для блокировки”:
- По умолчанию: 10 неудачных попыток
- Диапазон: от 3 до 100 попыток
- Учитывается скользящее окно времени (последние 24 часа)
Настройка “Failures для отправки в AbuseIPDB”:
- По умолчанию: 20 неудачных попыток
- Предотвращает спам-отчёты в AbuseIPDB
- Должна быть больше или равна настройке блокировки
2.4.4 Конфигурация AbuseIPDB API
Получение API ключа:
- Регистрация на abuseipdb.com
- Верификация домена (для увеличения лимитов)
- Создание API ключа v2 в панели управления
Настройки интеграции:
- API Key — вставка полученного ключа
- Test Connection — проверка доступности API
- Daily Limit Monitoring — отслеживание использованных запросов
2.4.5 Настройка автоматической проверки
“Проверять IP в AbuseIPDB после X Failures”:
- Автоматический запрос к AbuseIPDB при достижении порога
- Получение Confidence of Abuse % для IP
- Принятие решения о блокировке на основе репутации
“Confidence of Abuse % для блокировки”:
- По умолчанию: 75% (высокая уверенность в том, что IP вредоносный)
- Диапазон: от 25% до 100%
- Рекомендуется: 75-90% для баланса между безопасностью и ложными срабатываниями
2.5 Мониторинг и анализ
2.5.1 Интерпретация цветовой маркировки
🟢 Зелёные записи (легитимные):
- Есть успешные подключения
- Менее 100 неудачных попыток
- Обычно администраторы или авторизованные пользователи
🟡 Жёлтые записи (подозрительные):
- Нет успешных подключений
- Менее 10 неудачных попыток
- Могут быть случайными сканированиями или ошибками пользователей
🔴 Красные записи (атаки):
- 10 или более неудачных попыток подряд
- Нет успешных подключений
- Высокая вероятность brute-force атаки
Специальные индикаторы:
- “ongoing” — атака продолжается в данный момент (последняя активность менее 2 минут назад)
- Мигающие записи — новая активность в реальном времени
2.5.2 Анализ статистики подключений
Общая статистика (строка состояния):
- Количество легитимных пользователей
- Количество подозрительных адресов
- Общее количество попыток входа
- Предупреждение об отключённом NLA (Network Level Authentication)
Паттерны атак:
- Распределённые атаки — множество IP с небольшим количеством попыток каждый
- Концентрированные атаки — один IP с большим количеством попыток
- Продолжительные атаки — длительные по времени кампании
2.5.3 Работа с отчётами
Генерация отчётов:
- Переход на вкладку Reports
- Выбор типа отчёта и периода
- Настройка параметров фильтрации
- Генерация и просмотр результатов
Анализ трендов:
- Динамика количества атак по времени
- Сезонность атакующей активности
- Корреляция с внешними событиями (публикация уязвимостей, праздники)
Экспорт данных:
- Сохранение в различных форматах для дальнейшего анализа
- Интеграция с системами Business Intelligence
- Предоставление отчётов руководству и службе безопасности
3. Руководство для разработчиков
3.1 Архитектура системы
3.1.1 Обзор компонентов
RDPmon построен на основе многоуровневой архитектуры, которая обеспечивает разделение ответственности и гибкость системы:[^9][^10]
Уровень представления (Presentation Layer):
MainForm.cs— главная форма приложенияSettingsForm.cs— форма настроек (новая)ReportsForm.cs— форма отчётов (новая)ShadowForm.cs— форма теневого подключения
Уровень бизнес-логики (Business Layer):
ConnectMon.cs— мониторинг подключенийSessionMon.cs— отслеживание сессийProcessMon.cs— мониторинг процессов- Новые модули расширения функциональности
Уровень служб (Service Layer):
Service.cs— Windows Service реализацияProgram.cs— точка входа и управление службой
Уровень доступа к данным (Data Access Layer):
LiteDB— встроенная NoSQL база данных[^11][^12]- Модели данных:
Addr,Session,Process
3.1.2 Паттерны проектирования
Service Pattern: Windows Service обеспечивает непрерывную работу мониторинга в фоновом режиме, независимо от пользовательских сессий.[^10]
Observer Pattern: Система событий для уведомления GUI о изменениях в данных:
EventWaitHandle waitEvent = new EventWaitHandle(false, EventResetMode.AutoReset, @"Global\RdpMonRefresh");
Repository Pattern: Инкапсуляция логики доступа к данным LiteDB:
public class Addrs
{
private LiteDatabase db;
private LiteCollection<Addr> addrTable;
public void Aggregate(LiteCollection<Addr> addrTable, string ip, DateTime utcTime, bool success, string userName)
{
// Логика агрегации данных
}
}
Factory Pattern: Создание мониторинговых компонентов:
var connectionsAggregator = new ConnectMon(true, true);
var sessionMon = new SessionMon();
var processMon = new ProcessMon();
3.1.3 Взаимодействие между компонентами
Схема взаимодействия:
- Windows Service запускается автоматически при загрузке системы
- ConnectMon периодически агрегирует события из Windows Event Log
- SessionMon отслеживает изменения сессий через WTS API
- Данные сохраняются в LiteDB для персистентности
- GUI подключается к службе через глобальные события
- Новые модули интегрируются через dependency injection
3.2 Структура исходного кода
3.2.1 Основные классы и модули
ConnectMon.cs — Мониторинг подключений:
public class ConnectMon
{
public const int FailureEvtId = 4625; // Неудачный вход
public const int SuccessEvtId = 4648; // Успешный вход
public Addrs Aggregate(DateTime fromUtc)
{
// Запрос событий из Event Log
// Агрегация по IP-адресам
// Обновление базы данных
}
}
SessionMon.cs — Мониторинг сессий:
public class SessionMon
{
public void OnSessionChange(SessionChangeDescription sessionChange)
{
switch (sessionChange.Reason)
{
case SessionChangeReason.SessionLogon:
// Обработка входа в сессию
break;
case SessionChangeReason.SessionLogoff:
// Обработка выхода из сессии
break;
}
}
}
Addr.cs — Модель IP-адреса:
public class Addr
{
public string AddrId { get; set; }
public int FailCount { get; set; }
public int SuccessCount { get; set; }
public DateTime First { get; set; }
public DateTime Last { get; set; }
public HashSet<string> UserNames { get; set; }
public bool IsAttack(int offset = 0) => (FailCount >= 10 + offset);
public bool IsLegit() => (SuccessCount > 0 && FailCount < 100);
}
3.2.2 Работа с базой данных LiteDB
Подключение к базе данных:
using (var db = new LiteDatabase("Filename=" + Utils.MyPath("RdpMon.db") + ";utc=true"))
{
var addrTable = db.GetCollection<Addr>("Addr");
var sessionTable = db.GetCollection<Session>("Session");
var processTable = db.GetCollection<Process>("Process");
}
Операции CRUD:
// Create/Update
addrTable.Upsert(addr);
// Read
var addresses = addrTable.FindAll();
var specificAddr = addrTable.FindById(ip);
// Query with conditions
var attackingIPs = addrTable.Find(x => x.FailCount >= 10);
// Delete
addrTable.Delete(ip);
Индексирование для производительности:
addrTable.EnsureIndex(x => x.Last); // Для сортировки по времени
addrTable.EnsureIndex(x => x.FailCount); // Для фильтрации атак
3.2.3 Windows Service реализация
Service.cs структура:
partial class Service : ServiceBase
{
Thread mainThread = null;
bool Stopping = false;
EventWaitHandle waitEvent;
protected override void OnStart(string[] args)
{
mainThread = new Thread(MainThread);
mainThread.Start();
}
protected override void OnStop()
{
Stopping = true;
waitEvent.Set();
}
void MainThread()
{
var connectionsAggregator = new ConnectMon(true, true);
while (!Stopping)
{
connectionsAggregator.Aggregate(DateTime.MinValue);
waitEvent.WaitOne(3 * 60 * 1000); // 3 минуты в обычном режиме
}
}
}
3.2.4 WinForms интерфейс
MainForm.cs — основные принципы:
public partial class MainForm : Form
{
private void RefreshLV(bool initialLoad)
{
if (initialLoad || tabs.SelectedIndex == 0)
RefreshConnectionsLV(initialLoad);
if (initialLoad || tabs.SelectedIndex == 1)
RefreshSessionsLV(initialLoad, WTS.ListSessions());
}
private void OnLvColumnClick(object sender, ColumnClickEventArgs e)
{
// Реализация сортировки столбцов
var sorter = connectsSorter;
if (e.Column == sorter.SortColumn)
sorter.Order = (sorter.Order == SortOrder.Ascending) ?
SortOrder.Descending : SortOrder.Ascending;
}
}
3.3 Новые модули — детальная реализация
3.3.1 Модуль блокировки IP (IPBlocker)
Назначение: Автоматическая и ручная блокировка IP-адресов через Windows Firewall API.
Основные функции:
- Создание правил блокировки для TCP/UDP порта 3389
- Управление существующими правилами
- Интеграция с чёрным списком
- Логирование операций блокировки
3.3.2 Модуль AbuseIPDB (AbuseReporter)
Назначение: Интеграция с внешним сервисом AbuseIPDB для проверки репутации IP и отправки сообщений о злоумышленниках.
API эндпоинты:[^5][^7]
/api/v2/check— проверка репутации IP/api/v2/report— сообщение о вредоносной активности/api/v2/blacklist— получение списка известных злоумышленников
Обработка ответов:
public class AbuseIPDBResponse
{
public int abuseConfidencePercentage { get; set; }
public string countryCode { get; set; }
public bool isWhitelisted { get; set; }
public DateTime lastReportedAt { get; set; }
}
3.3.3 Модуль управления списками (ListManager)
Назначение: Централизованное управление белыми и чёрными списками IP-адресов.
Функциональность:
- CRUD операции со списками
- Валидация IP-адресов и CIDR диапазонов
- Импорт/экспорт списков
- Синхронизация с внешними источниками угроз
3.3.4 Модуль настроек (SettingsManager)
Назначение: Управление конфигурацией системы с персистентным хранением настроек.
Настройки:
public class RdpMonSettings
{
public int FailuresForBlocking { get; set; } = 10;
public int FailuresForAbuseReport { get; set; } = 20;
public string AbuseIPDBApiKey { get; set; }
public int AbuseConfidenceThreshold { get; set; } = 75;
public bool EnableAutoBlocking { get; set; } = true;
public bool EnableAbuseIPDBCheck { get; set; } = false;
}
3.3.5 Модуль отчётов (ReportsManager)
Назначение: Генерация различных типов отчётов для анализа безопасности.
Типы отчётов:
- Сводка атак за период
- Топ атакующих IP-адресов
- Временные паттерны активности
- Географическое распределение (при наличии GeoIP данных)
3.3.6 Контекстное меню (ContextMenuManager)
Назначение: Предоставление быстрого доступа к основным действиям через контекстное меню.
Действия:
- Блокировка IP
- Копирование IP в буфер обмена
- Отправка сообщения в AbuseIPDB
- Добавление в белый/чёрный список
3.4 API и интерфейсы
3.4.1 Windows Event Log API
Чтение событий безопасности:
var query = "*[" +
"(System/EventID=" + SuccessEvtId.ToString() + " or " + "System/EventID=" + FailureEvtId.ToString() + ")" +
" and " +
"System[TimeCreated[@SystemTime>'" + fromUtc.ToString("yyyy-MM-dd") + "T" + fromUtc.ToString("HH:mm:ss") + ".000000000Z" + "']]" +
"]";
var eventsQuery = new EventLogQuery("Security", PathType.LogName, query);
var logReader = new EventLogReader(eventsQuery);
for (var evt = logReader.ReadEvent(); evt != null; evt = logReader.ReadEvent())
{
ProcessSecurityEvent(evt);
}
События для мониторинга:
- 4625 — Неудачная попытка входа
- 4648 — Успешный вход (explicit credentials)
- 21 — Terminal Services session logon (из TS-LocalSessionManager log)
3.4.2 Windows Terminal Services API
Получение информации о сессиях:
[DllImport("wtsapi32.dll")]
static extern int WTSEnumerateSessions(
IntPtr pServer,
int iReserved,
int iVersion,
ref IntPtr pSessionInfo,
ref int iCount);
[DllImport("Wtsapi32.dll")]
public static extern bool WTSQuerySessionInformation(
IntPtr pServer,
Int32 iSessionID,
WTS_INFO_CLASS oInfoClass,
out IntPtr pBuffer,
out uint iBytesReturned);
Типы информации WTS:
WTSUserName— имя пользователя сессииWTSSessionId— идентификатор сессииWTSConnectState— состояние подключенияWTSClientAddress— IP-адрес клиентаWTSLogonTime— время входа в сессию
3.4.3 Windows Firewall API
Управление правилами фаервола:
Type typeFWPolicy2 = Type.GetTypeFromCLSID(new Guid("{E2B3C97F-6AE1-41AC-817A-F6F92166D7DD}"));
Type typeFWRule = Type.GetTypeFromCLSID(new Guid("{2C5BC43E-3369-4C33-AB0C-BE9469677AF4}"));
INetFwPolicy2 fwPolicy2 = (INetFwPolicy2)Activator.CreateInstance(typeFWPolicy2);
// Создание правила блокировки
var newRule = (INetFwRule)Activator.CreateInstance(typeFWRule);
newRule.Name = "RdpMon.BlackList TCP";
newRule.Protocol = (int)NET_FW_IP_PROTOCOL_.NET_FW_IP_PROTOCOL_TCP;
newRule.LocalPorts = "3389";
newRule.Direction = NET_FW_RULE_DIRECTION_.NET_FW_RULE_DIR_IN;
newRule.Action = NET_FW_ACTION_.NET_FW_ACTION_BLOCK;
newRule.RemoteAddresses = ipAddress + "/255.255.255.255";
fwPolicy2.Rules.Add(newRule);
3.4.4 AbuseIPDB REST API
HTTP клиент для AbuseIPDB:
public async Task<AbuseIPDBCheckResponse> CheckIPAsync(string ipAddress)
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Key", apiKey);
client.DefaultRequestHeaders.Add("Accept", "application/json");
var response = await client.GetAsync($"https://api.abuseipdb.com/api/v2/check?ipAddress={ipAddress}&maxAgeInDays=90");
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<AbuseIPDBCheckResponse>(content);
}
}
public async Task<bool> ReportIPAsync(string ipAddress, string comment, int[] categories)
{
using (var client = new HttpClient())
{
var formData = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("ip", ipAddress),
new KeyValuePair<string, string>("categories", string.Join(",", categories)),
new KeyValuePair<string, string>("comment", comment)
};
var response = await client.PostAsync("https://api.abuseipdb.com/api/v2/report",
new FormUrlEncodedContent(formData));
return response.IsSuccessStatusCode;
}
}
3.5 База данных и модели данных
3.5.1 Схема базы данных
Коллекции LiteDB:
- Addr — агрегированная информация по IP-адресам
- Session — детали пользовательских сессий
- Process — процессы, запущенные в сессиях
- Prop — системные свойства и настройки
- WhiteList — белый список IP (новая)
- BlackList — чёрный список IP (новая)
- Settings — настройки приложения (новая)
3.5.2 Модели данных
Addr — агрегация по IP-адресам:
public class Addr
{
[BsonId]
public string AddrId { get; set; } // IP-адрес
public long Type { get; set; }
public long Flags { get; set; }
public long Prot { get; set; } // Protection flags
public int FailCount { get; set; } // Неудачные попытки
public int SuccessCount { get; set; } // Успешные подключения
public DateTime First { get; set; } // Первое обнаружение
public DateTime Last { get; set; } // Последняя активность
public HashSet<string> UserNames { get; set; } // Список пользователей
[Flags]
public enum ProtFlag { None = 0, Blocked = 1 };
public bool IsOngoing() => (DateTime.UtcNow.Subtract(this.Last).TotalMinutes < 2);
public bool IsAttack(int offset = 0) => (FailCount >= 10 + offset);
public bool IsLegit() => (SuccessCount > 0 && FailCount < 100);
}
Session — пользовательские сессии:
public class Session
{
[BsonId]
public long SessionUid { get; set; } // Уникальный ID
public long WtsSessionId { get; set; } // WTS Session ID
public DateTime Start { get; set; } // Начало сессии
public DateTime? End { get; set; } // Конец сессии (null для активных)
public string User { get; set; } // Имя пользователя
public long Flags { get; set; }
public string Addr { get; set; } // IP-адрес клиента
public static long GetSessionUid(long wtsSessionId, DateTime start)
{
long ret = start.Ticks;
ret &= ~(0x7ff0000000000000);
long hiBit = wtsSessionId;
hiBit <<= 48;
ret |= hiBit;
return ret;
}
}
3.5.3 Новые модели данных
Settings — настройки приложения:
public class Settings
{
[BsonId]
public string SettingId { get; set; }
public int FailuresForBlocking { get; set; } = 10;
public int FailuresForAbuseReport { get; set; } = 20;
public string AbuseIPDBApiKey { get; set; }
public int AbuseConfidenceThreshold { get; set; } = 75;
public bool EnableAutoBlocking { get; set; } = true;
public bool EnableAbuseIPDBCheck { get; set; } = false;
public int CheckFailuresThreshold { get; set; } = 5;
public DateTime LastModified { get; set; }
}
WhiteList — белый список:
public class WhiteListEntry
{
[BsonId]
public string Id { get; set; }
public string IPAddress { get; set; }
public string Description { get; set; }
public DateTime Added { get; set; }
public string AddedBy { get; set; }
public bool IsRange { get; set; } // CIDR диапазон
public string CIDR { get; set; } // Для диапазонов
}
BlackList — чёрный список:
public class BlackListEntry
{
[BsonId]
public string Id { get; set; }
public string IPAddress { get; set; }
public DateTime BlockedAt { get; set; }
public string Reason { get; set; }
public bool AutoBlocked { get; set; } // Автоматическая или ручная блокировка
public int FailuresCount { get; set; }
public bool FirewallRuleCreated { get; set; }
public bool ReportedToAbuseIPDB { get; set; }
}
3.5.4 Индексы и оптимизация
Создание индексов для производительности:
// Основные индексы для быстрого поиска
addrTable.EnsureIndex(x => x.Last); // Сортировка по времени
addrTable.EnsureIndex(x => x.FailCount); // Фильтрация атак
addrTable.EnsureIndex(x => x.SuccessCount); // Легитимные подключения
sessionTable.EnsureIndex(x => x.Start); // Сортировка сессий
sessionTable.EnsureIndex(x => x.User); // Поиск по пользователю
sessionTable.EnsureIndex(x => x.Addr); // Поиск по IP
blacklistTable.EnsureIndex(x => x.BlockedAt); // Сортировка блокировок
whitelistTable.EnsureIndex(x => x.Added); // Сортировка исключений
Оптимизация запросов:
// Эффективные запросы с использованием индексов
var recentAttacks = addrTable.Find(Query.And(
Query.GTE("Last", DateTime.UtcNow.AddHours(-24)),
Query.GTE("FailCount", 10)
));
var activeSessions = sessionTable.Find(Query.EQ("End", null));
// Агрегация с группировкой
var topAttackers = addrTable
.Find(Query.GTE("FailCount", 10))
.OrderByDescending(x => x.FailCount)
.Take(10)
.ToList();
3.6 Полные коды новых модулей
3.6.1 IPBlocker.cs — Блокировка IP-адресов
using NetFwTypeLib;
using System;
using System.IO;
using LiteDB;
namespace Cameyo.RdpMon
{
/// <summary>
/// Модуль для автоматической и ручной блокировки IP-адресов через Windows Firewall
/// </summary>
public class IPBlocker
{
private const string RULE_NAME_TCP = "RdpMon.BlackList TCP";
private const string RULE_NAME_UDP = "RdpMon.BlackList UDP";
private const string RULE_DESCRIPTION = "RDP brute-force addresses blocked by RdpMon";
private readonly WinFirewall firewall;
private readonly ListManager listManager;
public IPBlocker()
{
firewall = new WinFirewall();
listManager = new ListManager();
}
/// <summary>
/// Блокирует IP-адрес через Windows Firewall и добавляет в чёрный список
/// </summary>
/// <param name="ipAddress">IP-адрес для блокировки</param>
/// <param name="reason">Причина блокировки</param>
/// <param name="autoBlocked">Была ли блокировка автоматической</param>
/// <param name="failuresCount">Количество неудачных попыток</param>
/// <returns>True если блокировка успешна</returns>
public bool BlockIP(string ipAddress, string reason, bool autoBlocked = false, int failuresCount = 0)
{
var logPrefix = $"BlockIP({ipAddress}): ";
try
{
// Проверяем, не находится ли IP в белом списке
if (listManager.IsInWhiteList(ipAddress))
{
Log(logPrefix + "IP находится в белом списке, блокировка отменена");
return false;
}
// Проверяем, не заблокирован ли уже
if (listManager.IsInBlackList(ipAddress))
{
Log(logPrefix + "IP уже заблокирован");
return true;
}
// Создаём правила фаервола для TCP и UDP
bool tcpSuccess = firewall.AddIp(false, ipAddress, 3389, false, RULE_NAME_TCP, RULE_DESCRIPTION);
bool udpSuccess = firewall.AddIp(false, ipAddress, 3389, false, RULE_NAME_UDP, RULE_DESCRIPTION);
if (tcpSuccess && udpSuccess)
{
// Добавляем в чёрный список
var blacklistEntry = new BlackListEntry
{
Id = Guid.NewGuid().ToString(),
IPAddress = ipAddress,
BlockedAt = DateTime.UtcNow,
Reason = reason,
AutoBlocked = autoBlocked,
FailuresCount = failuresCount,
FirewallRuleCreated = true,
ReportedToAbuseIPDB = false
};
listManager.AddToBlackList(blacklistEntry);
Log(logPrefix + $"успешно заблокирован. Причина: {reason}");
return true;
}
else
{
Log(logPrefix + "ошибка создания правил фаервола");
return false;
}
}
catch (Exception ex)
{
Log(logPrefix + "исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Разблокирует IP-адрес
/// </summary>
/// <param name="ipAddress">IP-адрес для разблокировки</param>
/// <returns>True если разблокировка успешна</returns>
public bool UnblockIP(string ipAddress)
{
var logPrefix = $"UnblockIP({ipAddress}): ";
try
{
// Удаляем правила фаервола
bool tcpRemoved = RemoveFirewallRule(RULE_NAME_TCP, ipAddress);
bool udpRemoved = RemoveFirewallRule(RULE_NAME_UDP, ipAddress);
// Удаляем из чёрного списка
listManager.RemoveFromBlackList(ipAddress);
Log(logPrefix + "разблокирован");
return tcpRemoved && udpRemoved;
}
catch (Exception ex)
{
Log(logPrefix + "исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Автоматическая блокировка при превышении лимита попыток
/// </summary>
/// <param name="addr">Информация об IP-адресе</param>
/// <param name="threshold">Пороговое значение неудачных попыток</param>
/// <returns>True если IP был заблокирован</returns>
public bool AutoBlockIfNeeded(Addr addr, int threshold)
{
if (addr.FailCount >= threshold && !addr.IsLegit())
{
string reason = $"Автоматическая блокировка: {addr.FailCount} неудачных попыток входа";
return BlockIP(addr.AddrId, reason, true, addr.FailCount);
}
return false;
}
/// <summary>
/// Удаляет конкретный IP из правила фаервола
/// </summary>
private bool RemoveFirewallRule(string ruleName, string ipAddress)
{
try
{
var policy = (INetFwPolicy2)Activator.CreateInstance(
Type.GetTypeFromCLSID(new Guid("{E2B3C97F-6AE1-41AC-817A-F6F92166D7DD}")));
var rule = policy.Rules.Item(ruleName);
if (rule != null)
{
var addresses = rule.RemoteAddresses;
var ipWithMask = ipAddress + "/255.255.255.255";
if (addresses.Contains(ipWithMask))
{
// Удаляем IP из списка адресов правила
var newAddresses = addresses.Replace(ipWithMask + ",", "").Replace("," + ipWithMask, "").Replace(ipWithMask, "");
if (string.IsNullOrEmpty(newAddresses) || newAddresses == ",")
{
// Если это был единственный IP, удаляем правило полностью
policy.Rules.Remove(ruleName);
}
else
{
// Обновляем правило с новым списком адресов
rule.RemoteAddresses = newAddresses;
}
}
}
return true;
}
catch (Exception ex)
{
Log($"RemoveFirewallRule({ruleName}, {ipAddress}): исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Получает список всех заблокированных IP-адресов
/// </summary>
/// <returns>Список заблокированных IP</returns>
public System.Collections.Generic.List<BlackListEntry> GetBlockedIPs()
{
return listManager.GetBlackList();
}
/// <summary>
/// Проверяет, заблокирован ли IP-адрес
/// </summary>
/// <param name="ipAddress">IP-адрес для проверки</param>
/// <returns>True если IP заблокирован</returns>
public bool IsBlocked(string ipAddress)
{
return listManager.IsInBlackList(ipAddress);
}
private static void Log(string msg)
{
Utils.Log("IPBlocker: " + msg);
}
}
}
3.6.2 AbuseReporter.cs — AbuseIPDB интеграция
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text;
using Newtonsoft.Json;
namespace Cameyo.RdpMon
{
/// <summary>
/// Модуль для интеграции с AbuseIPDB API
/// </summary>
public class AbuseReporter
{
private readonly string apiKey;
private readonly HttpClient httpClient;
private const string BASE_URL = "https://api.abuseipdb.com/api/v2";
// Категории AbuseIPDB
public static class Categories
{
public const int SSH_BRUTEFORCE = 18;
public const int BRUTE_FORCE = 18;
public const int BAD_WEB_BOT = 19;
public const int EXPLOITED_HOST = 20;
public const int WEB_APP_ATTACK = 21;
public const int SSH = 22;
}
public AbuseReporter(string abuseIPDBApiKey)
{
apiKey = abuseIPDBApiKey;
httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Key", apiKey);
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
}
/// <summary>
/// Проверяет репутацию IP-адреса в AbuseIPDB
/// </summary>
/// <param name="ipAddress">IP-адрес для проверки</param>
/// <param name="maxAgeInDays">Максимальный возраст отчётов (по умолчанию 90 дней)</param>
/// <returns>Информация о репутации IP</returns>
public async Task<AbuseIPDBCheckResponse> CheckIPAsync(string ipAddress, int maxAgeInDays = 90)
{
var logPrefix = $"CheckIP({ipAddress}): ";
try
{
var url = $"{BASE_URL}/check?ipAddress={ipAddress}&maxAgeInDays={maxAgeInDays}&verbose";
Log(logPrefix + "отправка запроса к AbuseIPDB");
var response = await httpClient.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var result = JsonConvert.DeserializeObject<AbuseIPDBCheckResponse>(content);
Log(logPrefix + $"получен ответ, confidence: {result.Data.AbuseConfidencePercentage}%");
return result;
}
else
{
Log(logPrefix + $"ошибка HTTP {response.StatusCode}: {content}");
return null;
}
}
catch (Exception ex)
{
Log(logPrefix + "исключение: " + ex.ToString());
return null;
}
}
/// <summary>
/// Отправляет сообщение о вредоносной активности IP-адреса
/// </summary>
/// <param name="ipAddress">IP-адрес злоумышленника</param>
/// <param name="categories">Категории атаки</param>
/// <param name="comment">Комментарий с деталями атаки</param>
/// <returns>True если сообщение успешно отправлено</returns>
public async Task<bool> ReportIPAsync(string ipAddress, int[] categories, string comment)
{
var logPrefix = $"ReportIP({ipAddress}): ";
try
{
var formData = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("ip", ipAddress),
new KeyValuePair<string, string>("categories", string.Join(",", categories)),
new KeyValuePair<string, string>("comment", comment)
};
Log(logPrefix + "отправка сообщения в AbuseIPDB");
var response = await httpClient.PostAsync($"{BASE_URL}/report",
new FormUrlEncodedContent(formData));
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
Log(logPrefix + "сообщение успешно отправлено");
return true;
}
else
{
Log(logPrefix + $"ошибка отправки HTTP {response.StatusCode}: {content}");
return false;
}
}
catch (Exception ex)
{
Log(logPrefix + "исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Автоматическая отправка сообщения о brute-force атаке
/// </summary>
/// <param name="addr">Информация об атакующем IP</param>
/// <returns>True если сообщение отправлено</returns>
public async Task<bool> ReportBruteForceAsync(Addr addr)
{
var comment = $"RDP brute-force attack detected by RdpMon. " +
$"Failed attempts: {addr.FailCount}, " +
$"Period: {addr.First:yyyy-MM-dd HH:mm} - {addr.Last:yyyy-MM-dd HH:mm} UTC, " +
$"Targeted users: {string.Join(", ", addr.UserNames)}";
var categories = new[] { Categories.BRUTE_FORCE, Categories.SSH };
return await ReportIPAsync(addr.AddrId, categories, comment);
}
/// <summary>
/// Получает чёрный список наиболее опасных IP от AbuseIPDB
/// </summary>
/// <param name="confidenceMinimum">Минимальный процент confidence (по умолчанию 90)</param>
/// <param name="limit">Количество записей (максимум 10000)</param>
/// <returns>Список IP-адресов</returns>
public async Task<AbuseIPDBBlacklistResponse> GetBlacklistAsync(int confidenceMinimum = 90, int limit = 1000)
{
var logPrefix = "GetBlacklist: ";
try
{
var url = $"{BASE_URL}/blacklist?confidenceMinimum={confidenceMinimum}&limit={limit}";
Log(logPrefix + $"запрос чёрного списка (confidence >= {confidenceMinimum}%)");
var response = await httpClient.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var result = JsonConvert.DeserializeObject<AbuseIPDBBlacklistResponse>(content);
Log(logPrefix + $"получено {result.Data.Count} записей");
return result;
}
else
{
Log(logPrefix + $"ошибка HTTP {response.StatusCode}: {content}");
return null;
}
}
catch (Exception ex)
{
Log(logPrefix + "исключение: " + ex.ToString());
return null;
}
}
/// <summary>
/// Проверяет корректность API ключа
/// </summary>
/// <returns>True если ключ валидный</returns>
public async Task<bool> TestConnectionAsync()
{
try
{
// Делаем тестовый запрос на проверку localhost
var result = await CheckIPAsync("127.0.0.1", 1);
return result != null;
}
catch
{
return false;
}
}
public void Dispose()
{
httpClient?.Dispose();
}
private static void Log(string msg)
{
Utils.Log("AbuseReporter: " + msg);
}
}
// Модели ответов AbuseIPDB
public class AbuseIPDBCheckResponse
{
public AbuseIPDBCheckData Data { get; set; }
}
public class AbuseIPDBCheckData
{
public string IpAddress { get; set; }
public bool IsPublic { get; set; }
public int IpVersion { get; set; }
public bool IsWhitelisted { get; set; }
public int AbuseConfidencePercentage { get; set; }
public string CountryCode { get; set; }
public string CountryName { get; set; }
public string UsageType { get; set; }
public string Isp { get; set; }
public string Domain { get; set; }
public int TotalReports { get; set; }
public int NumDistinctUsers { get; set; }
public DateTime? LastReportedAt { get; set; }
}
public class AbuseIPDBBlacklistResponse
{
public List<AbuseIPDBBlacklistItem> Data { get; set; }
}
public class AbuseIPDBBlacklistItem
{
public string IpAddress { get; set; }
public int AbuseConfidencePercentage { get; set; }
public string CountryCode { get; set; }
public DateTime LastReportedAt { get; set; }
}
}
3.6.3 ListManager.cs — Управление списками
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.IO;
using LiteDB;
namespace Cameyo.RdpMon
{
/// <summary>
/// Модуль для управления белыми и чёрными списками IP-адресов
/// </summary>
public class ListManager
{
private readonly string dbPath;
public ListManager()
{
dbPath = Utils.MyPath("RdpMon.db");
}
#region Белый список
/// <summary>
/// Добавляет IP-адрес в белый список
/// </summary>
/// <param name="ipAddress">IP-адрес или CIDR диапазон</param>
/// <param name="description">Описание</param>
/// <param name="addedBy">Кто добавил</param>
/// <returns>True если успешно добавлено</returns>
public bool AddToWhiteList(string ipAddress, string description = "", string addedBy = "System")
{
var logPrefix = $"AddToWhiteList({ipAddress}): ";
try
{
if (!IsValidIPOrCIDR(ipAddress))
{
Log(logPrefix + "некорректный IP-адрес или CIDR");
return false;
}
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var collection = db.GetCollection<WhiteListEntry>("WhiteList");
// Проверяем, не существует ли уже
if (collection.FindOne(x => x.IPAddress == ipAddress) != null)
{
Log(logPrefix + "IP уже в белом списке");
return false;
}
var entry = new WhiteListEntry
{
Id = Guid.NewGuid().ToString(),
IPAddress = ipAddress,
Description = description,
Added = DateTime.UtcNow,
AddedBy = addedBy,
IsRange = ipAddress.Contains("/"),
CIDR = ipAddress.Contains("/") ? ipAddress : null
};
collection.Insert(entry);
Log(logPrefix + "добавлено в белый список");
return true;
}
}
catch (Exception ex)
{
Log(logPrefix + "исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Удаляет IP-адрес из белого списка
/// </summary>
/// <param name="ipAddress">IP-адрес для удаления</param>
/// <returns>True если успешно удалено</returns>
public bool RemoveFromWhiteList(string ipAddress)
{
try
{
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var collection = db.GetCollection<WhiteListEntry>("WhiteList");
var deleted = collection.Delete(x => x.IPAddress == ipAddress);
Log($"RemoveFromWhiteList({ipAddress}): удалено {deleted} записей");
return deleted > 0;
}
}
catch (Exception ex)
{
Log($"RemoveFromWhiteList({ipAddress}): исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Проверяет, находится ли IP в белом списке
/// </summary>
/// <param name="ipAddress">IP-адрес для проверки</param>
/// <returns>True если IP в белом списке</returns>
public bool IsInWhiteList(string ipAddress)
{
try
{
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var collection = db.GetCollection<WhiteListEntry>("WhiteList");
var entries = collection.FindAll().ToList();
foreach (var entry in entries)
{
if (entry.IsRange)
{
// Проверка CIDR диапазона
if (IsIPInCIDR(ipAddress, entry.CIDR))
return true;
}
else
{
// Точное совпадение IP
if (entry.IPAddress == ipAddress)
return true;
}
}
return false;
}
}
catch (Exception ex)
{
Log($"IsInWhiteList({ipAddress}): исключение: " + ex.ToString());
return false; // В случае ошибки считаем, что IP не в белом списке
}
}
/// <summary>
/// Получает все записи белого списка
/// </summary>
/// <returns>Список записей белого списка</returns>
public List<WhiteListEntry> GetWhiteList()
{
try
{
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var collection = db.GetCollection<WhiteListEntry>("WhiteList");
return collection.FindAll().OrderBy(x => x.Added).ToList();
}
}
catch (Exception ex)
{
Log("GetWhiteList: исключение: " + ex.ToString());
return new List<WhiteListEntry>();
}
}
#endregion
#region Чёрный список
/// <summary>
/// Добавляет IP-адрес в чёрный список
/// </summary>
/// <param name="entry">Запись чёрного списка</param>
/// <returns>True если успешно добавлено</returns>
public bool AddToBlackList(BlackListEntry entry)
{
var logPrefix = $"AddToBlackList({entry.IPAddress}): ";
try
{
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var collection = db.GetCollection<BlackListEntry>("BlackList");
// Проверяем, не существует ли уже
var existing = collection.FindOne(x => x.IPAddress == entry.IPAddress);
if (existing != null)
{
// Обновляем существующую запись
existing.FailuresCount = entry.FailuresCount;
existing.Reason = entry.Reason;
existing.FirewallRuleCreated = entry.FirewallRuleCreated;
existing.ReportedToAbuseIPDB = entry.ReportedToAbuseIPDB;
collection.Update(existing);
Log(logPrefix + "обновлена существующая запись");
}
else
{
collection.Insert(entry);
Log(logPrefix + "добавлено в чёрный список");
}
return true;
}
}
catch (Exception ex)
{
Log(logPrefix + "исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Удаляет IP-адрес из чёрного списка
/// </summary>
/// <param name="ipAddress">IP-адрес для удаления</param>
/// <returns>True если успешно удалено</returns>
public bool RemoveFromBlackList(string ipAddress)
{
try
{
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var collection = db.GetCollection<BlackListEntry>("BlackList");
var deleted = collection.Delete(x => x.IPAddress == ipAddress);
Log($"RemoveFromBlackList({ipAddress}): удалено {deleted} записей");
return deleted > 0;
}
}
catch (Exception ex)
{
Log($"RemoveFromBlackList({ipAddress}): исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Проверяет, находится ли IP в чёрном списке
/// </summary>
/// <param name="ipAddress">IP-адрес для проверки</param>
/// <returns>True если IP в чёрном списке</returns>
public bool IsInBlackList(string ipAddress)
{
try
{
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var collection = db.GetCollection<BlackListEntry>("BlackList");
return collection.FindOne(x => x.IPAddress == ipAddress) != null;
}
}
catch (Exception ex)
{
Log($"IsInBlackList({ipAddress}): исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Получает все записи чёрного списка
/// </summary>
/// <returns>Список записей чёрного списка</returns>
public List<BlackListEntry> GetBlackList()
{
try
{
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var collection = db.GetCollection<BlackListEntry>("BlackList");
return collection.FindAll().OrderByDescending(x => x.BlockedAt).ToList();
}
}
catch (Exception ex)
{
Log("GetBlackList: исключение: " + ex.ToString());
return new List<BlackListEntry>();
}
}
/// <summary>
/// Отмечает IP как отправленный в AbuseIPDB
/// </summary>
/// <param name="ipAddress">IP-адрес</param>
/// <returns>True если успешно обновлено</returns>
public bool MarkAsReportedToAbuseIPDB(string ipAddress)
{
try
{
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var collection = db.GetCollection<BlackListEntry>("BlackList");
var entry = collection.FindOne(x => x.IPAddress == ipAddress);
if (entry != null)
{
entry.ReportedToAbuseIPDB = true;
collection.Update(entry);
return true;
}
return false;
}
}
catch (Exception ex)
{
Log($"MarkAsReportedToAbuseIPDB({ipAddress}): исключение: " + ex.ToString());
return false;
}
}
#endregion
#region Импорт/Экспорт
/// <summary>
/// Импортирует белый список из файла
/// </summary>
/// <param name="filePath">Путь к файлу (один IP на строку)</param>
/// <param name="description">Описание для всех импортируемых IP</param>
/// <returns>Количество импортированных IP</returns>
public int ImportWhiteListFromFile(string filePath, string description = "Импортировано из файла")
{
var imported = 0;
try
{
var lines = File.ReadAllLines(filePath);
foreach (var line in lines)
{
var ip = line.Trim();
if (!string.IsNullOrEmpty(ip) && !ip.StartsWith("#") && IsValidIPOrCIDR(ip))
{
if (AddToWhiteList(ip, description, "Import"))
imported++;
}
}
Log($"ImportWhiteListFromFile: импортировано {imported} IP из {lines.Length} строк");
}
catch (Exception ex)
{
Log("ImportWhiteListFromFile: исключение: " + ex.ToString());
}
return imported;
}
/// <summary>
/// Экспортирует белый список в файл
/// </summary>
/// <param name="filePath">Путь к файлу для экспорта</param>
/// <returns>True если успешно экспортировано</returns>
public bool ExportWhiteListToFile(string filePath)
{
try
{
var entries = GetWhiteList();
var lines = new List<string>
{
"# RdpMon White List Export",
"# Generated: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
"# Format: IP_ADDRESS [CIDR] # DESCRIPTION",
""
};
foreach (var entry in entries)
{
var line = entry.IPAddress;
if (!string.IsNullOrEmpty(entry.Description))
line += " # " + entry.Description;
lines.Add(line);
}
File.WriteAllLines(filePath, lines);
Log($"ExportWhiteListToFile: экспортировано {entries.Count} записей в {filePath}");
return true;
}
catch (Exception ex)
{
Log("ExportWhiteListToFile: исключение: " + ex.ToString());
return false;
}
}
#endregion
#region Утилиты
/// <summary>
/// Проверяет, является ли строка корректным IP-адресом или CIDR диапазоном
/// </summary>
/// <param name="ipOrCidr">IP-адрес или CIDR</param>
/// <returns>True если корректный</returns>
private bool IsValidIPOrCIDR(string ipOrCidr)
{
if (string.IsNullOrEmpty(ipOrCidr))
return false;
// Проверка CIDR
if (ipOrCidr.Contains("/"))
{
var parts = ipOrCidr.Split('/');
if (parts.Length != 2)
return false;
if (!IPAddress.TryParse(parts[^0], out _))
return false;
if (!int.TryParse(parts[^1], out int prefix) || prefix < 0 || prefix > 32)
return false;
return true;
}
// Проверка IP
return IPAddress.TryParse(ipOrCidr, out _);
}
/// <summary>
/// Проверяет, входит ли IP-адрес в CIDR диапазон
/// </summary>
/// <param name="ipAddress">IP-адрес</param>
/// <param name="cidr">CIDR диапазон (например, 192.168.1.0/24)</param>
/// <returns>True если IP входит в диапазон</returns>
private bool IsIPInCIDR(string ipAddress, string cidr)
{
try
{
var parts = cidr.Split('/');
var networkIP = IPAddress.Parse(parts[^0]);
var prefixLength = int.Parse(parts[^1]);
var checkIP = IPAddress.Parse(ipAddress);
var networkBytes = networkIP.GetAddressBytes();
var checkBytes = checkIP.GetAddressBytes();
if (networkBytes.Length != checkBytes.Length)
return false;
var bytesToCheck = prefixLength / 8;
var bitsToCheck = prefixLength % 8;
// Проверяем полные байты
for (int i = 0; i < bytesToCheck; i++)
{
if (networkBytes[i] != checkBytes[i])
return false;
}
// Проверяем оставшиеся биты
if (bitsToCheck > 0)
{
var mask = (byte)(0xFF << (8 - bitsToCheck));
if ((networkBytes[bytesToCheck] & mask) != (checkBytes[bytesToCheck] & mask))
return false;
}
return true;
}
catch
{
return false;
}
}
private static void Log(string msg)
{
Utils.Log("ListManager: " + msg);
}
#endregion
}
}
3.6.4 SettingsManager.cs — Настройки
using System;
using LiteDB;
namespace Cameyo.RdpMon
{
/// <summary>
/// Модуль для управления настройками приложения
/// </summary>
public class SettingsManager
{
private readonly string dbPath;
private const string SETTINGS_ID = "MainSettings";
public SettingsManager()
{
dbPath = Utils.MyPath("RdpMon.db");
InitializeDefaultSettings();
}
/// <summary>
/// Получает текущие настройки
/// </summary>
/// <returns>Объект настроек</returns>
public RdpMonSettings GetSettings()
{
try
{
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var collection = db.GetCollection<RdpMonSettings>("Settings");
var settings = collection.FindById(SETTINGS_ID);
if (settings == null)
{
// Создаём настройки по умолчанию
settings = CreateDefaultSettings();
collection.Insert(settings);
}
return settings;
}
}
catch (Exception ex)
{
Log("GetSettings: исключение: " + ex.ToString());
return CreateDefaultSettings();
}
}
/// <summary>
/// Сохраняет настройки
/// </summary>
/// <param name="settings">Настройки для сохранения</param>
/// <returns>True если успешно сохранено</returns>
public bool SaveSettings(RdpMonSettings settings)
{
try
{
settings.SettingId = SETTINGS_ID;
settings.LastModified = DateTime.UtcNow;
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var collection = db.GetCollection<RdpMonSettings>("Settings");
collection.Upsert(settings);
Log("SaveSettings: настройки успешно сохранены");
return true;
}
}
catch (Exception ex)
{
Log("SaveSettings: исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Получает конкретный параметр настроек
/// </summary>
/// <typeparam name="T">Тип параметра</typeparam>
/// <param name="parameterName">Имя параметра</param>
/// <param name="defaultValue">Значение по умолчанию</param>
/// <returns>Значение параметра</returns>
public T GetSetting<T>(string parameterName, T defaultValue)
{
try
{
var settings = GetSettings();
var property = typeof(RdpMonSettings).GetProperty(parameterName);
if (property != null)
{
var value = property.GetValue(settings);
if (value is T)
return (T)value;
}
return defaultValue;
}
catch (Exception ex)
{
Log($"GetSetting({parameterName}): исключение: " + ex.ToString());
return defaultValue;
}
}
/// <summary>
/// Устанавливает конкретный параметр настроек
/// </summary>
/// <typeparam name="T">Тип параметра</typeparam>
/// <param name="parameterName">Имя параметра</param>
/// <param name="value">Новое значение</param>
/// <returns>True если успешно установлено</returns>
public bool SetSetting<T>(string parameterName, T value)
{
try
{
var settings = GetSettings();
var property = typeof(RdpMonSettings).GetProperty(parameterName);
if (property != null && property.CanWrite)
{
property.SetValue(settings, value);
return SaveSettings(settings);
}
return false;
}
catch (Exception ex)
{
Log($"SetSetting({parameterName}): исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Сбрасывает настройки к значениям по умолчанию
/// </summary>
/// <returns>True если успешно сброшено</returns>
public bool ResetToDefaults()
{
try
{
var defaultSettings = CreateDefaultSettings();
return SaveSettings(defaultSettings);
}
catch (Exception ex)
{
Log("ResetToDefaults: исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Проверяет валидность настроек
/// </summary>
/// <param name="settings">Настройки для проверки</param>
/// <returns>Результат валидации</returns>
public SettingsValidationResult ValidateSettings(RdpMonSettings settings)
{
var result = new SettingsValidationResult { IsValid = true };
// Проверка количества попыток для блокировки
if (settings.FailuresForBlocking < 1 || settings.FailuresForBlocking > 1000)
{
result.IsValid = false;
result.Errors.Add("Количество попыток для блокировки должно быть от 1 до 1000");
}
// Проверка количества попыток для AbuseIPDB
if (settings.FailuresForAbuseReport < settings.FailuresForBlocking)
{
result.IsValid = false;
result.Errors.Add("Количество попыток для отправки в AbuseIPDB должно быть не меньше количества для блокировки");
}
// Проверка порога confidence
if (settings.AbuseConfidenceThreshold < 1 || settings.AbuseConfidenceThreshold > 100)
{
result.IsValid = false;
result.Errors.Add("Порог Confidence of Abuse должен быть от 1% до 100%");
}
// Проверка API ключа AbuseIPDB (если включена интеграция)
if (settings.EnableAbuseIPDBCheck && string.IsNullOrWhiteSpace(settings.AbuseIPDBApiKey))
{
result.IsValid = false;
result.Errors.Add("API ключ AbuseIPDB обязателен при включённой интеграции");
}
// Проверка порога для автоматической проверки
if (settings.CheckFailuresThreshold < 1 || settings.CheckFailuresThreshold > settings.FailuresForBlocking)
{
result.IsValid = false;
result.Errors.Add("Порог проверки в AbuseIPDB должен быть от 1 до значения блокировки");
}
return result;
}
/// <summary>
/// Экспортирует настройки в JSON файл
/// </summary>
/// <param name="filePath">Путь к файлу</param>
/// <returns>True если успешно экспортировано</returns>
public bool ExportSettings(string filePath)
{
try
{
var settings = GetSettings();
var json = Newtonsoft.Json.JsonConvert.SerializeObject(settings, Newtonsoft.Json.Formatting.Indented);
System.IO.File.WriteAllText(filePath, json);
Log($"ExportSettings: настройки экспортированы в {filePath}");
return true;
}
catch (Exception ex)
{
Log("ExportSettings: исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Импортирует настройки из JSON файла
/// </summary>
/// <param name="filePath">Путь к файлу</param>
/// <returns>True если успешно импортировано</returns>
public bool ImportSettings(string filePath)
{
try
{
var json = System.IO.File.ReadAllText(filePath);
var settings = Newtonsoft.Json.JsonConvert.DeserializeObject<RdpMonSettings>(json);
var validation = ValidateSettings(settings);
if (!validation.IsValid)
{
Log("ImportSettings: импортированные настройки не прошли валидацию: " +
string.Join(", ", validation.Errors));
return false;
}
var saved = SaveSettings(settings);
if (saved)
Log($"ImportSettings: настройки импортированы из {filePath}");
return saved;
}
catch (Exception ex)
{
Log("ImportSettings: исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Инициализирует настройки по умолчанию при первом запуске
/// </summary>
private void InitializeDefaultSettings()
{
try
{
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var collection = db.GetCollection<RdpMonSettings>("Settings");
var existing = collection.FindById(SETTINGS_ID);
if (existing == null)
{
var defaultSettings = CreateDefaultSettings();
collection.Insert(defaultSettings);
Log("InitializeDefaultSettings: созданы настройки по умолчанию");
}
}
}
catch (Exception ex)
{
Log("InitializeDefaultSettings: исключение: " + ex.ToString());
}
}
/// <summary>
/// Создаёт объект настроек с значениями по умолчанию
/// </summary>
/// <returns>Настройки по умолчанию</returns>
private RdpMonSettings CreateDefaultSettings()
{
return new RdpMonSettings
{
SettingId = SETTINGS_ID,
FailuresForBlocking = 10,
FailuresForAbuseReport = 20,
AbuseIPDBApiKey = "",
AbuseConfidenceThreshold = 75,
EnableAutoBlocking = true,
EnableAbuseIPDBCheck = false,
CheckFailuresThreshold = 5,
EnableAutoReport = false,
LastModified = DateTime.UtcNow
};
}
private static void Log(string msg)
{
Utils.Log("SettingsManager: " + msg);
}
}
/// <summary>
/// Класс настроек RdpMon
/// </summary>
public class RdpMonSettings
{
[BsonId]
public string SettingId { get; set; }
/// <summary>
/// Количество неудачных попыток для автоматической блокировки IP
/// </summary>
public int FailuresForBlocking { get; set; } = 10;
/// <summary>
/// Количество неудачных попыток для отправки сообщения в AbuseIPDB
/// </summary>
public int FailuresForAbuseReport { get; set; } = 20;
/// <summary>
/// API ключ для AbuseIPDB
/// </summary>
public string AbuseIPDBApiKey { get; set; } = "";
/// <summary>
/// Порог Confidence of Abuse % для классификации IP как вредоносный
/// </summary>
public int AbuseConfidenceThreshold { get; set; } = 75;
/// <summary>
/// Включить автоматическую блокировку IP
/// </summary>
public bool EnableAutoBlocking { get; set; } = true;
/// <summary>
/// Включить проверку IP в AbuseIPDB
/// </summary>
public bool EnableAbuseIPDBCheck { get; set; } = false;
/// <summary>
/// Количество неудачных попыток для автоматической проверки в AbuseIPDB
/// </summary>
public int CheckFailuresThreshold { get; set; } = 5;
/// <summary>
/// Включить автоматическую отправку сообщений в AbuseIPDB
/// </summary>
public bool EnableAutoReport { get; set; } = false;
/// <summary>
/// Время последнего изменения настроек
/// </summary>
public DateTime LastModified { get; set; }
}
/// <summary>
/// Результат валидации настроек
/// </summary>
public class SettingsValidationResult
{
public bool IsValid { get; set; }
public System.Collections.Generic.List<string> Errors { get; set; } =
new System.Collections.Generic.List<string>();
}
}
3.6.5 ReportsManager.cs — Отчёты
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using LiteDB;
namespace Cameyo.RdpMon
{
/// <summary>
/// Модуль для генерации отчётов по безопасности RDP
/// </summary>
public class ReportsManager
{
private readonly string dbPath;
public ReportsManager()
{
dbPath = Utils.MyPath("RdpMon.db");
}
/// <summary>
/// Генерирует сводный отчёт по атакам за указанный период
/// </summary>
/// <param name="fromDate">Начальная дата</param>
/// <param name="toDate">Конечная дата</param>
/// <returns>Сводный отчёт</returns>
public AttackSummaryReport GenerateAttackSummary(DateTime fromDate, DateTime toDate)
{
var report = new AttackSummaryReport
{
PeriodStart = fromDate,
PeriodEnd = toDate,
GeneratedAt = DateTime.UtcNow
};
try
{
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var addrCollection = db.GetCollection<Addr>("Addr");
// Получаем все адреса с активностью в указанном периоде
var addresses = addrCollection.Find(x =>
x.Last >= fromDate && x.First <= toDate).ToList();
// Анализ по типам
var attacks = addresses.Where(x => x.IsAttack()).ToList();
var legitConnections = addresses.Where(x => x.IsLegit()).ToList();
var suspiciousConnections = addresses.Where(x => !x.IsAttack() && !x.IsLegit()).ToList();
// Заполняем основные метрики
report.TotalUniqueIPs = addresses.Count;
report.AttackingIPs = attacks.Count;
report.LegitimateIPs = legitConnections.Count;
report.SuspiciousIPs = suspiciousConnections.Count;
report.TotalFailedAttempts = attacks.Sum(x => x.FailCount);
report.TotalSuccessfulConnections = legitConnections.Sum(x => x.SuccessCount);
// Топ атакующих IP
report.TopAttackingIPs = attacks
.OrderByDescending(x => x.FailCount)
.Take(10)
.Select(x => new TopAttackerInfo
{
IPAddress = x.AddrId,
FailedAttempts = x.FailCount,
FirstSeen = x.First,
LastSeen = x.Last,
TargetedUsers = x.UserNames.ToList()
})
.ToList();
// Анализ по дням
report.DailyStats = GenerateDailyStats(fromDate, toDate, addresses);
// Анализ по часам
report.HourlyDistribution = GenerateHourlyDistribution(attacks);
Log($"GenerateAttackSummary: сгенерирован отчёт за период {fromDate:yyyy-MM-dd} - {toDate:yyyy-MM-dd}");
}
}
catch (Exception ex)
{
Log("GenerateAttackSummary: исключение: " + ex.ToString());
}
return report;
}
/// <summary>
/// Генерирует отчёт по заблокированным IP-адресам
/// </summary>
/// <param name="fromDate">Начальная дата</param>
/// <param name="toDate">Конечная дата</param>
/// <returns>Отчёт по блокировкам</returns>
public BlockedIPsReport GenerateBlockedIPsReport(DateTime fromDate, DateTime toDate)
{
var report = new BlockedIPsReport
{
PeriodStart = fromDate,
PeriodEnd = toDate,
GeneratedAt = DateTime.UtcNow
};
try
{
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var blacklistCollection = db.GetCollection<BlackListEntry>("BlackList");
var blockedEntries = blacklistCollection.Find(x =>
x.BlockedAt >= fromDate && x.BlockedAt <= toDate).ToList();
report.TotalBlockedIPs = blockedEntries.Count;
report.AutoBlockedIPs = blockedEntries.Count(x => x.AutoBlocked);
report.ManuallyBlockedIPs = blockedEntries.Count(x => !x.AutoBlocked);
report.ReportedToAbuseIPDB = blockedEntries.Count(x => x.ReportedToAbuseIPDB);
report.BlockedIPs = blockedEntries
.OrderByDescending(x => x.BlockedAt)
.Select(x => new BlockedIPInfo
{
IPAddress = x.IPAddress,
BlockedAt = x.BlockedAt,
Reason = x.Reason,
FailuresCount = x.FailuresCount,
AutoBlocked = x.AutoBlocked,
ReportedToAbuseIPDB = x.ReportedToAbuseIPDB
})
.ToList();
Log($"GenerateBlockedIPsReport: сгенерирован отчёт по {blockedEntries.Count} заблокированным IP");
}
}
catch (Exception ex)
{
Log("GenerateBlockedIPsReport: исключение: " + ex.ToString());
}
return report;
}
/// <summary>
/// Генерирует отчёт по пользовательским сессиям
/// </summary>
/// <param name="fromDate">Начальная дата</param>
/// <param name="toDate">Конечная дата</param>
/// <returns>Отчёт по сессиям</returns>
public SessionsReport GenerateSessionsReport(DateTime fromDate, DateTime toDate)
{
var report = new SessionsReport
{
PeriodStart = fromDate,
PeriodEnd = toDate,
GeneratedAt = DateTime.UtcNow
};
try
{
using (var db = new LiteDatabase($"Filename={dbPath};utc=true"))
{
var sessionCollection = db.GetCollection<Session>("Session");
var sessions = sessionCollection.Find(x =>
x.Start >= fromDate && x.Start <= toDate).ToList();
report.TotalSessions = sessions.Count;
report.ActiveSessions = sessions.Count(x => x.End == null);
report.CompletedSessions = sessions.Count(x => x.End != null);
// Анализ по пользователям
var userStats = sessions
.GroupBy(x => x.User)
.Select(g => new UserSessionInfo
{
UserName = g.Key,
SessionCount = g.Count(),
TotalDuration = g.Where(s => s.End != null)
.Sum(s => (s.End.Value - s.Start).TotalMinutes),
UniqueIPs = g.Select(s => s.Addr).Distinct().Count()
})
.OrderByDescending(x => x.SessionCount)
.ToList();
report.UserStats = userStats;
// Анализ по IP-адресам
var ipStats = sessions
.Where(x => !string.IsNullOrEmpty(x.Addr))
.GroupBy(x => x.Addr)
.Select(g => new IPSessionInfo
{
IPAddress = g.Key,
SessionCount = g.Count(),
UniqueUsers = g.Select(s => s.User).Distinct().Count(),
FirstConnection = g.Min(s => s.Start),
LastConnection = g.Max(s => s.Start)
})
.OrderByDescending(x => x.SessionCount)
.ToList();
report.IPStats = ipStats;
Log($"GenerateSessionsReport: сгенерирован отчёт по {sessions.Count} сессиям");
}
}
catch (Exception ex)
{
Log("GenerateSessionsReport: исключение: " + ex.ToString());
}
return report;
}
/// <summary>
/// Экспортирует отчёт в CSV формат
/// </summary>
/// <param name="report">Отчёт для экспорта</param>
/// <param name="filePath">Путь к файлу</param>
/// <returns>True если успешно экспортировано</returns>
public bool ExportToCSV(AttackSummaryReport report, string filePath)
{
try
{
var csv = new StringBuilder();
// Заголовок
csv.AppendLine("RdpMon Attack Summary Report");
csv.AppendLine($"Period: {report.PeriodStart:yyyy-MM-dd} - {report.PeriodEnd:yyyy-MM-dd}");
csv.AppendLine($"Generated: {report.GeneratedAt:yyyy-MM-dd HH:mm:ss} UTC");
csv.AppendLine();
// Сводная статистика
csv.AppendLine("Summary Statistics");
csv.AppendLine("Metric,Value");
csv.AppendLine($"Total Unique IPs,{report.TotalUniqueIPs}");
csv.AppendLine($"Attacking IPs,{report.AttackingIPs}");
csv.AppendLine($"Legitimate IPs,{report.LegitimateIPs}");
csv.AppendLine($"Suspicious IPs,{report.SuspiciousIPs}");
csv.AppendLine($"Total Failed Attempts,{report.TotalFailedAttempts}");
csv.AppendLine($"Total Successful Connections,{report.TotalSuccessfulConnections}");
csv.AppendLine();
// Топ атакующих IP
csv.AppendLine("Top Attacking IPs");
csv.AppendLine("Rank,IP Address,Failed Attempts,First Seen,Last Seen,Targeted Users");
for (int i = 0; i < report.TopAttackingIPs.Count; i++)
{
var attacker = report.TopAttackingIPs[i];
csv.AppendLine($"{i + 1},{attacker.IPAddress},{attacker.FailedAttempts}," +
$"{attacker.FirstSeen:yyyy-MM-dd HH:mm},{attacker.LastSeen:yyyy-MM-dd HH:mm}," +
$"\"{string.Join("; ", attacker.TargetedUsers)}\"");
}
csv.AppendLine();
// Статистика по дням
csv.AppendLine("Daily Statistics");
csv.AppendLine("Date,Unique IPs,Attacks,Failed Attempts,Successful Connections");
foreach (var day in report.DailyStats)
{
csv.AppendLine($"{day.Date:yyyy-MM-dd},{day.UniqueIPs},{day.AttackingIPs}," +
$"{day.FailedAttempts},{day.SuccessfulConnections}");
}
File.WriteAllText(filePath, csv.ToString(), Encoding.UTF8);
Log($"ExportToCSV: отчёт экспортирован в {filePath}");
return true;
}
catch (Exception ex)
{
Log("ExportToCSV: исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Экспортирует отчёт в HTML формат
/// </summary>
/// <param name="report">Отчёт для экспорта</param>
/// <param name="filePath">Путь к файлу</param>
/// <returns>True если успешно экспортировано</returns>
public bool ExportToHTML(AttackSummaryReport report, string filePath)
{
try
{
var html = new StringBuilder();
html.AppendLine("<!DOCTYPE html>");
html.AppendLine("<html>");
html.AppendLine("<head>");
html.AppendLine("<meta charset='utf-8'>");
html.AppendLine("<title>RdpMon Attack Summary Report</title>");
html.AppendLine("<style>");
html.AppendLine("body { font-family: Arial, sans-serif; margin: 20px; }");
html.AppendLine("table { border-collapse: collapse; width: 100%; margin: 20px 0; }");
html.AppendLine("th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }");
html.AppendLine("th { background-color: #f2f2f2; }");
html.AppendLine(".summary { background-color: #f9f9f9; padding: 15px; margin: 20px 0; }");
html.AppendLine(".red { color: #d9534f; }");
html.AppendLine(".green { color: #5cb85c; }");
html.AppendLine(".orange { color: #f0ad4e; }");
html.AppendLine("</style>");
html.AppendLine("</head>");
html.AppendLine("<body>");
// Заголовок
html.AppendLine("<h1>RdpMon Attack Summary Report</h1>");
html.AppendLine($"<p><strong>Period:</strong> {report.PeriodStart:yyyy-MM-dd} - {report.PeriodEnd:yyyy-MM-dd}</p>");
html.AppendLine($"<p><strong>Generated:</strong> {report.GeneratedAt:yyyy-MM-dd HH:mm:ss} UTC</p>");
// Сводная статистика
html.AppendLine("<div class='summary'>");
html.AppendLine("<h2>Summary Statistics</h2>");
html.AppendLine("<table>");
html.AppendLine("<tr><th>Metric</th><th>Value</th></tr>");
html.AppendLine($"<tr><td>Total Unique IPs</td><td>{report.TotalUniqueIPs}</td></tr>");
html.AppendLine($"<tr><td>Attacking IPs</td><td class='red'>{report.AttackingIPs}</td></tr>");
html.AppendLine($"<tr><td>Legitimate IPs</td><td class='green'>{report.LegitimateIPs}</td></tr>");
html.AppendLine($"<tr><td>Suspicious IPs</td><td class='orange'>{report.SuspiciousIPs}</td></tr>");
html.AppendLine($"<tr><td>Total Failed Attempts</td><td class='red'>{report.TotalFailedAttempts}</td></tr>");
html.AppendLine($"<tr><td>Total Successful Connections</td><td class='green'>{report.TotalSuccessfulConnections}</td></tr>");
html.AppendLine("</table>");
html.AppendLine("</div>");
// Топ атакующих IP
html.AppendLine("<h2>Top Attacking IPs</h2>");
html.AppendLine("<table>");
html.AppendLine("<tr><th>Rank</th><th>IP Address</th><th>Failed Attempts</th><th>First Seen</th><th>Last Seen</th><th>Targeted Users</th></tr>");
for (int i = 0; i < report.TopAttackingIPs.Count; i++)
{
var attacker = report.TopAttackingIPs[i];
html.AppendLine($"<tr>");
html.AppendLine($"<td>{i + 1}</td>");
html.AppendLine($"<td>{attacker.IPAddress}</td>");
html.AppendLine($"<td class='red'>{attacker.FailedAttempts}</td>");
html.AppendLine($"<td>{attacker.FirstSeen:yyyy-MM-dd HH:mm}</td>");
html.AppendLine($"<td>{attacker.LastSeen:yyyy-MM-dd HH:mm}</td>");
html.AppendLine($"<td>{string.Join(", ", attacker.TargetedUsers)}</td>");
html.AppendLine($"</tr>");
}
html.AppendLine("</table>");
html.AppendLine("</body>");
html.AppendLine("</html>");
File.WriteAllText(filePath, html.ToString(), Encoding.UTF8);
Log($"ExportToHTML: отчёт экспортирован в {filePath}");
return true;
}
catch (Exception ex)
{
Log("ExportToHTML: исключение: " + ex.ToString());
return false;
}
}
/// <summary>
/// Генерирует статистику по дням
/// </summary>
private List<DayStats> GenerateDailyStats(DateTime fromDate, DateTime toDate, List<Addr> addresses)
{
var dailyStats = new List<DayStats>();
for (var date = fromDate.Date; date <= toDate.Date; date = date.AddDays(1))
{
var dayAddresses = addresses.Where(x =>
x.Last.Date == date || (x.First.Date <= date && x.Last.Date >= date)).ToList();
var attacks = dayAddresses.Where(x => x.IsAttack()).ToList();
var legitConnections = dayAddresses.Where(x => x.IsLegit()).ToList();
dailyStats.Add(new DayStats
{
Date = date,
UniqueIPs = dayAddresses.Count,
AttackingIPs = attacks.Count,
FailedAttempts = attacks.Sum(x => x.FailCount),
SuccessfulConnections = legitConnections.Sum(x => x.SuccessCount)
});
}
return dailyStats;
}
/// <summary>
/// Генерирует распределение атак по часам
/// </summary>
private List<HourStats> GenerateHourlyDistribution(List<Addr> attacks)
{
var hourlyStats = new List<HourStats>();
for (int hour = 0; hour < 24; hour++)
{
var hourAttacks = attacks.Where(x => x.Last.Hour == hour).ToList();
hourlyStats.Add(new HourStats
{
Hour = hour,
AttackCount = hourAttacks.Count,
TotalFailedAttempts = hourAttacks.Sum(x => x.FailCount)
});
}
return hourlyStats;
}
private static void Log(string msg)
{
Utils.Log("ReportsManager: " + msg);
}
}
#region Модели отчётов
/// <summary>
/// Сводный отчёт по атакам
/// </summary>
public class AttackSummaryReport
{
public DateTime PeriodStart { get; set; }
public DateTime PeriodEnd { get; set; }
public DateTime GeneratedAt { get; set; }
public int TotalUniqueIPs { get; set; }
public int AttackingIPs { get; set; }
public int LegitimateIPs { get; set; }
public int SuspiciousIPs { get; set; }
public long TotalFailedAttempts { get; set; }
public long TotalSuccessfulConnections { get; set; }
public List<TopAttackerInfo> TopAttackingIPs { get; set; } = new List<TopAttackerInfo>();
public List<DayStats> DailyStats { get; set; } = new List<DayStats>();
public List<HourStats> HourlyDistribution { get; set; } = new List<HourStats>();
}
/// <summary>
/// Информация о топ атакующем IP
/// </summary>
public class TopAttackerInfo
{
public string IPAddress { get; set; }
public int FailedAttempts { get; set; }
public DateTime FirstSeen { get; set; }
public DateTime LastSeen { get; set; }
public List<string> TargetedUsers { get; set; } = new List<string>();
}
/// <summary>
/// Статистика за день
/// </summary>
public class DayStats
{
public DateTime Date { get; set; }
public int UniqueIPs { get; set; }
public int AttackingIPs { get; set; }
public long FailedAttempts { get; set; }
public long SuccessfulConnections { get; set; }
}
/// <summary>
/// Статистика за час
/// </summary>
public class HourStats
{
public int Hour { get; set; }
public int AttackCount { get; set; }
public long TotalFailedAttempts { get; set; }
}
/// <summary>
/// Отчёт по заблокированным IP
/// </summary>
public class BlockedIPsReport
{
public DateTime PeriodStart { get; set; }
public DateTime PeriodEnd { get; set; }
public DateTime GeneratedAt { get; set; }
public int TotalBlockedIPs { get; set; }
public int AutoBlockedIPs { get; set; }
public int ManuallyBlockedIPs { get; set; }
public int ReportedToAbuseIPDB { get; set; }
public List<BlockedIPInfo> BlockedIPs { get; set; } = new List<BlockedIPInfo>();
}
/// <summary>
/// Информация о заблокированном IP
/// </summary>
public class BlockedIPInfo
{
public string IPAddress { get; set; }
public DateTime BlockedAt { get; set; }
public string Reason { get; set; }
public int FailuresCount { get; set; }
public bool AutoBlocked { get; set; }
public bool ReportedToAbuseIPDB { get; set; }
}
/// <summary>
/// Отчёт по сессиям
/// </summary>
public class SessionsReport
{
public DateTime PeriodStart { get; set; }
public DateTime PeriodEnd { get; set; }
public DateTime GeneratedAt { get; set; }
public int TotalSessions { get; set; }
public int ActiveSessions { get; set; }
public int CompletedSessions { get; set; }
public List<UserSessionInfo> UserStats { get; set; } = new List<UserSessionInfo>();
public List<IPSessionInfo> IPStats { get; set; } = new List<IPSessionInfo>();
}
/// <summary>
/// Информация о сессиях пользователя
/// </summary>
public class UserSessionInfo
{
public string UserName { get; set; }
public int SessionCount { get; set; }
public double TotalDuration { get; set; }
public int UniqueIPs { get; set; }
}
/// <summary>
/// Информация о сессиях с IP
/// </summary>
public class IPSessionInfo
{
public string IPAddress { get; set; }
public int SessionCount { get; set; }
public int UniqueUsers { get; set; }
public DateTime FirstConnection { get; set; }
public DateTime LastConnection { get; set; }
}
#endregion
}
3.6.6 ContextMenuManager.cs — Контекстное меню
using System;
using System.Windows.Forms;
using System.Threading.Tasks;
namespace Cameyo.RdpMon
{
/// <summary>
/// Модуль для управления контекстным меню записей подключений
/// </summary>
public class ContextMenuManager
{
private readonly IPBlocker ipBlocker;
private readonly AbuseReporter abuseReporter;
private readonly ListManager listManager;
private readonly SettingsManager settingsManager;
public ContextMenuManager()
{
ipBlocker = new IPBlocker();
listManager = new ListManager();
settingsManager = new SettingsManager();
// Инициализируем AbuseReporter только если есть API ключ
var settings = settingsManager.GetSettings();
if (!string.IsNullOrEmpty(settings.AbuseIPDBApiKey))
{
abuseReporter = new AbuseReporter(settings.AbuseIPDBApiKey);
}
}
/// <summary>
/// Создаёт контекстное меню для записи подключения
/// </summary>
/// <param name="addr">Информация об IP-адресе</param>
/// <returns>Настроенное контекстное меню</returns>
public ContextMenuStrip CreateConnectionContextMenu(Addr addr)
{
var contextMenu = new ContextMenuStrip();
// Заголовок с IP-адресом
var headerItem = new ToolStripLabel($"IP: {addr.AddrId}")
{
Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold)
};
contextMenu.Items.Add(headerItem);
contextMenu.Items.Add(new ToolStripSeparator());
// Копировать IP
var copyIPItem = new ToolStripMenuItem("Копировать IP")
{
Image = Properties.Resources.Copy, // Предполагается наличие иконки
ShortcutKeys = Keys.Control | Keys.C
};
copyIPItem.Click += (sender, e) => CopyIPToClipboard(addr.AddrId);
contextMenu.Items.Add(copyIPItem);
contextMenu.Items.Add(new ToolStripSeparator());
// Проверить в AbuseIPDB
if (abuseReporter != null)
{
var checkAbuseItem = new ToolStripMenuItem("Проверить в AbuseIPDB")
{
Image = Properties.Resources.Search
};
checkAbuseItem.Click += async (sender, e) => await CheckIPInAbuseIPDB(addr);
contextMenu.Items.Add(checkAbuseItem);
}
// Блокировать IP (если не заблокирован и не в белом списке)
if (!listManager.IsInBlackList(addr.AddrId) && !listManager.IsInWhiteList(addr.AddrId))
{
var blockIPItem = new ToolStripMenuItem("Блокировать IP")
{
Image = Properties.Resources.Block,
ForeColor = System.Drawing.Color.Red
};
blockIPItem.Click += (sender, e) => BlockIP(addr);
contextMenu.Items.Add(blockIPItem);
}
// Разблокировать IP (если заблокирован)
if (listManager.IsInBlackList(addr.AddrId))
{
var unblockIPItem = new ToolStripMenuItem("Разблокировать IP")
{
Image = Properties.Resources.Unblock,
ForeColor = System.Drawing.Color.Green
};
unblockIPItem.Click += (sender, e) => UnblockIP(addr);
contextMenu.Items.Add(unblockIPItem);
}
contextMenu.Items.Add(new ToolStripSeparator());
// Добавить в белый список (если не в белом списке)
if (!listManager.IsInWhiteList(addr.AddrId))
{
var addToWhitelistItem = new ToolStripMenuItem("Добавить в белый список")
{
Image = Properties.Resources.Whitelist
};
addToWhitelistItem.Click += (sender, e) => AddToWhitelist(addr);
contextMenu.Items.Add(addToWhitelistItem);
}
// Отправить в AbuseIPDB (только для атак)
if (addr.IsAttack() && abuseReporter != null)
{
contextMenu.Items.Add(new ToolStripSeparator());
var reportToAbuseItem = new ToolStripMenuItem("Отправить информацию о атаке в AbuseIPDB")
{
Image = Properties.Resources.Report,
ForeColor = System.Drawing.Color.DarkOrange
};
reportToAbuseItem.Click += async (sender, e) => await ReportToAbuseIPDB(addr);
contextMenu.Items.Add(reportToAbuseItem);
}
contextMenu.Items.Add(new ToolStripSeparator());
// Детальная информация
var detailsItem = new ToolStripMenuItem("Детальная информация")
{
Image = Properties.Resources.Info
};
detailsItem.Click += (sender, e) => ShowDetailedInfo(addr);
contextMenu.Items.Add(detailsItem);
return contextMenu;
}
/// <summary>
/// Копирует IP-адрес в буфер обмена
/// </summary>
/// <param name="ipAddress">IP-адрес для копирования</param>
private void CopyIPToClipboard(string ipAddress)
{
try
{
Clipboard.SetText(ipAddress);
ShowNotification($"IP-адрес {ipAddress} скопирован в буфер обмена",
ToolTipIcon.Info);
Log($"CopyIPToClipboard: {ipAddress} скопирован в буфер обмена");
}
catch (Exception ex)
{
Log($"CopyIPToClipboard: ошибка копирования {ipAddress}: " + ex.Message);
MessageBox.Show("Ошибка копирования в буфер обмена", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
/// <summary>
/// Блокирует IP-адрес
/// </summary>
/// <param name="addr">Информация об IP-адресе</param>
private void BlockIP(Addr addr)
{
try
{
var result = MessageBox.Show(
$"Заблокировать IP-адрес {addr.AddrId}?\n\n" +
$"Неудачных попыток: {addr.FailCount}\n" +
$"Последняя активность: {addr.Last:yyyy-MM-dd HH:mm:ss}",
"Подтверждение блокировки",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
string reason = $"Ручная блокировка: {addr.FailCount} неудачных попыток";
bool success = ipBlocker.BlockIP(addr.AddrId, reason, false, addr.FailCount);
if (success)
{
ShowNotification($"IP-адрес {addr.AddrId} успешно заблокирован",
ToolTipIcon.Info);
Log($"BlockIP: {addr.AddrId} заблокирован вручную");
}
else
{
MessageBox.Show("Ошибка блокировки IP-адреса", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
catch (Exception ex)
{
Log($"BlockIP: ошибка блокировки {addr.AddrId}: " + ex.ToString());
MessageBox.Show("Произошла ошибка при блокировке IP-адреса", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// Разблокирует IP-адрес
/// </summary>
/// <param name="addr">Информация об IP-адресе</param>
private void UnblockIP(Addr addr)
{
try
{
var result = MessageBox.Show(
$"Разблокировать IP-адрес {addr.AddrId}?",
"Подтверждение разблокировки",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
bool success = ipBlocker.UnblockIP(addr.AddrId);
if (success)
{
ShowNotification($"IP-адрес {addr.AddrId} разблокирован",
ToolTipIcon.Info);
Log($"UnblockIP: {addr.AddrId} разблокирован");
}
else
{
MessageBox.Show("Ошибка разблокировки IP-адреса", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
catch (Exception ex)
{
Log($"UnblockIP: ошибка разблокировки {addr.AddrId}: " + ex.ToString());
MessageBox.Show("Произошла ошибка при разблокировке IP-адреса", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// Добавляет IP в белый список
/// </summary>
/// <param name="addr">Информация об IP-адресе</param>
private void AddToWhitelist(Addr addr)
{
try
{
var inputForm = new InputForm(
"Добавление в белый список",
$"Добавить IP-адрес {addr.AddrId} в белый список?",
"Описание (опционально):");
if (inputForm.ShowDialog() == DialogResult.OK)
{
string description = inputForm.InputText;
bool success = listManager.AddToWhiteList(addr.AddrId, description, "Manual");
if (success)
{
ShowNotification($"IP-адрес {addr.AddrId} добавлен в белый список",
ToolTipIcon.Info);
Log($"AddToWhitelist: {addr.AddrId} добавлен в белый список");
// Если IP был заблокирован, разблокируем его
if (listManager.IsInBlackList(addr.AddrId))
{
ipBlocker.UnblockIP(addr.AddrId);
}
}
else
{
MessageBox.Show("Ошибка добавления в белый список", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
catch (Exception ex)
{
Log($"AddToWhitelist: ошибка добавления {addr.AddrId}: " + ex.ToString());
MessageBox.Show("Произошла ошибка при добавлении в белый список", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// Проверяет IP в AbuseIPDB
/// </summary>
/// <param name="addr">Информация об IP-адресе</param>
private async Task CheckIPInAbuseIPDB(Addr addr)
{
try
{
ShowNotification($"Проверка {addr.AddrId} в AbuseIPDB...", ToolTipIcon.Info);
var response = await abuseReporter.CheckIPAsync(addr.AddrId);
if (response != null && response.Data != null)
{
var data = response.Data;
var message = $"Результат проверки IP {addr.AddrId} в AbuseIPDB:\n\n" +
$"Confidence of Abuse: {data.AbuseConfidencePercentage}%\n" +
$"Страна: {data.CountryName} ({data.CountryCode})\n" +
$"ISP: {data.Isp}\n" +
$"Общее количество сообщений: {data.TotalReports}\n" +
$"Количество репортёров: {data.NumDistinctUsers}\n" +
$"Последнее сообщение: {data.LastReportedAt?.ToString("yyyy-MM-dd HH:mm") ?? "N/A"}\n" +
$"В белом списке: {(data.IsWhitelisted ? "Да" : "Нет")}";
var icon = data.AbuseConfidencePercentage >= 75 ? MessageBoxIcon.Warning :
data.AbuseConfidencePercentage >= 25 ? MessageBoxIcon.Information :
MessageBoxIcon.None;
MessageBox.Show(message, "Результат проверки AbuseIPDB",
MessageBoxButtons.OK, icon);
Log($"CheckIPInAbuseIPDB: {addr.AddrId} проверен, confidence: {data.AbuseConfidencePercentage}%");
}
else
{
MessageBox.Show("Не удалось получить информацию из AbuseIPDB", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
catch (Exception ex)
{
Log($"CheckIPInAbuseIPDB: ошибка проверки {addr.AddrId}: " + ex.ToString());
MessageBox.Show("Произошла ошибка при проверке в AbuseIPDB", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// Отправляет сообщение о атаке в AbuseIPDB
/// </summary>
/// <param name="addr">Информация об атакующем IP</param>
private async Task ReportToAbuseIPDB(Addr addr)
{
try
{
var result = MessageBox.Show(
$"Отправить сообщение о атаке с IP {addr.AddrId} в AbuseIPDB?\n\n" +
$"Неудачных попыток: {addr.FailCount}\n" +
$"Период активности: {addr.First:yyyy-MM-dd HH:mm} - {addr.Last:yyyy-MM-dd HH:mm}",
"Отправка сообщения в AbuseIPDB",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
ShowNotification($"Отправка сообщения о {addr.AddrId} в AbuseIPDB...",
ToolTipIcon.Info);
bool success = await abuseReporter.ReportBruteForceAsync(addr);
if (success)
{
ShowNotification($"Сообщение о {addr.AddrId} отправлено в AbuseIPDB",
ToolTipIcon.Info);
// Отмечаем в базе данных, что сообщение отправлено
listManager.MarkAsReportedToAbuseIPDB(addr.AddrId);
Log($"ReportToAbuseIPDB: {addr.AddrId} сообщение отправлено");
}
else
{
MessageBox.Show("Ошибка отправки сообщения в AbuseIPDB", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
catch (Exception ex)
{
Log($"ReportToAbuseIPDB: ошибка отправки {addr.AddrId}: " + ex.ToString());
MessageBox.Show("Произошла ошибка при отправке сообщения в AbuseIPDB", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// Показывает детальную информацию об IP-адресе
/// </summary>
/// <param name="addr">Информация об IP-адресе</param>
private void ShowDetailedInfo(Addr addr)
{
try
{
var details = $"Детальная информация об IP-адресе {addr.AddrId}\n\n" +
$"Тип подключения: {(addr.IsAttack() ? "🔴 Атака" : addr.IsLegit() ? "🟢 Легитимный" : "🟡 Подозрительный")}\n" +
$"Неудачных попыток: {addr.FailCount}\n" +
$"Успешных подключений: {addr.SuccessCount}\n" +
$"Первое обнаружение: {addr.First:yyyy-MM-dd HH:mm:ss} UTC\n" +
$"Последняя активность: {addr.Last:yyyy-MM-dd HH:mm:ss} UTC\n" +
$"Продолжительность: {Utils.DurationStr(addr.Last.Subtract(addr.First))}\n" +
$"Текущая активность: {(addr.IsOngoing() ? "Да" : "Нет")}\n\n" +
$"Пользователи, на которых нацелены атаки:\n{string.Join("\n", addr.UserNames)}\n\n" +
$"В белом списке: {(listManager.IsInWhiteList(addr.AddrId) ? "Да" : "Нет")}\n" +
$"В чёрном списке: {(listManager.IsInBlackList(addr.AddrId) ? "Да" : "Нет")}";
MessageBox.Show(details, "Детальная информация",
MessageBoxButtons.OK, MessageBoxIcon.Information);
Log($"ShowDetailedInfo: показана информация о {addr.AddrId}");
}
catch (Exception ex)
{
Log($"ShowDetailedInfo: ошибка показа информации {addr.AddrId}: " + ex.ToString());
MessageBox.Show("Произошла ошибка при отображении информации", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// Показывает уведомление пользователю
/// </summary>
/// <param name="message">Текст уведомления</param>
/// <param name="icon">Иконка уведомления</param>
4. Ссылки по теме
- RDP Monitoring — Cameyo
- New Open Source Tool Helps Prevent Brute Force and Ransomware Attacks — malwaretips.com
- Cameyo introduces security features to protect customers from brute-force attacks and ransomware
- Exposed RDP Servers See 150k Brute Force Attempts Per Week — simpatico.com
- AbuseIPDB Integration for Cortex XSOAR — xsoar.pan.dev
- AbuseIPDB and Splunk Integration
- AbuseIPDB Documentation
- Fail2ban Integration — AbuseIPDB
- Architecture for WinForms Applications — Stack Overflow
- Service Application Programming Architecture — Microsoft Docs
- LiteDB in C# — FoxLearn
- LiteDB in ASP.NET Core API — Blog
- RDPmon на GitHub
- PDF Инструкция по RdpMon
- Supported platforms: Windows Monitoring — Syteca
- RDCMan на GitHub
- GitHub Copilot: Исследование проектов на GitHub (документация на русском)
- Cameyo на GitHub
- Обзор ПО для мониторинга удалённого рабочего стола — CurrentWare
- RDPMonWebGUI — GitHub
- Создание нового репозитория на GitHub (русская документация)
- RDP Port Shield против ransomware — SensorsTechForum
- Загрузка RDCMan — Microsoft
- RdpMon.cs — исходный код
- Главная страница GitHub
- Топ 5 RDP-менеджеров — diright.ru
- README.md RDPmon — GitHub (radtek)
- Data_analyst — GitHub
- Мониторинг пользователей терминала Windows через Zabbix
- Cameyo защищает от brute-force атак и ransomware — PremisesNetworks.com
- Remote Desktop Services — Википедия (англ.)
- Logical Architecture for Desktop Hosting — Microsoft
- README.md RDPmon — GitHub (plain)
- Reference Architecture for Desktop Hosting — Microsoft
- AbuseIPDB API — GitHub topics
- Скачать RDPmon — Cameyo
- UI Design Pattern for Windows Forms — Stack Overflow
- Как структурировать WinForms приложение — Reddit
- WinForms Architecture Patterns — GitHub
- Сетевые отчёты Splunk — research.splunk.com
- Model-View-Presenter для WinForms — markheath.net
- Исследования по RDP мониторингу — muni.cz
- YouTube: Introduction to RDPmon
- LiteDB and CommunityToolkit DataSync
- Data Science для кибербезопасности: анализ brute force по RDP — Microsoft
- Использование шаблонов проектирования в WinForms — MSDN Magazine
- Concurrent Access to LiteDB file over Network — Stack Overflow
- Playbook of the Week: Responding to RDP Brute Force Attacks — PaloAlto Networks
- Пример отчёта, PNG (AI output direct)
- Исходный CSV пример (AI output direct)