From 818fa53728205f425ae2c44be41377ec5d25367f Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Thu, 23 Apr 2026 14:57:44 +0200 Subject: [PATCH] v2 single-rail 5.7: migrate Analyzer trio + AgentAnalyzerManager to Signal-Post (B24-B26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full migration — the 3 analyzer classes and their manager all move to InformationalEventPost, no Action-bypass remains in the analyzer surface (no dual-use concerns; no standalone CLI mode touches these types). Analyzers (1 Emit-site each): * LocalAdminAnalyzer (B24): local_admin_analysis. * SoftwareInventoryAnalyzer (B25): software_inventory_analysis (startup baseline + shutdown delta; WG-part discriminator unchanged). * IntegrityBypassAnalyzer (B26): integrity_bypass_analysis. Manager: * AgentAnalyzerManager stores InformationalEventPost _post and passes it to all 3 analyzer constructors in Initialize(). Program.cs wiring: * AgentAnalyzerManager construction deferred into orchestrator.Start's onIngressReady hook so lifecyclePost is non-null at construction time. The outer-scope `AgentAnalyzerManager analyzerManager = null;` declaration is captured by both the later RunStartup() call (after Start returns) and the terminationHandler constructor (inside the same hook). Tests: * AgentAnalyzerManagerTests helper NewPost() returns a throwaway InformationalEventPost(new FakeSignalIngressSink(), VirtualClock) for the 7 ctor sites including the null-arg test. * 538 V2.Core + 108 DecisionCore = 646 green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Runtime/AgentAnalyzerManagerTests.cs | 26 +++++++++++------- .../Analyzers/IntegrityBypassAnalyzer.cs | 9 ++++--- .../Telemetry/Analyzers/LocalAdminAnalyzer.cs | 9 ++++--- .../Analyzers/SoftwareInventoryAnalyzer.cs | 9 ++++--- .../Runtime/AgentAnalyzerManager.cs | 13 ++++----- .../AutopilotMonitor.Agent.V2/Program.cs | 27 +++++++++++++------ 6 files changed, 58 insertions(+), 35 deletions(-) diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Runtime/AgentAnalyzerManagerTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Runtime/AgentAnalyzerManagerTests.cs index 0d037a73..cc382408 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Runtime/AgentAnalyzerManagerTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Runtime/AgentAnalyzerManagerTests.cs @@ -5,8 +5,10 @@ using System.Threading; using AutopilotMonitor.Agent.V2.Core.Configuration; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers; +using AutopilotMonitor.Agent.V2.Core.Orchestration; using AutopilotMonitor.Agent.V2.Core.Runtime; using AutopilotMonitor.Agent.V2.Core.Tests.Harness; +using AutopilotMonitor.Agent.V2.Core.Tests.Orchestration; using AutopilotMonitor.Shared.Models; using Xunit; @@ -24,12 +26,18 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Runtime private static AgentLogger NewLogger(string dir) => new AgentLogger(dir, AgentLogLevel.Info); + // Throwaway InformationalEventPost for tests that exercise manager lifecycle, not + // emission surface. Tracker events flow into a discarded FakeSignalIngressSink so + // tests that inject a FakeAnalyzer see no cross-talk from real analyzer emissions. + private static InformationalEventPost NewPost() => + new InformationalEventPost(new FakeSignalIngressSink(), new VirtualClock(new DateTime(2026, 4, 23, 10, 0, 0, DateTimeKind.Utc))); + [Fact] public void Initialize_respects_toggle_all_off_registers_no_analyzers() { using var tmp = new TempDirectory(); var sut = new AgentAnalyzerManager( - Config(), NewLogger(tmp.Path), _ => { }, + Config(), NewLogger(tmp.Path), NewPost(), new AnalyzerConfiguration { EnableLocalAdminAnalyzer = false, @@ -46,7 +54,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Runtime { using var tmp = new TempDirectory(); var sut = new AgentAnalyzerManager( - Config(), NewLogger(tmp.Path), _ => { }, + Config(), NewLogger(tmp.Path), NewPost(), new AnalyzerConfiguration { EnableLocalAdminAnalyzer = true, @@ -66,7 +74,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Runtime public void Initialize_tolerates_null_analyzer_config() { using var tmp = new TempDirectory(); - var sut = new AgentAnalyzerManager(Config(), NewLogger(tmp.Path), _ => { }, analyzerConfig: null); + var sut = new AgentAnalyzerManager(Config(), NewLogger(tmp.Path), NewPost(), analyzerConfig: null); sut.Initialize(); // Default AnalyzerConfiguration enables LocalAdmin + IntegrityBypass, disables SoftwareInventory. @@ -81,7 +89,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Runtime { using var tmp = new TempDirectory(); var sut = new AgentAnalyzerManager( - Config(), NewLogger(tmp.Path), _ => { }, + Config(), NewLogger(tmp.Path), NewPost(), new AnalyzerConfiguration { EnableLocalAdminAnalyzer = true }); sut.Initialize(); @@ -96,9 +104,9 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Runtime using var tmp = new TempDirectory(); var logger = NewLogger(tmp.Path); Assert.Throws(() => - new AgentAnalyzerManager(null!, logger, _ => { }, null)); + new AgentAnalyzerManager(null!, logger, NewPost(), null)); Assert.Throws(() => - new AgentAnalyzerManager(Config(), null!, _ => { }, null)); + new AgentAnalyzerManager(Config(), null!, NewPost(), null)); Assert.Throws(() => new AgentAnalyzerManager(Config(), logger, null!, null)); } @@ -149,7 +157,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Runtime public void RunShutdown_invokes_AnalyzeAtShutdown_on_every_analyzer() { using var tmp = new TempDirectory(); - var sut = new AgentAnalyzerManager(Config(), NewLogger(tmp.Path), _ => { }, new AnalyzerConfiguration()); + var sut = new AgentAnalyzerManager(Config(), NewLogger(tmp.Path), NewPost(), new AnalyzerConfiguration()); var fake = new FakeAnalyzer(); InjectAnalyzer(sut, fake); @@ -161,7 +169,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Runtime public void RunShutdown_swallows_analyzer_exceptions_and_continues() { using var tmp = new TempDirectory(); - var sut = new AgentAnalyzerManager(Config(), NewLogger(tmp.Path), _ => { }, new AnalyzerConfiguration()); + var sut = new AgentAnalyzerManager(Config(), NewLogger(tmp.Path), NewPost(), new AnalyzerConfiguration()); var fake = new FakeAnalyzer { ThrowOnShutdown = new InvalidOperationException("boom") }; InjectAnalyzer(sut, fake); @@ -175,7 +183,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Runtime public void RunShutdown_is_safe_on_empty_analyzer_list() { using var tmp = new TempDirectory(); - var sut = new AgentAnalyzerManager(Config(), NewLogger(tmp.Path), _ => { }, + var sut = new AgentAnalyzerManager(Config(), NewLogger(tmp.Path), NewPost(), new AnalyzerConfiguration { EnableLocalAdminAnalyzer = false, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/IntegrityBypassAnalyzer.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/IntegrityBypassAnalyzer.cs index e9480b58..09e7abce 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/IntegrityBypassAnalyzer.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/IntegrityBypassAnalyzer.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Management; using AutopilotMonitor.Agent.V2.Core.Logging; +using AutopilotMonitor.Agent.V2.Core.Orchestration; using AutopilotMonitor.Shared.Models; using Microsoft.Win32; @@ -34,7 +35,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers { private readonly string _sessionId; private readonly string _tenantId; - private readonly Action _emitEvent; + private readonly InformationalEventPost _post; private readonly AgentLogger _logger; public string Name => "IntegrityBypassAnalyzer"; @@ -63,12 +64,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers public IntegrityBypassAnalyzer( string sessionId, string tenantId, - Action emitEvent, + InformationalEventPost post, AgentLogger logger) { _sessionId = sessionId ?? throw new ArgumentNullException(nameof(sessionId)); _tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId)); - _emitEvent = emitEvent ?? throw new ArgumentNullException(nameof(emitEvent)); + _post = post ?? throw new ArgumentNullException(nameof(post)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -220,7 +221,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers } }; - _emitEvent(new EnrollmentEvent + _post.Emit(new EnrollmentEvent { SessionId = _sessionId, TenantId = _tenantId, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/LocalAdminAnalyzer.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/LocalAdminAnalyzer.cs index 3927e44b..c58f87c3 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/LocalAdminAnalyzer.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/LocalAdminAnalyzer.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Management; using AutopilotMonitor.Agent.V2.Core.Logging; +using AutopilotMonitor.Agent.V2.Core.Orchestration; using AutopilotMonitor.Shared.Models; using Microsoft.Win32; @@ -30,7 +31,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers { private readonly string _sessionId; private readonly string _tenantId; - private readonly Action _emitEvent; + private readonly InformationalEventPost _post; private readonly AgentLogger _logger; private readonly List _allowedAccounts; @@ -57,13 +58,13 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers public LocalAdminAnalyzer( string sessionId, string tenantId, - Action emitEvent, + InformationalEventPost post, AgentLogger logger, List allowedAccounts = null) { _sessionId = sessionId ?? throw new ArgumentNullException(nameof(sessionId)); _tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId)); - _emitEvent = emitEvent ?? throw new ArgumentNullException(nameof(emitEvent)); + _post = post ?? throw new ArgumentNullException(nameof(post)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); // Tenant-supplied accounts are additive (union with built-ins, not replacement) @@ -203,7 +204,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers ? $"{Name}: No unexpected local admins detected" : $"{Name}: Unexpected admin activity detected (confidence={confidenceScore})"; - _emitEvent(new EnrollmentEvent + _post.Emit(new EnrollmentEvent { SessionId = _sessionId, TenantId = _tenantId, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/SoftwareInventoryAnalyzer.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/SoftwareInventoryAnalyzer.cs index 02c6835e..32bd2b2a 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/SoftwareInventoryAnalyzer.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/SoftwareInventoryAnalyzer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using AutopilotMonitor.Agent.V2.Core.Logging; +using AutopilotMonitor.Agent.V2.Core.Orchestration; using AutopilotMonitor.Shared.Models; using Microsoft.Win32; @@ -22,7 +23,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers { private readonly string _sessionId; private readonly string _tenantId; - private readonly Action _emitEvent; + private readonly InformationalEventPost _post; private readonly AgentLogger _logger; // Captured at startup for delta detection at shutdown @@ -349,12 +350,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers public SoftwareInventoryAnalyzer( string sessionId, string tenantId, - Action emitEvent, + InformationalEventPost post, AgentLogger logger) { _sessionId = sessionId ?? throw new ArgumentNullException(nameof(sessionId)); _tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId)); - _emitEvent = emitEvent ?? throw new ArgumentNullException(nameof(emitEvent)); + _post = post ?? throw new ArgumentNullException(nameof(post)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -874,7 +875,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers : $"{Name}: Shutdown inventory ({inventory.Count} items, {newInstalls?.Count ?? 0} new during enrollment)") : $"{Name}: Inventory chunk {i + 1}/{totalChunks}"; - _emitEvent(new EnrollmentEvent + _post.Emit(new EnrollmentEvent { SessionId = _sessionId, TenantId = _tenantId, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Runtime/AgentAnalyzerManager.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Runtime/AgentAnalyzerManager.cs index c8d587bf..e01e2a8f 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Runtime/AgentAnalyzerManager.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Runtime/AgentAnalyzerManager.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using AutopilotMonitor.Agent.V2.Core.Configuration; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers; +using AutopilotMonitor.Agent.V2.Core.Orchestration; using AutopilotMonitor.Shared.Models; namespace AutopilotMonitor.Agent.V2.Core.Runtime @@ -24,7 +25,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Runtime { private readonly AgentConfiguration _configuration; private readonly AgentLogger _logger; - private readonly Action _emitEvent; + private readonly InformationalEventPost _post; private readonly AnalyzerConfiguration _analyzerConfig; private readonly List _analyzers = new List(); @@ -33,12 +34,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Runtime public AgentAnalyzerManager( AgentConfiguration configuration, AgentLogger logger, - Action emitEvent, + InformationalEventPost post, AnalyzerConfiguration? analyzerConfig) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _emitEvent = emitEvent ?? throw new ArgumentNullException(nameof(emitEvent)); + _post = post ?? throw new ArgumentNullException(nameof(post)); _analyzerConfig = analyzerConfig ?? new AnalyzerConfiguration(); } @@ -56,7 +57,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Runtime _analyzers.Add(new LocalAdminAnalyzer( _configuration.SessionId, _configuration.TenantId, - _emitEvent, + _post, _logger, _analyzerConfig.LocalAdminAllowedAccounts)); _logger.Info("LocalAdminAnalyzer registered"); @@ -71,7 +72,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Runtime _analyzers.Add(new SoftwareInventoryAnalyzer( _configuration.SessionId, _configuration.TenantId, - _emitEvent, + _post, _logger)); _logger.Info("SoftwareInventoryAnalyzer registered"); } @@ -85,7 +86,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Runtime _analyzers.Add(new IntegrityBypassAnalyzer( _configuration.SessionId, _configuration.TenantId, - _emitEvent, + _post, _logger)); _logger.Info("IntegrityBypassAnalyzer registered"); } diff --git a/src/Agent/AutopilotMonitor.Agent.V2/Program.cs b/src/Agent/AutopilotMonitor.Agent.V2/Program.cs index a2b9dc8f..00eb68be 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2/Program.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2/Program.cs @@ -503,14 +503,14 @@ namespace AutopilotMonitor.Agent.V2 using (var shutdown = new ManualResetEventSlim(false)) using (var shutdownComplete = new ManualResetEventSlim(false)) { - // M4.6.δ — Analyzer manager. Emits through the live orchestrator's event - // emitter. RunStartup fires after orchestrator.Start; RunShutdown is wired - // into the termination handler so it runs before diagnostics upload. - var analyzerManager = new AgentAnalyzerManager( - configuration: agentConfig, - logger: logger, - emitEvent: evt => { orchestrator.EventEmitter.Emit(evt); }, - analyzerConfig: remoteConfig.Analyzers); + // M4.6.δ — Analyzer manager. Single-rail refactor (plan §5.7): the manager + // now takes an InformationalEventPost so LocalAdmin / SoftwareInventory / + // IntegrityBypass analyzer events flow through the same ingress pipe as + // every other telemetry source. Construction is deferred into + // orchestrator.Start's onIngressReady hook so lifecyclePost is non-null at + // construction time. RunStartup fires after orchestrator.Start; RunShutdown + // is wired into the termination handler so it runs before diagnostics upload. + AgentAnalyzerManager analyzerManager = null; // M4.6.β — the peripheral termination sequence (FinalStatus + SummaryDialog // + diagnostics upload + enrollment-complete.marker + CleanupService) lives @@ -675,6 +675,17 @@ namespace AutopilotMonitor.Agent.V2 EmitVersionCheckEventIfAny(lifecyclePost, agentConfig, logger); EmitUnrestrictedModeAuditIfChanged(lifecyclePost, agentConfig, configMergeResult, logger); + // Single-rail refactor (plan §5.7) — AgentAnalyzerManager emits through + // the same InformationalEventPost. Constructed inside this hook so + // lifecyclePost is guaranteed non-null by the time RunStartup (below, + // after Start returns) and RunShutdown (via terminationHandler) call + // into the three analyzers. + analyzerManager = new AgentAnalyzerManager( + configuration: agentConfig, + logger: logger, + post: lifecyclePost, + analyzerConfig: remoteConfig.Analyzers); + // Single-rail refactor (plan §5.3) — EnrollmentTerminationHandler emits // through the same InformationalEventPost. Constructed inside this hook // so lifecyclePost is guaranteed non-null; the terminated-dispatch wrapper