-
Notifications
You must be signed in to change notification settings - Fork 0
RdpAudit Service ‐ Technical Specification v2.0
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/AccessMaskfields, LSASS RunAsPPL prerequisite check.
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).
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 |
RdpAudit.Core.dll — models, database context, configuration schema, IPC contracts.
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/
<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.*" /><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.*" /><PackageReference Include="Microsoft.Win32.Registry" Version="5.*" />
<PackageReference Include="MessagePack" Version="2.*" />
<!-- WinForms is included via <UseWindowsForms>true</UseWindowsForms> -->All projects: net8.0-windows, <PlatformTarget>x64</PlatformTarget>, <RuntimeIdentifier>win-x64</RuntimeIdentifier>.
SQLite file located at %ProgramData%\RdpAudit\rdpaudit.db. Connection string in appsettings.json. WAL mode and synchronous=NORMAL set via PRAGMA at context initialization.
The service monitors ALL of the following events. Each event descriptor specifies the log channel, EventID, required audit policy, and fields to extract.
| 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 |
| 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=
NTLMis the primary indicator. Always extract and store theAuthPackagefield for PtH correlation.
| 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 |
| 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 |
| 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 |
| 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
NewProcessNameends withtscon.exe— this is the primary indicator of session theft. Also monitormstsc.exewithCommandLinecontaining/shadowfor 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.exeANDNewProcessNameends withcmd.exeorpowershell.exe— this indicates execution of a backdoor triggered from the Windows logon screen.
| 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 |
| 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 |
| 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 |
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 storeTicketEncryptionTypeas a dedicated field in theDetailsJSON.
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
ObjectNamecontainsImage File Execution Options\sethc,\utilman,\osk.exe,\magnify.exe,\narrator.exe, or\displayswitch.exe.
T1572 (RDP Port Change): Monitor EventID 4657 where
ObjectNamecontainsTerminal Server\WinStations\RDP-TcpandValueName = PortNumber.
T1003 (LSASS Credential Dump): Monitor EventID 4656 where
ObjectType = Process,ObjectNamecontainslsass.exe, andAccessMaskincludes0x10(PROCESS_VM_READ) or0x1FFFFF(PROCESS_ALL_ACCESS). Also monitor EventID 4657 whereObjectNamecontainsLsaandValueName = RunAsPPLfor tamper detection.
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -32000;
PRAGMA foreign_keys = ON;
PRAGMA auto_vacuum = INCREMENTAL;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
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)
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
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 }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; }
}public class DbProp
{
public string Key { get; set; } // PRIMARY KEY
public string Value { get; set; }
public DateTime UpdatedUtc { get; set; }
}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"
}
}
]
}
}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();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
EventLogBookmarkfromBookmarktable at startup. If no bookmark exists, create one fromDateTime.UtcNow.AddDays(-1)to catch up recent events. - On
EventRecordWritten, serialize the EventRecord XML to theDetailsfield 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:
DropOldestwith a warning log. - Handle
EventLogWatcherrestart onEntryWrittenEventArgs.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);
}
}
// ...
}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 afterSaveChangesAsync. - Batch size: 100 events or 500ms timeout (use
PeriodicTimerfor timeout drain). - Field normalization:
- Filter
SourceIpvalues"::1","127.0.0.1","-","0.0.0.0","LOCAL"→ set tonull. - Parse
UserNamefromDOMAIN\Userformat, store domain separately. - Normalize
LogonIdto lowercase hex string. - Populate
Address.IsPublicIp:trueif 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.
- Filter
- Session resolution: On EventID 21 (LogonSucceeded with non-null SourceIp), create a new
Sessionrecord. On EventID 23/24/25/4634/4647, update the matching session'sDisconnectUtc/LogoffUtc/Status. Match sessions byWtsSessionId+Status == Active. - Address resolution: For every non-null
SourceIp, upsertAddressrecord (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));
}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
SourceIpfrom the DB to evaluate frequency. - Mark
Processed = truein 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 |
Responsibility: Host a Named Pipe server \\.\pipe\RdpAuditService for bidirectional communication with the Configurator.
Implementation rules:
- Use
NamedPipeServerStreamin async mode withPipeDirection.InOut,PipeTransmissionMode.Message. - Support multiple concurrent connections (max 10) via a loop that creates a new pipe instance per accepted connection.
- Grant
PipeAccessRuletoBuiltin\Administratorsfor 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; }
}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.
Main window: TabControl with four pages:
- Prerequisites — system component checks
- Audit Policy — Windows audit and log settings
- Service — service lifecycle management
- 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.
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\Lsa → RunAsPPL = 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.
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\Audit → ProcessCreationIncludeCmdLine_Enabled
|
REG_DWORD = 1 | Write registry value |
| LSASS Protection |
HKLM\SYSTEM\CurrentControlSet\Control\Lsa → RunAsPPL
|
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 $aclEvent 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).
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]— runssc create RdpAuditService binPath= "..." DisplayName= "RDP Audit Service" start= autovia Process (elevated). Shows a dialog to configure service credentials (default: LocalSystem). -
[Uninstall Service]— runssc 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.
Form-based editor for RdpAuditOptions from appsettings.json. Fields:
Monitoring section:
-
CheckedListBoxforEnabledChannels(one per channel with description) -
CheckedListBoxfor 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 +
TimePickerx 2:EnableOffHoursAlert/BusinessHoursStart/BusinessHoursEnd - Checkbox:
EnableNewUserAlert - Checkbox:
EnableExternalRdpAlert -
TextBox:KerberosExpectedEncryptionType(default0x12) -
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.
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() { ... }
}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;
}
}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");
}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;
}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; }
}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
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
Create %ProgramData%\RdpAudit\ with:
-
SYSTEM: Full Control -
Builtin\Administrators: Full Control -
Builtin\Users: Read (for Configurator to open DB read-only)
On service first start, call dbContext.Database.MigrateAsync() before starting workers. Migrations are embedded in the service binary.
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.
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();-
Named Pipe ACL: Restrict to
Builtin\Administratorsonly. - DB file ACL: Service writes as SYSTEM; Configurator reads as admin user.
-
Settings file ACL:
%ProgramData%\RdpAudit\appsettings.json— write access for Administrators only. -
No sensitive data logging: Never log plaintext passwords. Event XML stored in
Detailsfield without sanitization, but log output omits it. -
Privilege: Service runs as
LocalSystem. The Configurator usesrunas-elevation only for specific operations. - SQL injection: EF Core with parameterized queries exclusively.
-
Event XML size limit: Cap
Detailsfield at 64KB. Truncate with warning if exceeded.
-
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).
-
AlertRuleTests: Each of the 21 alert rules tested with mockIAlertContext. Cover true-positive, false-positive, and threshold edge cases. -
EventNormalizerTests: Parse sample EventRecord XML for each EventID in the catalog. Verify field extraction includingAuthPackage,ObjectName,AccessMask. -
AuditPolicyParserTests: Parse sampleauditpol /rCSV output. -
IpcSerializerTests: Round-trip serialize/deserialize all message types via MessagePack. -
IpClassificationTests: VerifyIsPublicIp()for RFC1918, loopback, link-local, APIPA, and public ranges. -
SaclCheckTests: VerifyIsSaclConfigured()returns correct result for mocked registry paths.
-
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.
Execute implementation in this order:
-
RdpAudit.Core: Models, DbContext, migrations, config schema, IPC contracts -
RdpAudit.Service: Program.cs, EventCollectorWorker, EventProcessorWorker (incl.IsPublicIp) -
RdpAudit.Service: AlertWorker with all 21 alert rules -
RdpAudit.Service: IpcServerWorker, MaintenanceWorker -
RdpAudit.Configurator: MainForm shell + IpcClient -
RdpAudit.Configurator: PrerequisitesPage + AuditPolicyPage (read-only check first) -
RdpAudit.Configurator: AuditPolicyPage — SACL configuration via PowerShell (elevated) -
RdpAudit.Configurator: ServicePage (status + actions) -
RdpAudit.Configurator: SettingsPage + all fix actions (elevated) - Unit tests for all 21 alert rules and event normalization
-
publish.ps1script for both binaries
- All code comments and identifiers must be in English.
- All string literals shown to the user in the Configurator must be in English.
- Use
CancellationTokenthroughout — all async methods accept and honor it. - Use
ILogger<T>injected via DI — nostaticlog calls in workers. - All
DateTimevalues stored and processed in UTC. Display local time only in the Configurator UI. - The service binary must support both
--consoleargument (runs as console app for debugging) and normal service mode. Detect viaWindowsServiceHelpers.IsWindowsService(). - Use
System.Text.Jsonfor JSON (not Newtonsoft). UseJsonSerializerOptionswithPropertyNamingPolicy = JsonNamingPolicy.CamelCase. - All P/Invoke declarations go in
RdpAudit.Core/Interop/NativeMethods.cs. - EventRecord XML field access: always use
evt.Properties[index].Valuewith bounds checking. Prefer XPath extraction viaXmlDocumentfor reliability across OS versions. - For
PASS_THE_HASHrule: mark alert Details with"heuristic": true— the absence of EventID 4648 is a heuristic, not a guarantee. - For
GOLDEN_TICKETrule: disable the check entirely ifKerberosExpectedEncryptionTypeis empty — the domain may use mixed encryption types. - For
LSASS_ACCESSrule: false positives from AV/EDR are common. Always checkProcessNameagainstLsassAccessWhitelistProcessesbefore firing.
| 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) |