diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.cs index eff122ee..266e6e10 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.cs @@ -38,9 +38,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors /// /// Runs all device info collectors at agent startup. - /// Returns the detected enrollment type ("v1" or "v2") and whether the profile indicates Hybrid Join. + /// Returns the detected enrollment type, Hybrid Join flag, and ESP skip configuration from registry. /// - public (string enrollmentType, bool isHybridJoin) CollectAll() + public (string enrollmentType, bool isHybridJoin, bool? skipUserStatusPage, bool? skipDeviceStatusPage) CollectAll() { _logger.Info("EnrollmentTracker: collecting device info (at start)"); @@ -48,12 +48,13 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors CollectNetworkAndDnsConfiguration(); CollectProxyConfiguration(); var profileResult = CollectAutopilotProfile(); + var espConfig = CollectEspConfiguration(); CollectSecureBootStatus(); CollectBitLockerStatus(); CollectTpmStatus(); CollectAadJoinStatus(); - return profileResult; + return (profileResult.enrollmentType, profileResult.isHybridJoin, espConfig.skipUserStatusPage, espConfig.skipDeviceStatusPage); } /// @@ -581,6 +582,86 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors } } + /// + /// Reads ESP skip configuration from the enrollment FirstSync registry key. + /// CSP SkipUserStatusPage/SkipDeviceStatusPage are written as DWORD: + /// 0xFFFFFFFF = skip (ESP page not shown), 0 = show, key missing = unknown. + /// We use defensive interpretation: non-null AND non-zero = skip. + /// + private (bool? skipUserStatusPage, bool? skipDeviceStatusPage) CollectEspConfiguration() + { + bool? skipUser = null; + bool? skipDevice = null; + string enrollmentGuid = null; + + try + { + const string enrollmentsKeyPath = @"SOFTWARE\Microsoft\Enrollments"; + + using (var enrollmentsKey = Registry.LocalMachine.OpenSubKey(enrollmentsKeyPath)) + { + if (enrollmentsKey == null) + { + _logger.Debug("EnrollmentTracker: Enrollments registry key not found — ESP config unknown"); + return (null, null); + } + + // Find the MDM enrollment entry (EnrollmentType == 6) + foreach (var guid in enrollmentsKey.GetSubKeyNames()) + { + using (var enrollmentKey = enrollmentsKey.OpenSubKey(guid)) + { + if (enrollmentKey == null) continue; + + var enrollmentType = enrollmentKey.GetValue("EnrollmentType"); + if (enrollmentType == null || Convert.ToInt32(enrollmentType) != 6) + continue; + + enrollmentGuid = guid; + + // Read FirstSync subkey + using (var firstSyncKey = enrollmentKey.OpenSubKey("FirstSync")) + { + if (firstSyncKey == null) + { + _logger.Debug($"EnrollmentTracker: FirstSync subkey not found for enrollment {guid}"); + break; + } + + var rawSkipUser = firstSyncKey.GetValue("SkipUserStatusPage"); + if (rawSkipUser != null) + skipUser = Convert.ToInt32(rawSkipUser) != 0; + + var rawSkipDevice = firstSyncKey.GetValue("SkipDeviceStatusPage"); + if (rawSkipDevice != null) + skipDevice = Convert.ToInt32(rawSkipDevice) != 0; + } + + break; // Found the MDM enrollment entry + } + } + } + + var data = new Dictionary + { + { "source", "registry_firstsync" } + }; + if (enrollmentGuid != null) data["enrollmentGuid"] = enrollmentGuid; + if (skipUser.HasValue) data["skipUserStatusPage"] = skipUser.Value; + if (skipDevice.HasValue) data["skipDeviceStatusPage"] = skipDevice.Value; + + var summary = $"SkipUser={skipUser?.ToString() ?? "unknown"}, SkipDevice={skipDevice?.ToString() ?? "unknown"}"; + _logger.Info($"EnrollmentTracker: ESP configuration detected — {summary}"); + EmitDeviceInfoEvent("esp_config_detected", $"ESP configuration: {summary}", data); + } + catch (Exception ex) + { + _logger.Warning($"EnrollmentTracker: failed to collect ESP configuration: {ex.Message}"); + } + + return (skipUser, skipDevice); + } + private void EmitDeviceInfoEvent(string eventType, string message, Dictionary data) { _emitEvent(new EnrollmentEvent diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs index f7891e4c..d75ef15f 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs @@ -107,6 +107,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking public bool EnrollmentCompleteEmitted { get; set; } public string EnrollmentType { get; set; } + // ESP configuration from registry (FirstSync\SkipUserStatusPage / SkipDeviceStatusPage) + public bool? SkipUserStatusPage { get; set; } + public bool? SkipDeviceStatusPage { get; set; } + // Signal timestamps for audit trail public DateTime? EspFirstSeenUtc { get; set; } public DateTime? EspFinalExitUtc { get; set; } diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs index e76e6af3..4ae80e88 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs @@ -57,6 +57,11 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking private Timer _deviceOnlyEspTimer; private const int DeviceOnlyEspTimerMinutes = 5; + // ESP configuration from registry (FirstSync\SkipUserStatusPage / SkipDeviceStatusPage) + // null = unknown (registry keys not present), true = skip, false = show + private bool? _skipUserStatusPage; + private bool? _skipDeviceStatusPage; + // Safety-net timer for waiting_for_hello state private Timer _waitingForHelloSafetyTimer; private const int WaitingForHelloSafetyTimeoutSeconds = 420; // 7 min — longer than Hello chain (330s) @@ -241,7 +246,11 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking { var result = _deviceInfoCollector.CollectAll(); _enrollmentType = result.enrollmentType; + _skipUserStatusPage = result.skipUserStatusPage; + _skipDeviceStatusPage = result.skipDeviceStatusPage; _stateData.EnrollmentType = _enrollmentType; + _stateData.SkipUserStatusPage = _skipUserStatusPage; + _stateData.SkipDeviceStatusPage = _skipDeviceStatusPage; _stateDirty = true; } @@ -873,10 +882,44 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking // during AccountSetup), the composite signal can fire immediately. TryEmitEnrollmentComplete("esp_hello_composite"); } + else if (_skipUserStatusPage == true) + { + // Registry definitively says no AccountSetup expected → immediate device-only classification + _logger.Info($"EnrollmentTracker: ESP phase exiting from '{_lastEspPhase ?? "unknown"}' — SkipUserStatusPage=true, classified as device-only ESP (registry)"); + + _espFinalExitSeen = true; + _stateData.EspFinalExitSeen = true; + _stateData.EspFinalExitUtc = DateTime.UtcNow; + RecordSignal("device_only_esp_registry"); + + _emitEvent(new EnrollmentEvent + { + SessionId = _sessionId, + TenantId = _tenantId, + EventType = "esp_phase_changed", + Severity = EventSeverity.Info, + Source = "EnrollmentTracker", + Phase = EnrollmentPhase.FinalizingSetup, + Message = "ESP phase: FinalizingSetup (device-only ESP — SkipUserStatusPage confirmed via registry)", + Data = new Dictionary + { + { "espPhase", "FinalizingSetup" }, + { "autoDetected", true }, + { "triggeredBy", "device_only_esp_registry" }, + { "previousPhase", _lastEspPhase ?? "unknown" }, + { "skipUserStatusPage", true } + } + }); + + CollectDeviceInfoAtFinalizingSetup("device_only_esp_registry"); + _espAndHelloTracker?.StartHelloWaitTimer(); + TryEmitEnrollmentComplete("device_only_esp_registry"); + } else { - // DeviceSetup phase exiting without desktop — intermediate transition (Device→Account) or device-only ESP - _logger.Info($"EnrollmentTracker: ESP phase exiting from '{_lastEspPhase ?? "unknown"}' - intermediate transition, starting device-only ESP detection timer ({DeviceOnlyEspTimerMinutes}min)"); + // Registry keys unknown or SkipUserStatusPage=false → fallback to timer-based detection + var fallbackReason = _skipUserStatusPage == null ? "registry keys not found" : "SkipUserStatusPage=false"; + _logger.Info($"EnrollmentTracker: ESP phase exiting from '{_lastEspPhase ?? "unknown"}' — {fallbackReason}, starting device-only ESP detection timer ({DeviceOnlyEspTimerMinutes}min)"); // Start device-only ESP detection timer _deviceOnlyEspTimer?.Dispose(); @@ -928,6 +971,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking _isWaitingForHello = loaded.IsWaitingForHello; _enrollmentCompleteEmitted = loaded.EnrollmentCompleteEmitted; _enrollmentType = loaded.EnrollmentType ?? _enrollmentType; + _skipUserStatusPage = loaded.SkipUserStatusPage; + _skipDeviceStatusPage = loaded.SkipDeviceStatusPage; _stateData = loaded; _logger.Info($"EnrollmentTracker: state restored — espEverSeen={_espEverSeen}, espFinalExitSeen={_espFinalExitSeen}, desktopArrived={_desktopArrived}, lastEspPhase={_lastEspPhase}, enrollmentCompleteEmitted={_enrollmentCompleteEmitted}"); @@ -1083,7 +1128,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking { "helloResolved", _espAndHelloTracker?.IsHelloCompleted ?? false }, { "helloPolicyConfigured", _espAndHelloTracker?.IsPolicyConfigured ?? false }, { "enrollmentType", _enrollmentType }, - { "lastEspPhase", _lastEspPhase ?? "none" } + { "lastEspPhase", _lastEspPhase ?? "none" }, + { "skipUserStatusPage", _skipUserStatusPage?.ToString() ?? "unknown" }, + { "skipDeviceStatusPage", _skipDeviceStatusPage?.ToString() ?? "unknown" } } }); } diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index 0d389107..9d683f8c 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -212 +214