From 92b38b3658e2b4bcd2195cf5f44eaeafaf3e0006 Mon Sep 17 00:00:00 2001 From: Lucas Falslev Date: Mon, 8 Dec 2025 16:59:38 -0700 Subject: [PATCH 1/3] write to compStatus for CA lookups --- src/Runtime/CollectionTask.cs | 7 ++-- src/Runtime/LDAPConsumer.cs | 6 ++-- src/Runtime/ObjectProcessors.cs | 62 +++++++++++++++++++-------------- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/Runtime/CollectionTask.cs b/src/Runtime/CollectionTask.cs index c0ada8f..e186b08 100644 --- a/src/Runtime/CollectionTask.cs +++ b/src/Runtime/CollectionTask.cs @@ -99,7 +99,6 @@ internal async Task StartCollection() await _outputChannel.Writer.WriteAsync(wkp); } - _outputChannel.Writer.Complete(); _compStatusChannel?.Writer.Complete(); _log.LogInformation("Output channel closed, waiting for output task to complete"); @@ -112,7 +111,7 @@ internal async Task StartCollection() internal async Task ConsumeSearchResults() { var log = _context.Logger; - var processor = new ObjectProcessors(_context, log); + var processor = new ObjectProcessors(_context, log, _compStatusChannel); var watch = new Stopwatch(); var threadId = Thread.CurrentThread.ManagedThreadId; @@ -130,7 +129,7 @@ internal async Task ConsumeSearchResults() log.LogTrace("Consumer {ThreadID} started processing {obj} ({type})", threadId, res.DisplayName, res.ObjectType); watch.Start(); - var processed = await processor.ProcessObject(item, res, _compStatusChannel); + var processed = await processor.ProcessObject(item, res); watch.Stop(); log.LogTrace("Consumer {ThreadID} took {time} ms to process {obj}", threadId, watch.Elapsed.TotalMilliseconds, res.DisplayName); @@ -147,6 +146,8 @@ internal async Task ConsumeSearchResults() { log.LogError(e, "error in consumer"); } + + processor.ClearEventHandlers(); log.LogDebug("Consumer task on thread {id} completed", Thread.CurrentThread.ManagedThreadId); } diff --git a/src/Runtime/LDAPConsumer.cs b/src/Runtime/LDAPConsumer.cs index 952b0f9..ced13f0 100644 --- a/src/Runtime/LDAPConsumer.cs +++ b/src/Runtime/LDAPConsumer.cs @@ -18,7 +18,7 @@ internal static async Task ConsumeSearchResults(Channel inputC int id) { var log = context.Logger; - var processor = new ObjectProcessors(context, log); + var processor = new ObjectProcessors(context, log, computerStatusChannel); var watch = new Stopwatch(); var threadId = Thread.CurrentThread.ManagedThreadId; @@ -34,7 +34,7 @@ internal static async Task ConsumeSearchResults(Channel inputC log.LogTrace("Consumer {ThreadID} started processing {obj} ({type})", threadId, res.DisplayName, res.ObjectType); watch.Start(); - var processed = await processor.ProcessObject(item, res, computerStatusChannel); + var processed = await processor.ProcessObject(item, res); watch.Stop(); log.LogTrace("Consumer {ThreadID} took {time} ms to process {obj}", threadId, watch.Elapsed.TotalMilliseconds, res.DisplayName); @@ -51,6 +51,8 @@ internal static async Task ConsumeSearchResults(Channel inputC { log.LogError(e, "error in consumer"); } + + processor.ClearEventHandlers(); log.LogDebug("Consumer task on thread {id} completed", Thread.CurrentThread.ManagedThreadId); } diff --git a/src/Runtime/ObjectProcessors.cs b/src/Runtime/ObjectProcessors.cs index f5571af..7ef7a69 100644 --- a/src/Runtime/ObjectProcessors.cs +++ b/src/Runtime/ObjectProcessors.cs @@ -40,7 +40,9 @@ public class ObjectProcessors { private readonly WebClientServiceProcessor _webClientProcessor; private readonly SmbProcessor _smbProcessor; private readonly ConcurrentDictionary _registryProcessorMap = new(); - public ObjectProcessors(IContext context, ILogger log) { + private readonly Channel _compStatusChannel; + + public ObjectProcessors(IContext context, ILogger log, Channel compStatusChannel) { _context = context; _aclProcessor = new ACLProcessor(context.LDAPUtils); _spnProcessor = new SPNProcessors(context.LDAPUtils); @@ -63,15 +65,29 @@ public ObjectProcessors(IContext context, ILogger log) { _methods = context.ResolvedCollectionMethods; _cancellationToken = context.CancellationTokenSource.Token; _log = log; + _compStatusChannel = compStatusChannel; + + _certAbuseProcessor.ComputerStatusEvent += HandleCompStatusEvent; + } + + internal void ClearEventHandlers() { + _certAbuseProcessor.ComputerStatusEvent -= HandleCompStatusEvent; + } + + private async Task HandleCompStatusEvent(CSVComputerStatus status) { + try { + await _compStatusChannel.Writer.WriteAsync(status, _cancellationToken); + } catch (Exception e) { + _log.LogWarning(e, "Caught exception writing to compstatus writer"); + } } - internal async Task ProcessObject(IDirectoryObject entry, - ResolvedSearchResult resolvedSearchResult, Channel compStatusChannel) { + internal async Task ProcessObject(IDirectoryObject entry, ResolvedSearchResult resolvedSearchResult) { switch (resolvedSearchResult.ObjectType) { case Label.User: return await ProcessUserObject(entry, resolvedSearchResult); case Label.Computer: - return await ProcessComputerObject(entry, resolvedSearchResult, compStatusChannel); + return await ProcessComputerObject(entry, resolvedSearchResult); case Label.Group: return await ProcessGroupObject(entry, resolvedSearchResult); case Label.GPO: @@ -88,7 +104,7 @@ internal async Task ProcessObject(IDirectoryObject entry, case Label.AIACA: return await ProcessAIACA(entry, resolvedSearchResult); case Label.EnterpriseCA: - return await ProcessEnterpriseCA(entry, resolvedSearchResult, compStatusChannel); + return await ProcessEnterpriseCA(entry, resolvedSearchResult); case Label.NTAuthStore: return await ProcessNTAuthStore(entry, resolvedSearchResult); case Label.CertTemplate: @@ -208,8 +224,7 @@ await _containerProcessor.GetContainingObject(dn) is (true, var container)) private async Task ProcessComputerObject( IDirectoryObject entry, - ResolvedSearchResult resolvedSearchResult, - Channel compStatusChannel + ResolvedSearchResult resolvedSearchResult ) { var ret = new Computer { ObjectIdentifier = resolvedSearchResult.ObjectId, @@ -280,8 +295,7 @@ await _containerProcessor.GetContainingObject(dn) is (true, var container)) { var availability = await _computerAvailability.IsComputerAvailable(resolvedSearchResult, entry); if (!availability.Connectable) { - await compStatusChannel.Writer.WriteAsync(availability.GetCSVStatus(resolvedSearchResult.DisplayName), - _cancellationToken); + await HandleCompStatusEvent(availability.GetCSVStatus(resolvedSearchResult.DisplayName)); ret.Status = availability; return ret; } @@ -297,12 +311,12 @@ await compStatusChannel.Writer.WriteAsync(availability.GetCSVStatus(resolvedSear resolvedSearchResult.ObjectId, resolvedSearchResult.Domain); ret.Sessions = sessionResult; if (_context.Flags.DumpComputerStatus) - await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus { + await HandleCompStatusEvent(new CSVComputerStatus { Status = sessionResult.Collected ? StatusSuccess : sessionResult.FailureReason, Task = "NetSessionEnum", ComputerName = resolvedSearchResult.DisplayName, ObjectId = resolvedSearchResult.ObjectId, - }, _cancellationToken); + }); } if (_methods.HasFlag(CollectionMethod.LoggedOn)) { @@ -313,12 +327,12 @@ await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus { ret.PrivilegedSessions = privSessionResult; if (_context.Flags.DumpComputerStatus) - await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus { + await HandleCompStatusEvent(new CSVComputerStatus { Status = privSessionResult.Collected ? StatusSuccess : privSessionResult.FailureReason, Task = "NetWkstaUserEnum", ComputerName = resolvedSearchResult.DisplayName, ObjectId = resolvedSearchResult.ObjectId, - }, _cancellationToken); + }); if (!_context.Flags.NoRegistryLoggedOn) { await _context.DoDelay(); @@ -326,12 +340,12 @@ await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus { resolvedSearchResult.Domain, resolvedSearchResult.ObjectId); ret.RegistrySessions = registrySessionResult; if (_context.Flags.DumpComputerStatus) - await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus { + await HandleCompStatusEvent(new CSVComputerStatus { Status = registrySessionResult.Collected ? StatusSuccess : registrySessionResult.FailureReason, Task = "RegistrySessions", ComputerName = resolvedSearchResult.DisplayName, ObjectId = resolvedSearchResult.ObjectId, - }, _cancellationToken); + }); } } @@ -704,9 +718,7 @@ private async Task ProcessAIACA(IDirectoryObject entry, ResolvedSearchRes return ret; } - private async Task ProcessEnterpriseCA(IDirectoryObject entry, - ResolvedSearchResult resolvedSearchResult, - Channel compStatusChannel) { + private async Task ProcessEnterpriseCA(IDirectoryObject entry, ResolvedSearchResult resolvedSearchResult) { var ret = new EnterpriseCA { ObjectIdentifier = resolvedSearchResult.ObjectId, Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)) @@ -749,14 +761,13 @@ private async Task ProcessEnterpriseCA(IDirectoryObject entry, if (await _context.LDAPUtils.ResolveHostToSid(dnsHostName, resolvedSearchResult.DomainSid) is (true, var sid) && sid.StartsWith("S-1-")) { ret.HostingComputer = sid; - await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus + await HandleCompStatusEvent(new CSVComputerStatus { Status = ComputerStatus.Success, ComputerName = resolvedSearchResult.DisplayName, Task = nameof(ProcessEnterpriseCA), ObjectId = resolvedSearchResult.ObjectId, - }, - _cancellationToken); + }); } else { _log.LogWarning("CA {Name} host ({Dns}) could not be resolved to a SID.", caName, dnsHostName); } @@ -779,24 +790,23 @@ await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus if (caName != null && dnsHostName != null) { if (await _context.LDAPUtils.ResolveHostToSid(dnsHostName, resolvedSearchResult.DomainSid) is (true, var sid) && sid.StartsWith("S-1-")) { - await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus + await HandleCompStatusEvent(new CSVComputerStatus { Status = ComputerStatus.Success, ComputerName = resolvedSearchResult.DisplayName, Task = nameof(ProcessEnterpriseCA), ObjectId = sid, - }, - _cancellationToken); + }); ret.HostingComputer = sid; } else { _log.LogWarning("CA {Name} host ({Dns}) could not be resolved to a SID.", caName, dnsHostName); } CARegistryData cARegistryData = new() { - IsUserSpecifiesSanEnabled = _certAbuseProcessor.IsUserSpecifiesSanEnabled(dnsHostName, caName), + IsUserSpecifiesSanEnabled = await _certAbuseProcessor.IsUserSpecifiesSanEnabled(dnsHostName, caName, ret.HostingComputer), EnrollmentAgentRestrictions = await _certAbuseProcessor.ProcessEAPermissions(caName, resolvedSearchResult.Domain, dnsHostName, ret.HostingComputer), - RoleSeparationEnabled = _certAbuseProcessor.RoleSeparationEnabled(dnsHostName, caName), + RoleSeparationEnabled = await _certAbuseProcessor.RoleSeparationEnabled(dnsHostName, caName, ret.HostingComputer), // The CASecurity exist in the AD object DACL and in registry of the CA server. We prefer to use the values from registry as they are the ground truth. // If changes are made on the CA server, registry and the AD object is updated. If changes are made directly on the AD object, the CA server registry is not updated. From e6da9e746af72b49fa177fde7e2a4021afc31a74 Mon Sep 17 00:00:00 2001 From: Lucas Falslev Date: Mon, 15 Dec 2025 10:27:10 -0700 Subject: [PATCH 2/3] update certAbuseProcessor call --- src/Runtime/ObjectProcessors.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Runtime/ObjectProcessors.cs b/src/Runtime/ObjectProcessors.cs index 7ef7a69..860952c 100644 --- a/src/Runtime/ObjectProcessors.cs +++ b/src/Runtime/ObjectProcessors.cs @@ -50,7 +50,7 @@ public ObjectProcessors(IContext context, ILogger log, Channel Date: Tue, 16 Dec 2025 08:16:04 -0700 Subject: [PATCH 3/3] register compstatus event handler for all processors --- src/Runtime/ObjectProcessors.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Runtime/ObjectProcessors.cs b/src/Runtime/ObjectProcessors.cs index 860952c..995ac55 100644 --- a/src/Runtime/ObjectProcessors.cs +++ b/src/Runtime/ObjectProcessors.cs @@ -67,10 +67,22 @@ public ObjectProcessors(IContext context, ILogger log, Channel