Skip to content

RdpAudit Service ‐ Technical Specification v2.0

Mikhail Deynekin edited this page May 4, 2026 · 1 revision

Purpose: This document is a complete, self-contained specification for an AI coding agent (Perplexity Computer) to implement RdpAudit — a professional Windows RDP security monitoring solution. All architectural decisions, data models, event catalogs, API contracts, and implementation constraints are described herein. The agent must follow this specification precisely.

Version: 2.0 — added full MITRE ATT&CK coverage (T1563.002, T1550.002/003, T1546.008, T1003, T1572, T1098.002, T1133), Kerberos event layer (4768/4769/4770/4771), Object Access layer (4656/4657/4663), 20 alert rules, SACL configuration automation, Address.IsPublicIp, RawEvent.AuthPackage/ObjectName/AccessMask fields, LSASS RunAsPPL prerequisite check.


1. Project Overview

1.1 Goals

RdpAudit captures, persists, and exposes the complete lifecycle of RDP security events on Windows 10/11 and Windows Server 2012 R2 / 2016 / 2022 / 2025. It extends and improves upon the open-source Cameyo/rdpmon architecture with a push-based event pipeline, a normalized relational database, and a professional setup/management interface.

The service provides full coverage of MITRE ATT&CK techniques relevant to RDP: T1110 (Brute Force), T1021.001 (RDP Lateral Movement), T1563.002 (RDP Session Hijacking), T1550.002 (Pass the Hash), T1550.003 (Pass the Ticket / Golden Ticket), T1546.008 (Accessibility Features/Sticky Keys), T1003 (LSASS Credential Dumping), T1572 (RDP Port Change), T1098.002 (Privileged Group Manipulation), T1133 (External Remote Services), T1136 (Create Account), T1053 (Scheduled Task Persistence), T1059 (Command/Script via RDP).

1.2 Deliverables

Two independently executable binaries sharing a common library:

Binary Type Purpose
RdpAudit.Configurator.exe .NET 8 WinForms (x64) Setup, prerequisite check, service management, settings UI
RdpAudit.Service.exe .NET 8 Worker Service (x64) Windows Service: event capture → pipeline → SQLite

1.3 Shared Library

RdpAudit.Core.dll — models, database context, configuration schema, IPC contracts.


2. Solution Structure

RdpAudit/
├── RdpAudit.sln
├── src/
│   ├── RdpAudit.Core/               # Shared library
│   │   ├── Models/                  # EF Core entities
│   │   ├── Data/                    # DbContext, Migrations
│   │   ├── Config/                  # AppSettings schema
│   │   ├── Ipc/                     # Named Pipe message contracts
│   │   └── Events/                  # EventDescriptor catalog
│   ├── RdpAudit.Service/            # Worker Service
│   │   ├── Program.cs
│   │   ├── Workers/
│   │   │   ├── EventCollectorWorker.cs
│   │   │   ├── EventProcessorWorker.cs
│   │   │   ├── AlertWorker.cs
│   │   │   └── IpcServerWorker.cs
│   │   ├── Collectors/              # Per-channel EventLogWatcher wrappers
│   │   ├── Processors/              # Event enrichment & normalization
│   │   ├── Alerts/                  # Alert rule implementations
│   │   └── appsettings.json
│   └── RdpAudit.Configurator/      # WinForms app
│       ├── Program.cs
│       ├── Forms/
│       │   ├── MainForm.cs
│       │   ├── PrerequisitesPage.cs
│       │   ├── AuditPolicyPage.cs
│       │   ├── ServicePage.cs
│       │   └── SettingsPage.cs
│       └── Services/
│           ├── PrerequisiteChecker.cs
│           ├── AuditPolicyManager.cs
│           ├── ServiceManager.cs
│           └── IpcClient.cs
└── tests/
    ├── RdpAudit.Core.Tests/
    └── RdpAudit.Service.Tests/

3. Technology Stack

3.1 NuGet Packages

RdpAudit.Core

<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.*" />
<PackageReference Include="MessagePack" Version="2.*" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.*" />

RdpAudit.Service

<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.*" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.*" />
<PackageReference Include="Serilog.Sinks.EventLog" Version="3.*" />
<PackageReference Include="Serilog.Sinks.File" Version="5.*" />
<PackageReference Include="Serilog.Formatting.Compact" Version="2.*" />

RdpAudit.Configurator

<PackageReference Include="Microsoft.Win32.Registry" Version="5.*" />
<PackageReference Include="MessagePack" Version="2.*" />
<!-- WinForms is included via <UseWindowsForms>true</UseWindowsForms> -->

3.2 Target Framework

All projects: net8.0-windows, <PlatformTarget>x64</PlatformTarget>, <RuntimeIdentifier>win-x64</RuntimeIdentifier>.

3.3 Database

SQLite file located at %ProgramData%\RdpAudit\rdpaudit.db. Connection string in appsettings.json. WAL mode and synchronous=NORMAL set via PRAGMA at context initialization.


4. Event Catalog

The service monitors ALL of the following events. Each event descriptor specifies the log channel, EventID, required audit policy, and fields to extract.

4.1 Network Connection Layer

Channel EventID Description Fields to Extract
Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational 1149 RDP network connection (pre-auth) UserName, Domain, ClientAddress
Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational 261 RDP listener received connection ClientAddress, SessionId
Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational 131 RDP connection attempt (with IP before auth) ClientIP
Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational 140 Authentication failure with IP ClientIP, FailureReason

4.2 Authentication Layer (Security Log)

Channel EventID Description LogonType Filter Fields to Extract
Security 4624 Successful logon Types 3, 7, 9, 10 SubjectUserName, TargetUserName, LogonType, IpAddress, LogonId, AuthPackage, WorkstationName
Security 4625 Failed logon Types 3, 10 TargetUserName, LogonType, IpAddress, FailureReason, Status, SubStatus, WorkstationName
Security 4648 Explicit credential logon (RunAs/NLA) All SubjectUserName, TargetUserName, TargetServerName, IpAddress, LogonId
Security 4776 NTLM credential validation All TargetUserName, Workstation, Status
Security 4672 Special privileges assigned at logon All SubjectUserName, SubjectLogonId, PrivilegeList

Note (T1550.002 Pass the Hash): EventID 4624 with LogonType=3 or LogonType=9 AND AuthPackage=NTLM is the primary indicator. Always extract and store the AuthPackage field for PtH correlation.

4.3 Session Lifecycle Layer

Channel EventID Description Fields to Extract
Microsoft-Windows-TerminalServices-LocalSessionManager/Operational 21 Session logon succeeded UserName, SessionId, SourceNetworkAddress
Microsoft-Windows-TerminalServices-LocalSessionManager/Operational 22 Shell start notification UserName, SessionId, SourceNetworkAddress
Microsoft-Windows-TerminalServices-LocalSessionManager/Operational 23 Session logoff succeeded UserName, SessionId
Microsoft-Windows-TerminalServices-LocalSessionManager/Operational 24 Session disconnected UserName, SessionId, SourceNetworkAddress
Microsoft-Windows-TerminalServices-LocalSessionManager/Operational 25 Session reconnection succeeded UserName, SessionId, SourceNetworkAddress
Microsoft-Windows-TerminalServices-LocalSessionManager/Operational 39 Session X disconnected by session Y SessionId, DisconnectingSessionId
Microsoft-Windows-TerminalServices-LocalSessionManager/Operational 40 Session disconnected, reason code SessionId, ReasonCode

4.4 Logoff Layer

Channel EventID Description Fields to Extract
Security 4634 Account logged off TargetUserName, LogonType, LogonId
Security 4647 User-initiated logoff TargetUserName, LogonId
System 9009 Desktop Window Manager exited ProcessId, ExitCode

4.5 Session Reconnect/Window Station (Security Log)

Channel EventID Description Fields to Extract
Security 4778 Session reconnected to Window Station AccountName, ClientAddress, SessionName, LogonId
Security 4779 Session disconnected from Window Station AccountName, ClientAddress, SessionName, LogonId

4.6 Privilege Escalation & Process Tracking

Channel EventID Description Fields to Extract
Security 4688 Process created SubjectUserName, NewProcessName, ParentProcessName, CommandLine, LogonId, ProcessId
Security 4689 Process exited SubjectUserName, ProcessName, ProcessId, ExitStatus
Security 4697 Service installed SubjectUserName, ServiceName, ServiceFileName, ServiceStartType

MITRE T1563.002 (RDP Session Hijacking): Monitor EventID 4688 where NewProcessName ends with tscon.exe — this is the primary indicator of session theft. Also monitor mstsc.exe with CommandLine containing /shadow for unauthorized shadow session control. Correlate with EventID 24 (disconnect) on the same SessionId within 60 seconds.

MITRE T1546.008 (Sticky Keys / Accessibility Features): Monitor EventID 4688 where ParentProcessName = winlogon.exe AND NewProcessName ends with cmd.exe or powershell.exe — this indicates execution of a backdoor triggered from the Windows logon screen.

4.7 Persistence & Account Changes (Security Log)

Channel EventID Description Fields to Extract
Security 4698 Scheduled task created SubjectUserName, TaskName, TaskContent
Security 4699 Scheduled task deleted SubjectUserName, TaskName
Security 4700 Scheduled task enabled SubjectUserName, TaskName
Security 4701 Scheduled task disabled SubjectUserName, TaskName
Security 4702 Scheduled task updated SubjectUserName, TaskName
Security 4720 User account created SubjectUserName, TargetUserName
Security 4722 User account enabled SubjectUserName, TargetUserName
Security 4725 User account disabled SubjectUserName, TargetUserName
Security 4726 User account deleted SubjectUserName, TargetUserName
Security 4728 Member added to global group SubjectUserName, MemberName, GroupName
Security 4732 Member added to local group SubjectUserName, MemberName, GroupName
Security 4756 Member added to universal group SubjectUserName, MemberName, GroupName
Security 4800 Workstation locked SubjectUserName, SessionId
Security 4801 Workstation unlocked SubjectUserName, SessionId

4.8 RD Gateway (if applicable)

Channel EventID Description Fields to Extract
Microsoft-Windows-TerminalServices-Gateway/Operational 302 User connected through RD Gateway UserName, ClientAddress, ResourceName
Microsoft-Windows-TerminalServices-Gateway/Operational 303 User disconnected from RD Gateway UserName, ClientAddress

4.9 RDP Client-Side (Outgoing)

Channel EventID Description Fields to Extract
Microsoft-Windows-TerminalServices-RDPClient/Operational 1024 RDP client connecting to server ServerName
Microsoft-Windows-TerminalServices-RDPClient/Operational 1102 RDP client got server IP ServerIp

4.10 Kerberos Authentication (MITRE T1550.003 — Pass the Ticket)

Requires: Advanced Audit Policy → Account Logon → Kerberos Service Ticket Operations (Success + Failure), Kerberos Authentication Service (Success + Failure).

Channel EventID Description Fields to Extract
Security 4768 Kerberos TGT requested TargetUserName, ClientAddress, TicketEncryptionType, Status
Security 4769 Kerberos service ticket requested TargetUserName, ClientAddress, TicketEncryptionType, ServiceName, Status
Security 4770 Kerberos service ticket renewed TargetUserName, ServiceName
Security 4771 Kerberos pre-auth failed ClientAddress, TargetUserName, FailureCode

Golden Ticket indicator: EventID 4769 with TicketEncryptionType = 0x17 (RC4-HMAC) on a domain that enforces AES (0x12/0x11) is a strong indicator of a forged ticket. Always store TicketEncryptionType as a dedicated field in the Details JSON.

4.11 File/Registry Integrity (MITRE T1546.008, T1572, T1003)

Requires: Advanced Audit Policy → Object Access → File System (Success) + Registry (Success). SACL must be configured on monitored registry keys — see Section 8.3.

Channel EventID Description Fields to Extract
Security 4656 Object handle requested (process/file) SubjectUserName, ObjectName, ObjectType, AccessMask, ProcessName
Security 4657 Registry value modified SubjectUserName, ObjectName, OldValue, NewValue, ProcessName
Security 4663 Object accessed (file/process) SubjectUserName, ObjectName, ObjectType, AccessMask, ProcessName

T1546.008 (Sticky Keys): Monitor EventID 4657 where ObjectName contains Image File Execution Options\sethc, \utilman, \osk.exe, \magnify.exe, \narrator.exe, or \displayswitch.exe.

T1572 (RDP Port Change): Monitor EventID 4657 where ObjectName contains Terminal Server\WinStations\RDP-Tcp and ValueName = PortNumber.

T1003 (LSASS Credential Dump): Monitor EventID 4656 where ObjectType = Process, ObjectName contains lsass.exe, and AccessMask includes 0x10 (PROCESS_VM_READ) or 0x1FFFFF (PROCESS_ALL_ACCESS). Also monitor EventID 4657 where ObjectName contains Lsa and ValueName = RunAsPPL for tamper detection.


5. Database Schema (SQLite, EF Core 8)

5.1 PRAGMA Settings (applied at DbContext.OnConfiguring)

PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -32000;
PRAGMA foreign_keys = ON;
PRAGMA auto_vacuum = INCREMENTAL;

5.2 Entity: RawEvent

public class RawEvent
{
    public long Id { get; set; }              // AUTOINCREMENT
    public int EventId { get; set; }           // e.g. 4624
    public string Channel { get; set; }        // "Security", "Microsoft-Windows-TerminalServices-..."
    public DateTime TimeUtc { get; set; }      // UTC, stored as TEXT ISO8601
    public string? SourceIp { get; set; }      // Parsed IP or null
    public string? UserName { get; set; }      // Domain\User or just User
    public string? Domain { get; set; }
    public int? SessionId { get; set; }        // WTS session ID
    public int? LogonType { get; set; }        // 3, 7, 9, 10 etc.
    public string? LogonId { get; set; }       // Hex logon ID (e.g. "0x4A32B1")
    public string? AuthPackage { get; set; }   // "Kerberos", "NTLM", "Negotiate" — for PtH detection
    public string? Status { get; set; }        // NTSTATUS for 4625
    public string? SubStatus { get; set; }     // Sub-status for 4625
    public string? ProcessName { get; set; }   // For 4688/4689
    public string? CommandLine { get; set; }   // For 4688 (if enabled)
    public string? ObjectName { get; set; }    // For 4656/4657/4663 — file or registry path
    public string? AccessMask { get; set; }    // For 4656/4663 — hex access mask
    public string? Details { get; set; }       // JSON blob: remaining event-specific fields
    public bool Processed { get; set; }        // false until AlertWorker runs
    public long? SessionFk { get; set; }       // FK → Session.Id (nullable)
    public long? AddressFk { get; set; }       // FK → Address.Id (nullable)
}

Indexes:

  • (EventId, TimeUtc DESC) — event query by type + time range
  • (SourceIp, TimeUtc DESC) — brute-force correlation
  • (LogonId, TimeUtc DESC) — session correlation
  • (SessionId, TimeUtc DESC) — WTS session tracking
  • (Processed, TimeUtc) — alert processing queue
  • (ObjectName, EventId) — registry/file integrity queries

5.3 Entity: Session

public class Session
{
    public long Id { get; set; }
    public string SessionUid { get; set; }     // Composite: "HOST:WtsId:ConnectUtc"
    public int WtsSessionId { get; set; }
    public string UserName { get; set; }
    public string? Domain { get; set; }
    public string? SourceIp { get; set; }
    public DateTime ConnectUtc { get; set; }
    public DateTime? DisconnectUtc { get; set; }
    public DateTime? LogoffUtc { get; set; }
    public int? LogonType { get; set; }
    public string? LogonId { get; set; }
    public SessionStatus Status { get; set; }  // Enum: Active, Disconnected, LoggedOff
    public long Flags { get; set; }
    public ICollection<RawEvent> Events { get; set; }
}

public enum SessionStatus { Active = 0, Disconnected = 1, LoggedOff = 2 }

Indexes:

  • (SessionUid) — UNIQUE
  • (UserName, ConnectUtc DESC)
  • (SourceIp, ConnectUtc DESC)
  • (Status)

5.4 Entity: Address

public class Address
{
    public long Id { get; set; }
    public string Ip { get; set; }             // PRIMARY key logic via unique index
    public int FailCount { get; set; }
    public int SuccessCount { get; set; }
    public DateTime FirstSeen { get; set; }
    public DateTime LastSeen { get; set; }
    public int ThreatScore { get; set; }       // Computed by AlertWorker
    public bool IsBlocked { get; set; }
    public DateTime? BlockedAt { get; set; }
    public string? BlockReason { get; set; }
    public string? UserNames { get; set; }     // JSON array of seen usernames
    public bool IsPublicIp { get; set; }       // T1133: true if not RFC1918/loopback/link-local
}

Indexes:

  • (Ip) — UNIQUE
  • (ThreatScore DESC)
  • (IsBlocked)
  • (IsPublicIp, ThreatScore DESC) — external RDP queries

5.5 Entity: Alert

public class Alert
{
    public long Id { get; set; }
    public string RuleId { get; set; }
    public AlertSeverity Severity { get; set; }  // Info, Low, Medium, High, Critical
    public DateTime TimeUtc { get; set; }
    public string? SourceIp { get; set; }
    public string? UserName { get; set; }
    public string Message { get; set; }
    public string? Details { get; set; }          // JSON context
    public bool Acknowledged { get; set; }
    public long? TriggerEventId { get; set; }     // FK → RawEvent.Id
}

public enum AlertSeverity { Info = 0, Low = 1, Medium = 2, High = 3, Critical = 4 }

5.6 Entity: Bookmark

public class Bookmark
{
    public string Channel { get; set; }          // PRIMARY KEY (via unique constraint)
    public string BookmarkXml { get; set; }      // EventLogBookmark serialized XML
    public DateTime UpdatedUtc { get; set; }
}

5.7 Entity: DbProp

public class DbProp
{
    public string Key { get; set; }             // PRIMARY KEY
    public string Value { get; set; }
    public DateTime UpdatedUtc { get; set; }
}

6. Configuration Schema (appsettings.json)

Both modules read from %ProgramData%\RdpAudit\appsettings.json. The service reads it at startup; the Configurator writes it via the Settings UI.

{
  "RdpAudit": {
    "DatabasePath": "%ProgramData%\\RdpAudit\\rdpaudit.db",
    "LogDirectory": "%ProgramData%\\RdpAudit\\Logs",
    "LogRetentionDays": 90,
    "EventRetentionDays": 365,
    "IpcPipeName": "RdpAuditService",
    "Monitoring": {
      "EnabledChannels": [
        "Security",
        "Microsoft-Windows-TerminalServices-LocalSessionManager/Operational",
        "Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational",
        "Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational",
        "Microsoft-Windows-TerminalServices-Gateway/Operational",
        "Microsoft-Windows-TerminalServices-RDPClient/Operational",
        "System"
      ],
      "EnabledEventIds": [
        1149, 261, 131, 140,
        4624, 4625, 4634, 4647, 4648, 4656, 4657, 4663, 4672, 4776,
        4688, 4689, 4697, 4698, 4699, 4700, 4701, 4702,
        4720, 4722, 4725, 4726, 4728, 4732, 4756,
        4768, 4769, 4770, 4771,
        4778, 4779, 4800, 4801,
        21, 22, 23, 24, 25, 39, 40,
        302, 303,
        1024, 1102,
        9009
      ],
      "FilterLocalAddresses": true,
      "TrackProcessCreation": true,
      "TrackScheduledTasks": true,
      "TrackAccountChanges": true,
      "TrackKerberos": true,
      "TrackObjectAccess": true
    },
    "Alerts": {
      "EnableBruteForceDetection": true,
      "BruteForceThreshold": 10,
      "BruteForceWindowMinutes": 5,
      "EnableOffHoursAlert": false,
      "BusinessHoursStart": "08:00",
      "BusinessHoursEnd": "20:00",
      "EnableNewUserAlert": true,
      "EnableExternalRdpAlert": true,
      "KerberosExpectedEncryptionType": "0x12",
      "LsassAccessWhitelistProcesses": ["MsMpEng.exe", "SenseIR.exe", "csrss.exe"],
      "WhitelistIps": [],
      "WhitelistUsers": []
    },
    "Firewall": {
      "AutoBlockBruteForce": false,
      "AutoBlockThreshold": 50,
      "BlockRuleName": "RdpAudit.BlackList"
    },
    "Service": {
      "GuiBoostIntervalSeconds": 10,
      "IdleIntervalSeconds": 180
    }
  },
  "Serilog": {
    "MinimumLevel": "Information",
    "WriteTo": [
      { "Name": "EventLog", "Args": { "source": "RdpAudit", "logName": "Application" } },
      {
        "Name": "File",
        "Args": {
          "path": "%ProgramData%\\RdpAudit\\Logs\\service-.log",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 30,
          "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact"
        }
      }
    ]
  }
}

7. Module 2: Windows Service (RdpAudit.Service)

7.1 Program.cs

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(o => o.ServiceName = "RdpAuditService");
builder.Services.Configure<RdpAuditOptions>(builder.Configuration.GetSection("RdpAudit"));
builder.Services.AddDbContextFactory<AuditDbContext>(opt =>
    opt.UseSqlite(ResolveConnectionString(builder.Configuration)));
builder.Services.AddSingleton<IEventChannel, EventChannel>();
builder.Services.AddSingleton<IBookmarkStore, BookmarkStore>();
builder.Services.AddSingleton<IAlertRuleRegistry, AlertRuleRegistry>();
builder.Services.AddSingleton<IFirewallService, WindowsFirewallService>();

// -- Brute force & authentication
builder.Services.AddSingleton<IAlertRule, BruteForceRule>();
builder.Services.AddSingleton<IAlertRule, BruteForceNtlmRule>();
builder.Services.AddSingleton<IAlertRule, PassTheHashRule>();
builder.Services.AddSingleton<IAlertRule, GoldenTicketRule>();
// -- Session & lateral movement
builder.Services.AddSingleton<IAlertRule, OffHoursLoginRule>();
builder.Services.AddSingleton<IAlertRule, ExternalRdpLoginRule>();
builder.Services.AddSingleton<IAlertRule, RdpSessionHijackRule>();
builder.Services.AddSingleton<IAlertRule, RapidReconnectRule>();
builder.Services.AddSingleton<IAlertRule, UnknownIpSuccessRule>();
// -- Privilege & process
builder.Services.AddSingleton<IAlertRule, PrivilegedLoginRule>();
builder.Services.AddSingleton<IAlertRule, ProcessAnomalyRule>();
builder.Services.AddSingleton<IAlertRule, LsassAccessRule>();
// -- Persistence & account
builder.Services.AddSingleton<IAlertRule, ScheduledTaskPersistenceRule>();
builder.Services.AddSingleton<IAlertRule, ScheduledTaskModifiedRule>();
builder.Services.AddSingleton<IAlertRule, ServiceInstallRule>();
builder.Services.AddSingleton<IAlertRule, NewAccountCreatedRule>();
builder.Services.AddSingleton<IAlertRule, PrivilegedGroupChangeRule>();
// -- Tampering & configuration
builder.Services.AddSingleton<IAlertRule, StickyKeysBackdoorRule>();
builder.Services.AddSingleton<IAlertRule, RdpPortChangedRule>();
builder.Services.AddSingleton<IAlertRule, LsassPplTamperRule>();

// Worker services
builder.Services.AddHostedService<EventCollectorWorker>();
builder.Services.AddHostedService<EventProcessorWorker>();
builder.Services.AddHostedService<AlertWorker>();
builder.Services.AddHostedService<IpcServerWorker>();
builder.Services.AddHostedService<MaintenanceWorker>();

builder.Logging.AddSerilog();
var host = builder.Build();
await host.RunAsync();

7.2 EventCollectorWorker

Responsibility: Create and manage one EventLogWatcher per enabled channel. On each event, deserialize the EventRecord, create a RawEvent DTO, and write to Channel<RawEvent>.

Critical implementation rules:

  • Load EventLogBookmark from Bookmark table at startup. If no bookmark exists, create one from DateTime.UtcNow.AddDays(-1) to catch up recent events.
  • On EventRecordWritten, serialize the EventRecord XML to the Details field before disposing it (EventRecord is not thread-safe and becomes invalid after the handler returns).
  • Save updated bookmark to DB every 100 events or every 30 seconds (whichever comes first), using a SemaphoreSlim(1,1) for thread-safe bookmark access.
  • Channel bounded capacity: 50,000. Full mode: DropOldest with a warning log.
  • Handle EventLogWatcher restart on EntryWrittenEventArgs.Entry == null (watcher can fail if log is cleared).
public class EventCollectorWorker : BackgroundService
{
    private readonly IEnumerable<string> _channels;
    private readonly Channel<RawEvent> _channel;
    private readonly IBookmarkStore _bookmarkStore;
    private readonly List<EventLogWatcher> _watchers = new();

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await InitializeWatchersAsync(stoppingToken);
        await Task.Delay(Timeout.Infinite, stoppingToken);
    }

    private async Task InitializeWatchersAsync(CancellationToken ct)
    {
        foreach (var channelName in _channels)
        {
            var bookmark = await _bookmarkStore.GetBookmarkAsync(channelName, ct);
            var query = BuildXPathQuery(channelName);
            var logQuery = new EventLogQuery(channelName, PathType.LogName, query);
            var watcher = bookmark != null
                ? new EventLogWatcher(logQuery, bookmark)
                : new EventLogWatcher(logQuery);
            watcher.EventRecordWritten += (s, e) => OnEventReceived(channelName, e);
            watcher.Enabled = true;
            _watchers.Add(watcher);
        }
    }
    // ...
}

7.3 EventProcessorWorker

Responsibility: Read from Channel<RawEvent>, normalize fields, resolve Session FK, resolve Address FK, bulk-insert into SQLite.

Implementation rules:

  • Use IDbContextFactory<AuditDbContext> — create a new DbContext per batch, dispose after SaveChangesAsync.
  • Batch size: 100 events or 500ms timeout (use PeriodicTimer for timeout drain).
  • Field normalization:
    • Filter SourceIp values "::1", "127.0.0.1", "-", "0.0.0.0", "LOCAL" → set to null.
    • Parse UserName from DOMAIN\User format, store domain separately.
    • Normalize LogonId to lowercase hex string.
    • Populate Address.IsPublicIp: true if IP is not in RFC1918 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), not loopback, not link-local.
  • Session resolution: On EventID 21 (LogonSucceeded with non-null SourceIp), create a new Session record. On EventID 23/24/25/4634/4647, update the matching session's DisconnectUtc/LogoffUtc/Status. Match sessions by WtsSessionId + Status == Active.
  • Address resolution: For every non-null SourceIp, upsert Address record (increment FailCount/SuccessCount, update LastSeen, merge UserNames JSON array, set IsPublicIp).

IP classification helper:

public static bool IsPublicIp(string ip)
{
    if (!IPAddress.TryParse(ip, out var addr)) return false;
    if (IPAddress.IsLoopback(addr)) return false;
    var bytes = addr.GetAddressBytes();
    if (addr.AddressFamily == AddressFamily.InterNetworkV6)
        return !addr.IsIPv6LinkLocal && !addr.IsIPv6SiteLocal;
    return !(bytes[0] == 10
        || (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31)
        || (bytes[0] == 192 && bytes[1] == 168)
        || (bytes[0] == 169 && bytes[1] == 254));
}

7.4 AlertWorker

Responsibility: Process RawEvent records where Processed = false. Apply alert rules. Write Alert records. Mark events as processed.

Implementation rules:

  • Query batch: 500 unprocessed events ordered by TimeUtc ASC.
  • Sliding window context: For brute-force rules, load the last 200 events for the same SourceIp from the DB to evaluate frequency.
  • Mark Processed = true in the same transaction as writing alerts.
  • Publish new alerts to the IPC broadcast channel for live GUI updates.

Alert Rule Interface:

public interface IAlertRule
{
    string RuleId { get; }
    string Name { get; }
    AlertSeverity Severity { get; }
    bool IsEnabled(RdpAuditOptions options);
    Task<Alert?> EvaluateAsync(RawEvent evt, IAlertContext context, CancellationToken ct);
}

public interface IAlertContext
{
    Task<IEnumerable<RawEvent>> GetRecentByIpAsync(string ip, int count, TimeSpan window);
    Task<IEnumerable<RawEvent>> GetRecentByUserAsync(string user, int count, TimeSpan window);
    Task<IEnumerable<RawEvent>> GetRecentBySessionIdAsync(int sessionId, int count, TimeSpan window);
    Task<Address?> GetAddressAsync(string ip);
    RdpAuditOptions Options { get; }
}

Required Alert Rules (21 total):

RuleId MITRE Trigger Severity Logic
BRUTE_FORCE_01 T1110 EventID 4625 High >= BruteForceThreshold failures from same IP within BruteForceWindowMinutes
BRUTE_FORCE_NTLM T1110 EventID 4776 Medium >= 20 NTLM failures from same workstation in 5 min
PASS_THE_HASH T1550.002 EventID 4624 High LogonType IN (3,9) AND AuthPackage=NTLM AND no preceding 4648 with same LogonId within 5s AND SourceIp not in whitelist
GOLDEN_TICKET T1550.003 EventID 4769 Critical TicketEncryptionType = 0x17 (RC4-HMAC) AND KerberosExpectedEncryptionType != 0x17 and != empty
OFF_HOURS_LOGIN T1133 EventID 4624 Type 10 Low Logon time outside BusinessHoursStart-BusinessHoursEnd (if enabled)
EXTERNAL_RDP_LOGIN T1133 EventID 1149 or 4624 Type 10 Medium Address.IsPublicIp = true AND EnableExternalRdpAlert = true
RDP_SESSION_HIJACK T1563.002 EventID 4688 Critical NewProcessName ends with tscon.exe OR (NewProcessName ends with mstsc.exe AND CommandLine LIKE %/shadow%)
RAPID_RECONNECT T1563.002 EventID 25 Medium Reconnect within 30s after EventID 24 on same SessionId with different SourceIp
UNKNOWN_IP_SUCCESS T1021.001 EventID 4624 Low Successful login from IP with 0 previous successes but >= 5 failures
PRIVILEGED_LOGIN T1078 EventID 4672 Medium SeDebugPrivilege or SeTcbPrivilege or SeLoadDriverPrivilege in PrivilegeList
PROCESS_ANOMALY T1059 EventID 4688 Medium cmd.exe/powershell.exe/wscript.exe/cscript.exe created from parent svchost.exe, mstsc.exe, or rdpclip.exe
LSASS_ACCESS T1003 EventID 4656 Critical ObjectType=Process, ObjectName LIKE %lsass%, AccessMask contains 0x10 or 0x1FFFFF, AND ProcessName NOT IN LsassAccessWhitelistProcesses
TASK_PERSISTENCE T1053 EventID 4698 High Scheduled task created
TASK_MODIFIED T1053 EventID 4702 Medium Scheduled task updated
SERVICE_INSTALL T1543 EventID 4697 High New service installed
NEW_ACCOUNT T1136 EventID 4720 High User account created
PRIVILEGED_GROUP_CHANGE T1098.002 EventID 4732 High GroupName IN (Administrators, Remote Desktop Users, Remote Management Users, Network Configuration Operators, Backup Operators)
STICKY_KEYS_BACKDOOR T1546.008 EventID 4657 or 4688 Critical EventID 4657 WHERE ObjectName LIKE %IFEO\sethc% OR %IFEO\utilman% OR %IFEO\osk.exe%; OR EventID 4688 WHERE ParentProcessName=winlogon.exe AND NewProcessName IN (cmd, powershell, wscript)
RDP_PORT_CHANGED T1572 EventID 4657 Critical ObjectName LIKE %RDP-Tcp% AND ValueName=PortNumber
LSASS_PPL_TAMPER T1003 EventID 4657 Critical ObjectName LIKE %\Lsa AND ValueName=RunAsPPL
KERBEROS_SPRAY T1110 EventID 4771 High >= 20 Kerberos pre-auth failures from same ClientAddress within 5 min

7.5 IpcServerWorker

Responsibility: Host a Named Pipe server \\.\pipe\RdpAuditService for bidirectional communication with the Configurator.

Implementation rules:

  • Use NamedPipeServerStream in async mode with PipeDirection.InOut, PipeTransmissionMode.Message.
  • Support multiple concurrent connections (max 10) via a loop that creates a new pipe instance per accepted connection.
  • Grant PipeAccessRule to Builtin\Administrators for connect access (ACL on pipe creation).
  • Serialization: MessagePack for all messages.
  • Each message frame: [4-byte length LE][MessagePack payload].

IPC Message Contracts:

[MessagePackObject]
public class IpcRequest
{
    [Key(0)] public IpcCommand Command { get; set; }
    [Key(1)] public string? Payload { get; set; }    // JSON parameters if needed
}

[MessagePackObject]
public class IpcResponse
{
    [Key(0)] public bool Success { get; set; }
    [Key(1)] public string? Error { get; set; }
    [Key(2)] public string? Payload { get; set; }    // JSON result
}

[MessagePackObject]
public class IpcPushEvent
{
    [Key(0)] public IpcPushType Type { get; set; }
    [Key(1)] public string Payload { get; set; }     // JSON: RawEvent or Alert
}

public enum IpcCommand
{
    Ping,
    GetStatus,
    GetRecentEvents,     // params: { count, fromUtc }
    GetRecentAlerts,     // params: { count }
    GetAddresses,        // params: { sortBy, page, pageSize }
    GetSessions,         // params: { active only, page, pageSize }
    AcknowledgeAlert,    // params: { alertId }
    BlockAddress,        // params: { ip }
    UnblockAddress,      // params: { ip }
    GetSettings,
    SaveSettings,        // params: full RdpAuditOptions JSON
}

public enum IpcPushType { NewEvent, NewAlert, ServiceStatus }

Status response payload:

public class ServiceStatus
{
    public bool Running { get; set; }
    public DateTime StartedUtc { get; set; }
    public long EventsTotal { get; set; }
    public long AlertsTotal { get; set; }
    public long AlertsUnack { get; set; }
    public long SessionsActive { get; set; }
    public string DbSize { get; set; }
    public string ServiceVersion { get; set; }
}

7.6 MaintenanceWorker

Responsibility: Run every 24 hours. Delete events older than EventRetentionDays. Run PRAGMA incremental_vacuum(500). Update ThreatScore on Address records (decay formula: score * 0.95 per day, floor 0). Compact log files older than LogRetentionDays.


8. Module 1: Configurator (RdpAudit.Configurator)

8.1 Application Layout

Main window: TabControl with four pages:

  1. Prerequisites — system component checks
  2. Audit Policy — Windows audit and log settings
  3. Service — service lifecycle management
  4. Settings — monitoring configuration

Navigation: standard tabs. Status bar: service status indicator (Green/Red/Yellow dot + text) + DB stats. Refresh every 5 seconds via System.Windows.Forms.Timer using IPC GetStatus.

8.2 Page 1: Prerequisites

This page checks and displays all components required for the service to function correctly.

Prerequisite items to check (one row per item):

# Check How to Check Auto-Fix Action
1 OS Version Environment.OSVersion — require Windows Server 2012 R2+ or Windows 10+ N/A (display only)
2 .NET 8 Runtime Environment.Version + registry HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.NETCore.App Link to download
3 PowerShell 7+ Check HKLM\SOFTWARE\Microsoft\PowerShellCore or pwsh.exe --version via Process Launch winget install Microsoft.PowerShell via Process.Start
4 Remote Desktop Services enabled sc query TermService — check RUNNING sc config TermService start= auto && net start TermService
5 RDP port open (3389) System.Net.NetworkInformation.IPGlobalProperties.GetActiveTcpListeners() Display only
6 Windows Firewall RDP rule NetFwPolicy2.Rules lookup for rule on port 3389 Add rule via NetFwTypeLib COM
7 Security EventLog enabled EventLog.Exists("Security") Always present on Windows
8 TerminalServices-LocalSessionManager/Operational log enabled EventLogConfiguration(...).IsEnabled wevtutil sl "..." /e:true
9 TerminalServices-RemoteConnectionManager/Operational log enabled Same approach wevtutil sl "..." /e:true
10 RdpCoreTS/Operational log enabled Same approach wevtutil sl "..." /e:true
11 Service account privileges Check service runs as LocalSystem or has SeSecurityPrivilege Note displayed
12 RdpAudit data directory writable Directory.CreateDirectory + temp write test Create directory with proper ACL
13 SQLite database accessible Attempt to open connection to DB file Create DB (run migrations)
14 Object Access Auditing enabled auditpol /get /subcategory:"File System" and "Registry" — check Success enabled auditpol /set /subcategory:"File System" /success:enable + "Registry" /success:enable
15 LSASS Protection (RunAsPPL) Read HKLM\SYSTEM\CurrentControlSet\Control\LsaRunAsPPL = 1 Write REG_DWORD RunAsPPL = 1 (elevated, requires reboot)

UI for each item:

  • Status icon: ✅ (green check), ⚠️ (yellow warning), ❌ (red cross)
  • Description label
  • [Fix] button (enabled only when auto-fix is available and item fails)
  • [Re-check] button at bottom applies to all items

All fix operations run in background Task with progress indicator. Result shown in status label per row.

8.3 Page 2: Audit Policy

This page checks and controls all Windows audit policy settings and event log sizes required for comprehensive RDP monitoring.

Audit policy checks using auditpol.exe /get /subcategory:"..." /r (parse CSV output):

Subcategory Required Setting auditpol Command to Enable
Logon Success + Failure auditpol /set /subcategory:"Logon" /success:enable /failure:enable
Logoff Success auditpol /set /subcategory:"Logoff" /success:enable
Special Logon Success auditpol /set /subcategory:"Special Logon" /success:enable
Other Logon/Logoff Events Success + Failure auditpol /set /subcategory:"Other Logon/Logoff Events" /success:enable /failure:enable
Credential Validation Success + Failure auditpol /set /subcategory:"Credential Validation" /success:enable /failure:enable
Kerberos Authentication Service Success + Failure auditpol /set /subcategory:"Kerberos Authentication Service" /success:enable /failure:enable
Kerberos Service Ticket Operations Success + Failure auditpol /set /subcategory:"Kerberos Service Ticket Operations" /success:enable /failure:enable
Process Creation Success auditpol /set /subcategory:"Process Creation" /success:enable
Process Termination Success auditpol /set /subcategory:"Process Termination" /success:enable
Security System Extension Success auditpol /set /subcategory:"Security System Extension" /success:enable
Other Object Access Events Success auditpol /set /subcategory:"Other Object Access Events" /success:enable
File System Success auditpol /set /subcategory:"File System" /success:enable
Registry Success auditpol /set /subcategory:"Registry" /success:enable
User Account Management Success + Failure auditpol /set /subcategory:"User Account Management" /success:enable /failure:enable
Security Group Management Success auditpol /set /subcategory:"Security Group Management" /success:enable

Registry checks:

Setting Registry Path Value Fix Action
Process cmdline logging HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\AuditProcessCreationIncludeCmdLine_Enabled REG_DWORD = 1 Write registry value
LSASS Protection HKLM\SYSTEM\CurrentControlSet\Control\LsaRunAsPPL REG_DWORD = 1 Write registry value (requires reboot)

SACL configuration (required for EventIDs 4656/4657/4663):

The Configurator must offer a [Configure SACLs] button that applies the following SACL settings via PowerShell (elevated). These SACLs are mandatory for T1546.008, T1572, and T1003 detection:

# Apply SACL to IFEO registry key (T1546.008 Sticky Keys detection)
$key = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options"
$acl = Get-Acl $key
$rule = New-Object System.Security.AccessControl.RegistryAuditRule(
    "Everyone", "SetValue,CreateSubKey",
    "ContainerInherit,ObjectInherit", "None", "Success")
$acl.AddAuditRule($rule)
Set-Acl $key $acl

# Apply SACL to RDP-Tcp registry key (T1572 RDP Port Change detection)
$key = "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp"
$acl = Get-Acl $key
$rule = New-Object System.Security.AccessControl.RegistryAuditRule(
    "Everyone", "SetValue", "None", "None", "Success")
$acl.AddAuditRule($rule)
Set-Acl $key $acl

# Apply SACL to LSA registry key (T1003 LSASS PPL tamper detection)
$key = "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa"
$acl = Get-Acl $key
$rule = New-Object System.Security.AccessControl.RegistryAuditRule(
    "Everyone", "SetValue", "None", "None", "Success")
$acl.AddAuditRule($rule)
Set-Acl $key $acl

Event Log size checks:

Log Minimum Size Check Fix
Security 512 MB EventLog.MaximumKilobytes Set to 524288 KB
System 64 MB Same Set to 65536 KB

Each row in the UI: status icon + description + current value + [Apply] button. [Apply All] button at bottom applies all pending fixes. All operations run elevated via UAC prompt (runas).

8.4 Page 3: Service

Service status panel (top):

  • Large status indicator: ● RUNNING (green) / ● STOPPED (red) / ● PENDING (yellow)
  • Service name, display name, start type
  • Process ID, uptime, .NET version
  • Statistics from IPC GetStatus: events captured, alerts, active sessions, DB size

Action buttons:

  • [Install Service] — runs sc create RdpAuditService binPath= "..." DisplayName= "RDP Audit Service" start= auto via Process (elevated). Shows a dialog to configure service credentials (default: LocalSystem).
  • [Uninstall Service] — runs sc delete RdpAuditService (elevated). Asks for confirmation. Option to retain database.
  • [Start]ServiceController.Start() + wait for Running with 10s timeout.
  • [Stop]ServiceController.Stop() + wait for Stopped with 10s timeout.
  • [Restart] — Stop then Start.
  • [Set Startup Type] — DropDown: Automatic / Manual / Disabled → sc config (elevated).

Live event log tail panel (bottom): RichTextBox, auto-scroll, last 200 lines from Serilog file or IPC push, refreshed every 2 seconds.

Recent Alerts panel: DataGridView showing last 10 unacknowledged alerts. Columns: Time, Severity (colored), RuleId, SourceIp, UserName, Message, [Ack] button.

8.5 Page 4: Settings

Form-based editor for RdpAuditOptions from appsettings.json. Fields:

Monitoring section:

  • CheckedListBox for EnabledChannels (one per channel with description)
  • CheckedListBox for event ID groups: Authentication / Sessions / Kerberos / Object Access / Processes / Persistence / Account Changes
  • Checkbox: FilterLocalAddresses
  • Checkbox: TrackProcessCreation
  • Checkbox: TrackScheduledTasks
  • Checkbox: TrackAccountChanges
  • Checkbox: TrackKerberos
  • Checkbox: TrackObjectAccess

Alerts section:

  • Checkbox + NumericUpDown: EnableBruteForceDetection / BruteForceThreshold / BruteForceWindowMinutes
  • Checkbox + TimePicker x 2: EnableOffHoursAlert / BusinessHoursStart / BusinessHoursEnd
  • Checkbox: EnableNewUserAlert
  • Checkbox: EnableExternalRdpAlert
  • TextBox: KerberosExpectedEncryptionType (default 0x12)
  • TextBox (comma-separated): LsassAccessWhitelistProcesses
  • TextBox (comma-separated): WhitelistIps
  • TextBox (comma-separated): WhitelistUsers

Firewall section:

  • Checkbox + NumericUpDown: AutoBlockBruteForce / AutoBlockThreshold
  • TextBox: BlockRuleName

Storage section:

  • NumericUpDown: EventRetentionDays (min 7, max 3650)
  • NumericUpDown: LogRetentionDays (min 7, max 365)
  • Label showing current DB size
  • [Open Log Folder] button
  • [Open DB Folder] button

Actions: [Save] — serialize to JSON and write to %ProgramData%\RdpAudit\appsettings.json. If service is running, send SaveSettings via IPC so the service hot-reloads via IOptionsMonitor<T>. [Restore Defaults] — reset to default values.


9. IPC Client (Configurator Side)

public class IpcClient : IDisposable
{
    private const string PipeName = "RdpAuditService";

    public async Task<T?> SendAsync<T>(IpcCommand command, object? payload = null,
        CancellationToken ct = default)
    {
        await using var pipe = new NamedPipeClientStream(".", PipeName,
            PipeDirection.InOut, PipeOptions.Asynchronous);
        await pipe.ConnectAsync(2000, ct);
        var request = new IpcRequest
        {
            Command = command,
            Payload = payload != null ? JsonSerializer.Serialize(payload) : null
        };
        var reqBytes = MessagePackSerializer.Serialize(request);
        await pipe.WriteAsync(BitConverter.GetBytes(reqBytes.Length), ct);
        await pipe.WriteAsync(reqBytes, ct);
        var lenBuf = new byte[4];
        await pipe.ReadExactlyAsync(lenBuf, ct);
        var respBytes = new byte[BitConverter.ToInt32(lenBuf)];
        await pipe.ReadExactlyAsync(respBytes, ct);
        var response = MessagePackSerializer.Deserialize<IpcResponse>(respBytes);
        if (!response.Success) throw new IpcException(response.Error);
        return response.Payload != null ? JsonSerializer.Deserialize<T>(response.Payload) : default;
    }

    public async Task<bool> IsServiceReachableAsync() { ... }
}

10. Windows Prerequisites: Detailed Check Implementation

10.1 Reading Audit Policy

public class AuditPolicyManager
{
    public record AuditPolicyEntry(string Subcategory, bool SuccessEnabled, bool FailureEnabled);

    public IEnumerable<AuditPolicyEntry> GetCurrentPolicy()
    {
        var result = RunProcess("auditpol.exe", "/get /category:* /r");
        return ParseCsvOutput(result);
    }

    public bool ApplyPolicy(string subcategory, bool success, bool failure)
    {
        var args = $"/set /subcategory:\"{subcategory}\"";
        if (success) args += " /success:enable"; else args += " /success:disable";
        if (failure) args += " /failure:enable"; else args += " /failure:disable";
        return RunElevated("auditpol.exe", args) == 0;
    }
}

10.2 Enabling Operational Logs

public static void EnableEventLog(string channelName)
{
    var config = new EventLogConfiguration(channelName);
    if (!config.IsEnabled)
    {
        config.IsEnabled = true;
        config.SaveChanges();
    }
    // Fallback: RunElevated("wevtutil.exe", $"sl \"{channelName}\" /e:true");
}

10.3 PowerShell 7 Detection

public static string? GetPowerShellCoreVersion()
{
    var regKey = Registry.LocalMachine.OpenSubKey(
        @"SOFTWARE\Microsoft\PowerShellCore\InstalledVersions");
    if (regKey != null) { /* parse latest version subkey */ }
    var pwsh = FindInPath("pwsh.exe");
    if (pwsh != null)
    {
        var output = RunProcess(pwsh, "--version");
        return output.Trim();
    }
    return null;
}

10.4 SACL Status Check

public static bool IsSaclConfigured(string registryPath)
{
    try
    {
        var key = Registry.LocalMachine.OpenSubKey(
            registryPath.Replace("HKLM:\\", ""),
            RegistryKeyPermissionCheck.ReadSubTree,
            RegistryRights.ReadPermissions | RegistryRights.ReadKey);
        if (key == null) return false;
        var security = key.GetAccessControl(AccessControlSections.Audit);
        return security.GetAuditRules(true, true, typeof(SecurityIdentifier)).Count > 0;
    }
    catch { return false; }
}

11. Service Installation & Deployment

11.1 Self-Contained Publish Command

dotnet publish src/RdpAudit.Service/RdpAudit.Service.csproj \
  -c Release -r win-x64 --self-contained true \
  -p:PublishSingleFile=true \
  -o ./publish/Service

dotnet publish src/RdpAudit.Configurator/RdpAudit.Configurator.csproj \
  -c Release -r win-x64 --self-contained true \
  -p:PublishSingleFile=true \
  -o ./publish/Configurator

11.2 Service Registration

Performed by the Configurator via elevated sc.exe calls:

sc create RdpAuditService \
  binPath= "C:\Program Files\RdpAudit\RdpAudit.Service.exe" \
  DisplayName= "RDP Audit Service" \
  start= auto \
  obj= LocalSystem

sc description RdpAuditService "Monitors and records Windows RDP security events"
sc failure RdpAuditService reset= 3600 actions= restart/5000/restart/10000/restart/30000

11.3 Data Directory Setup

Create %ProgramData%\RdpAudit\ with:

  • SYSTEM: Full Control
  • Builtin\Administrators: Full Control
  • Builtin\Users: Read (for Configurator to open DB read-only)

11.4 EF Core Migrations

On service first start, call dbContext.Database.MigrateAsync() before starting workers. Migrations are embedded in the service binary.


12. Firewall Integration

The WindowsFirewallService interface:

public interface IFirewallService
{
    bool BlockAddress(string ip, string ruleName, string description);
    bool UnblockAddress(string ip, string ruleName);
    bool RuleExists(string ruleName);
    IEnumerable<string> GetBlockedAddresses(string ruleName);
}

Auto-block logic (when AutoBlockBruteForce = true): called by AlertWorker after generating a BRUTE_FORCE_01 alert if Address.FailCount >= AutoBlockThreshold. Update Address.IsBlocked = true, BlockedAt, BlockReason.


13. Logging Strategy

Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()
    .Enrich.WithProperty("ServiceVersion", Assembly.GetExecutingAssembly()
        .GetName().Version?.ToString())
    .WriteTo.EventLog("RdpAudit", restrictedToMinimumLevel: LogEventLevel.Warning)
    .WriteTo.File(new CompactJsonFormatter(), logPath,
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 30,
        shared: true)
    .CreateLogger();

14. Security Considerations

  1. Named Pipe ACL: Restrict to Builtin\Administrators only.
  2. DB file ACL: Service writes as SYSTEM; Configurator reads as admin user.
  3. Settings file ACL: %ProgramData%\RdpAudit\appsettings.json — write access for Administrators only.
  4. No sensitive data logging: Never log plaintext passwords. Event XML stored in Details field without sanitization, but log output omits it.
  5. Privilege: Service runs as LocalSystem. The Configurator uses runas-elevation only for specific operations.
  6. SQL injection: EF Core with parameterized queries exclusively.
  7. Event XML size limit: Cap Details field at 64KB. Truncate with warning if exceeded.

15. Error Handling & Resilience

  • EventLogWatcher failure: Catch EventLogException, restart watcher after 30-second backoff. Max 10 retries.
  • DB write failure: Catch SqliteException, retry with exponential backoff (100ms, 200ms, 400ms). After 5 failures, drop the batch and log critical.
  • IPC connection failure: Configurator shows "Service unreachable" — never throw to UI.
  • Channel full (backpressure): Log dropped event count every minute. Never block the EventLogWatcher thread.
  • Bookmark corruption: If bookmark XML fails to deserialize, fall back to DateTime.UtcNow.AddDays(-1).

16. Testing Requirements

16.1 Unit Tests (xUnit)

  • AlertRuleTests: Each of the 21 alert rules tested with mock IAlertContext. Cover true-positive, false-positive, and threshold edge cases.
  • EventNormalizerTests: Parse sample EventRecord XML for each EventID in the catalog. Verify field extraction including AuthPackage, ObjectName, AccessMask.
  • AuditPolicyParserTests: Parse sample auditpol /r CSV output.
  • IpcSerializerTests: Round-trip serialize/deserialize all message types via MessagePack.
  • IpClassificationTests: Verify IsPublicIp() for RFC1918, loopback, link-local, APIPA, and public ranges.
  • SaclCheckTests: Verify IsSaclConfigured() returns correct result for mocked registry paths.

16.2 Integration Tests

  • DatabaseTests: In-memory SQLite to verify schema, migrations, and bulk insert performance (target: 10,000 events in < 2 seconds).
  • BookmarkTests: Verify bookmark save/load across service restart simulation.

17. Implementation Priorities for AI Agent

Execute implementation in this order:

  1. RdpAudit.Core: Models, DbContext, migrations, config schema, IPC contracts
  2. RdpAudit.Service: Program.cs, EventCollectorWorker, EventProcessorWorker (incl. IsPublicIp)
  3. RdpAudit.Service: AlertWorker with all 21 alert rules
  4. RdpAudit.Service: IpcServerWorker, MaintenanceWorker
  5. RdpAudit.Configurator: MainForm shell + IpcClient
  6. RdpAudit.Configurator: PrerequisitesPage + AuditPolicyPage (read-only check first)
  7. RdpAudit.Configurator: AuditPolicyPage — SACL configuration via PowerShell (elevated)
  8. RdpAudit.Configurator: ServicePage (status + actions)
  9. RdpAudit.Configurator: SettingsPage + all fix actions (elevated)
  10. Unit tests for all 21 alert rules and event normalization
  11. publish.ps1 script for both binaries

18. Additional Notes for the AI Agent

  • All code comments and identifiers must be in English.
  • All string literals shown to the user in the Configurator must be in English.
  • Use CancellationToken throughout — all async methods accept and honor it.
  • Use ILogger<T> injected via DI — no static log calls in workers.
  • All DateTime values stored and processed in UTC. Display local time only in the Configurator UI.
  • The service binary must support both --console argument (runs as console app for debugging) and normal service mode. Detect via WindowsServiceHelpers.IsWindowsService().
  • Use System.Text.Json for JSON (not Newtonsoft). Use JsonSerializerOptions with PropertyNamingPolicy = JsonNamingPolicy.CamelCase.
  • All P/Invoke declarations go in RdpAudit.Core/Interop/NativeMethods.cs.
  • EventRecord XML field access: always use evt.Properties[index].Value with bounds checking. Prefer XPath extraction via XmlDocument for reliability across OS versions.
  • For PASS_THE_HASH rule: mark alert Details with "heuristic": true — the absence of EventID 4648 is a heuristic, not a guarantee.
  • For GOLDEN_TICKET rule: disable the check entirely if KerberosExpectedEncryptionType is empty — the domain may use mixed encryption types.
  • For LSASS_ACCESS rule: false positives from AV/EDR are common. Always check ProcessName against LsassAccessWhitelistProcesses before firing.

19. MITRE ATT&CK Coverage Matrix

MITRE ID Technique EventIDs Used Alert Rule(s) Coverage
T1110.001 Brute Force — Password Guessing 4625 BRUTE_FORCE_01 Full
T1110.002 Brute Force — Password Spraying 4625, 4776 BRUTE_FORCE_01, BRUTE_FORCE_NTLM Full
T1110.003 Brute Force — Kerberospraying 4771 KERBEROS_SPRAY Full
T1021.001 Remote Desktop Protocol 4624 Type 10, 1149, 21 EXTERNAL_RDP_LOGIN, UNKNOWN_IP_SUCCESS Full
T1563.002 RDP Session Hijacking (tscon) 4688, 24→25 RDP_SESSION_HIJACK, RAPID_RECONNECT Full
T1550.002 Pass the Hash 4624 NTLM LogonType 3/9 PASS_THE_HASH Heuristic
T1550.003 Pass the Ticket / Golden Ticket 4769 RC4 GOLDEN_TICKET Heuristic
T1546.008 Sticky Keys / Accessibility Features 4657 IFEO, 4688 winlogon parent STICKY_KEYS_BACKDOOR Full
T1003.001 Credential Dumping — LSASS Memory 4656 lsass AccessMask LSASS_ACCESS Full
T1003 LSASS PPL Protection Tamper 4657 RunAsPPL LSASS_PPL_TAMPER Full
T1572 Protocol Tunneling — RDP Port 4657 PortNumber RDP_PORT_CHANGED Full
T1098.002 Account Manipulation — RDP Group 4732 Remote Desktop Users PRIVILEGED_GROUP_CHANGE Full
T1133 External Remote Services 1149, 4624 public IP EXTERNAL_RDP_LOGIN Full
T1136 Create Account 4720 NEW_ACCOUNT Full
T1053.005 Scheduled Task Persistence 4698, 4702 TASK_PERSISTENCE, TASK_MODIFIED Full
T1543.003 Create/Modify System Service 4697 SERVICE_INSTALL Full
T1059 Command/Script Execution via RDP 4688 parent=mstsc/rdpclip PROCESS_ANOMALY Full
T1078 Valid Accounts — Privileged 4672 PRIVILEGED_LOGIN Full
T1040 Network Sniffing Not covered Network-level only (IDS required)
T1557 AiTM / NTLM Relay Not covered Network-level only (IDS required)

Clone this wiki locally