diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Security/TenantIdResolver.cs b/src/Agent/AutopilotMonitor.Agent.Core/Security/TenantIdResolver.cs
new file mode 100644
index 00000000..36c5551a
--- /dev/null
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Security/TenantIdResolver.cs
@@ -0,0 +1,281 @@
+using System;
+using System.Collections.Generic;
+using AutopilotMonitor.Agent.Core.Logging;
+using Microsoft.Win32;
+
+namespace AutopilotMonitor.Agent.Core.Security
+{
+ ///
+ /// Resolves the device's AAD TenantId from one of several Windows registry signals.
+ /// V1 backport of the V2 resolver — same probe order, same diagnostic logging.
+ ///
+ /// Probe order, first non-empty hit wins:
+ ///
+ ///
+ /// -
+ /// HKLM\SOFTWARE\Microsoft\Enrollments\{guid} with EnrollmentType=6
+ /// (Intune MDM) and AADTenantID. Authoritative once MDM enrollment finished.
+ ///
+ /// -
+ /// HKLM\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\TenantInfo\{TenantId}
+ /// — the sub-key name itself is the TenantId GUID. Written by AAD join, which in
+ /// Autopilot/OOBE happens before MDM enrollment.
+ ///
+ /// -
+ /// HKLM\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\JoinInfo\{thumbprint}
+ /// value TenantId. Same AAD-join phase as TenantInfo, used as a secondary
+ /// reading because some flows populate one but not the other.
+ ///
+ ///
+ ///
+ /// Returns null when no source carries a TenantId. Never throws. On miss the
+ /// resolver dumps a diagnostic summary of which keys/values it saw so the bootstrap
+ /// log carries enough evidence to classify "device truly not enrolled" vs. "agent
+ /// fired before enrollment finished writing".
+ ///
+ ///
+ public static class TenantIdResolver
+ {
+ private const string EnrollmentsKeyPath = @"SOFTWARE\Microsoft\Enrollments";
+ private const string CloudDomainJoinTenantInfoPath = @"SYSTEM\CurrentControlSet\Control\CloudDomainJoin\TenantInfo";
+ private const string CloudDomainJoinJoinInfoPath = @"SYSTEM\CurrentControlSet\Control\CloudDomainJoin\JoinInfo";
+ private const int IntuneMdmEnrollmentType = 6;
+
+ public const string SourceEnrollmentsRegistry = "enrollments_registry";
+ public const string SourceCloudDomainJoinTenantInfo = "cloud_domain_join_tenant_info";
+ public const string SourceCloudDomainJoinJoinInfo = "cloud_domain_join_join_info";
+
+ ///
+ /// Tries each probe in order and returns the first non-empty TenantId.
+ /// Logs the winning source at Info level. On full miss, logs a diagnostic
+ /// summary at Warning level. Returns null when nothing resolves.
+ ///
+ public static string Resolve(AgentLogger logger = null)
+ {
+ var diagnostics = new ResolverDiagnostics();
+
+ var fromEnrollments = TryEnrollmentsRegistry(diagnostics);
+ if (!string.IsNullOrEmpty(fromEnrollments))
+ {
+ logger?.Info($"TenantIdResolver: resolved TenantId={fromEnrollments} from {SourceEnrollmentsRegistry}.");
+ return fromEnrollments;
+ }
+
+ var fromTenantInfo = TryCloudDomainJoinTenantInfo(diagnostics);
+ if (!string.IsNullOrEmpty(fromTenantInfo))
+ {
+ logger?.Info($"TenantIdResolver: resolved TenantId={fromTenantInfo} from {SourceCloudDomainJoinTenantInfo} (Enrollments registry had no Intune MDM hit).");
+ return fromTenantInfo;
+ }
+
+ var fromJoinInfo = TryCloudDomainJoinJoinInfo(diagnostics);
+ if (!string.IsNullOrEmpty(fromJoinInfo))
+ {
+ logger?.Info($"TenantIdResolver: resolved TenantId={fromJoinInfo} from {SourceCloudDomainJoinJoinInfo} (Enrollments registry + CloudDomainJoin\\TenantInfo had no hit).");
+ return fromJoinInfo;
+ }
+
+ LogResolutionMiss(logger, diagnostics);
+ return null;
+ }
+
+ private static string TryEnrollmentsRegistry(ResolverDiagnostics diag)
+ {
+ try
+ {
+ using (var enrollmentsKey = Registry.LocalMachine.OpenSubKey(EnrollmentsKeyPath))
+ {
+ if (enrollmentsKey == null)
+ {
+ diag.EnrollmentsRootMissing = true;
+ return null;
+ }
+
+ foreach (var enrollmentGuid in enrollmentsKey.GetSubKeyNames())
+ {
+ using (var enrollmentKey = enrollmentsKey.OpenSubKey(enrollmentGuid))
+ {
+ if (enrollmentKey == null) continue;
+
+ var enrollmentType = enrollmentKey.GetValue("EnrollmentType");
+ int? typeValue = null;
+ if (enrollmentType != null)
+ {
+ try { typeValue = Convert.ToInt32(enrollmentType); }
+ catch { /* unparseable — record as null */ }
+ }
+
+ var aadTenantId = enrollmentKey.GetValue("AADTenantID")?.ToString();
+ var enrollmentState = enrollmentKey.GetValue("EnrollmentState")?.ToString();
+
+ diag.EnrollmentsSeen.Add(new EnrollmentSnapshot
+ {
+ Guid = enrollmentGuid,
+ EnrollmentType = typeValue,
+ EnrollmentState = enrollmentState,
+ HasAadTenantId = !string.IsNullOrEmpty(aadTenantId),
+ });
+
+ if (typeValue == IntuneMdmEnrollmentType && !string.IsNullOrEmpty(aadTenantId))
+ return aadTenantId;
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ diag.EnrollmentsProbeError = ex.GetType().Name + ": " + ex.Message;
+ }
+
+ return null;
+ }
+
+ private static string TryCloudDomainJoinTenantInfo(ResolverDiagnostics diag)
+ {
+ try
+ {
+ using (var tenantInfoKey = Registry.LocalMachine.OpenSubKey(CloudDomainJoinTenantInfoPath))
+ {
+ if (tenantInfoKey == null)
+ {
+ diag.TenantInfoRootMissing = true;
+ return null;
+ }
+
+ var subKeys = tenantInfoKey.GetSubKeyNames();
+ diag.TenantInfoSubKeys.AddRange(subKeys);
+
+ foreach (var subKey in subKeys)
+ {
+ if (Guid.TryParse(subKey, out _))
+ return subKey;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ diag.TenantInfoProbeError = ex.GetType().Name + ": " + ex.Message;
+ }
+
+ return null;
+ }
+
+ private static string TryCloudDomainJoinJoinInfo(ResolverDiagnostics diag)
+ {
+ try
+ {
+ using (var joinInfoKey = Registry.LocalMachine.OpenSubKey(CloudDomainJoinJoinInfoPath))
+ {
+ if (joinInfoKey == null)
+ {
+ diag.JoinInfoRootMissing = true;
+ return null;
+ }
+
+ foreach (var thumbprint in joinInfoKey.GetSubKeyNames())
+ {
+ using (var entry = joinInfoKey.OpenSubKey(thumbprint))
+ {
+ if (entry == null) continue;
+
+ var tenantId = entry.GetValue("TenantId")?.ToString();
+ diag.JoinInfoEntries.Add(new JoinInfoSnapshot
+ {
+ Thumbprint = thumbprint,
+ HasTenantId = !string.IsNullOrEmpty(tenantId),
+ });
+
+ if (!string.IsNullOrEmpty(tenantId))
+ return tenantId;
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ diag.JoinInfoProbeError = ex.GetType().Name + ": " + ex.Message;
+ }
+
+ return null;
+ }
+
+ private static void LogResolutionMiss(AgentLogger logger, ResolverDiagnostics diag)
+ {
+ if (logger == null) return;
+
+ logger.Warning("TenantIdResolver: no TenantId resolvable from registry — device likely not yet AAD-joined / MDM-enrolled, or enrollment write hasn't completed.");
+
+ // Enrollments root.
+ if (diag.EnrollmentsProbeError != null)
+ logger.Warning($"TenantIdResolver: Enrollments probe failed: {diag.EnrollmentsProbeError}");
+ else if (diag.EnrollmentsRootMissing)
+ logger.Warning($"TenantIdResolver: HKLM\\{EnrollmentsKeyPath} not found.");
+ else if (diag.EnrollmentsSeen.Count == 0)
+ logger.Warning($"TenantIdResolver: HKLM\\{EnrollmentsKeyPath} present but empty.");
+ else
+ {
+ logger.Warning($"TenantIdResolver: HKLM\\{EnrollmentsKeyPath} carries {diag.EnrollmentsSeen.Count} enrollment(s) but none was Intune MDM (Type=6) with AADTenantID:");
+ foreach (var e in diag.EnrollmentsSeen)
+ {
+ logger.Warning(
+ $" - {e.Guid}: EnrollmentType={(e.EnrollmentType?.ToString() ?? "")}, " +
+ $"EnrollmentState={e.EnrollmentState ?? ""}, " +
+ $"AADTenantID={(e.HasAadTenantId ? "" : "")}");
+ }
+ }
+
+ // CloudDomainJoin\TenantInfo.
+ if (diag.TenantInfoProbeError != null)
+ logger.Warning($"TenantIdResolver: TenantInfo probe failed: {diag.TenantInfoProbeError}");
+ else if (diag.TenantInfoRootMissing)
+ logger.Warning($"TenantIdResolver: HKLM\\{CloudDomainJoinTenantInfoPath} not found (device not AAD-joined yet).");
+ else if (diag.TenantInfoSubKeys.Count == 0)
+ logger.Warning($"TenantIdResolver: HKLM\\{CloudDomainJoinTenantInfoPath} present but no sub-keys.");
+ else
+ logger.Warning($"TenantIdResolver: HKLM\\{CloudDomainJoinTenantInfoPath} sub-keys: [{string.Join(", ", diag.TenantInfoSubKeys)}] — none parsed as a GUID.");
+
+ // CloudDomainJoin\JoinInfo.
+ if (diag.JoinInfoProbeError != null)
+ logger.Warning($"TenantIdResolver: JoinInfo probe failed: {diag.JoinInfoProbeError}");
+ else if (diag.JoinInfoRootMissing)
+ logger.Warning($"TenantIdResolver: HKLM\\{CloudDomainJoinJoinInfoPath} not found.");
+ else if (diag.JoinInfoEntries.Count == 0)
+ logger.Warning($"TenantIdResolver: HKLM\\{CloudDomainJoinJoinInfoPath} present but no thumbprint sub-keys.");
+ else
+ {
+ logger.Warning($"TenantIdResolver: HKLM\\{CloudDomainJoinJoinInfoPath} carries {diag.JoinInfoEntries.Count} entry/entries but none had a non-empty TenantId value:");
+ foreach (var j in diag.JoinInfoEntries)
+ logger.Warning($" - thumbprint={j.Thumbprint}, TenantId={(j.HasTenantId ? "" : "")}");
+ }
+ }
+
+ private sealed class ResolverDiagnostics
+ {
+ public bool EnrollmentsRootMissing;
+ public string EnrollmentsProbeError;
+ public readonly List EnrollmentsSeen = new List();
+
+ public bool TenantInfoRootMissing;
+ public string TenantInfoProbeError;
+ public readonly List TenantInfoSubKeys = new List();
+
+ public bool JoinInfoRootMissing;
+ public string JoinInfoProbeError;
+ public readonly List JoinInfoEntries = new List();
+ }
+
+ private struct EnrollmentSnapshot
+ {
+ public string Guid;
+ public int? EnrollmentType;
+ public string EnrollmentState;
+ public bool HasAadTenantId;
+ }
+
+ private struct JoinInfoSnapshot
+ {
+ public string Thumbprint;
+ public bool HasTenantId;
+ }
+ }
+}
diff --git a/src/Agent/AutopilotMonitor.Agent.V2/Program.GatherRulesMode.cs b/src/Agent/AutopilotMonitor.Agent.V2/Program.GatherRulesMode.cs
index 6d51cf3d..cf5a6cef 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2/Program.GatherRulesMode.cs
+++ b/src/Agent/AutopilotMonitor.Agent.V2/Program.GatherRulesMode.cs
@@ -21,8 +21,8 @@ namespace AutopilotMonitor.Agent.V2
///
/// Fetches remote config, executes all startup gather rules against a freshly-registered
/// ephemeral session, uploads the collected events, and exits. Parity with Legacy
- /// Program.GatherRulesMode.cs — the V2 differences are minimal (V2 TenantIdResolver
- /// instead of GetTenantIdFromRegistry, V2 BackendApiClient ctor).
+ /// Program.GatherRulesMode.cs — the V2 differences are minimal (V2 BackendApiClient
+ /// ctor; both rails now share the same TenantIdResolver shape).
///
///
public static partial class Program
diff --git a/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt
index 3091e8ee..80e3e6ea 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt
+++ b/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt
@@ -1 +1 @@
-504
+506
diff --git a/src/Agent/AutopilotMonitor.Agent/Program.Configuration.cs b/src/Agent/AutopilotMonitor.Agent/Program.Configuration.cs
index 7543c0fb..39bf82f9 100644
--- a/src/Agent/AutopilotMonitor.Agent/Program.Configuration.cs
+++ b/src/Agent/AutopilotMonitor.Agent/Program.Configuration.cs
@@ -5,6 +5,7 @@ using System.Reflection;
using AutopilotMonitor.Agent.Core.Configuration;
using AutopilotMonitor.Agent.Core.Logging;
using AutopilotMonitor.Agent.Core.Monitoring.Runtime;
+using AutopilotMonitor.Agent.Core.Security;
using AutopilotMonitor.Shared;
using Microsoft.Win32;
using AutopilotMonitor.Agent.Core.Monitoring.Enrollment;
@@ -17,39 +18,6 @@ namespace AutopilotMonitor.Agent
{
partial class Program
{
- static string GetTenantIdFromRegistry()
- {
- try
- {
- const string enrollmentsKeyPath = @"SOFTWARE\Microsoft\Enrollments";
-
- using (var enrollmentsKey = Registry.LocalMachine.OpenSubKey(enrollmentsKeyPath))
- {
- if (enrollmentsKey == null)
- return null;
-
- foreach (var enrollmentGuid in enrollmentsKey.GetSubKeyNames())
- {
- using (var enrollmentKey = enrollmentsKey.OpenSubKey(enrollmentGuid))
- {
- if (enrollmentKey == null) continue;
-
- var enrollmentType = enrollmentKey.GetValue("EnrollmentType");
- if (enrollmentType != null && Convert.ToInt32(enrollmentType) == 6)
- {
- var tenantId = enrollmentKey.GetValue("AADTenantID");
- if (tenantId != null)
- return tenantId.ToString();
- }
- }
- }
- }
- }
- catch { }
-
- return null;
- }
-
static AgentConfiguration LoadConfiguration(string[] args = null)
{
var noCleanup = args?.Contains("--no-cleanup") ?? false;
@@ -119,8 +87,10 @@ namespace AutopilotMonitor.Agent
selfDestructOnComplete = false;
}
- // Determine TenantId from registry (or bootstrap-config.json fallback below)
- string tenantId = GetTenantIdFromRegistry();
+ // Determine TenantId from registry (or bootstrap-config.json fallback below).
+ // No logger available at config-load time — the bail-site below re-probes with
+ // a logger so the diagnostic block lands in the agent log on a real miss.
+ string tenantId = TenantIdResolver.Resolve(logger: null);
var dataDirectory = Environment.ExpandEnvironmentVariables(Constants.AgentDataDirectory);
var sessionPersist = new SessionPersistence(dataDirectory);
diff --git a/src/Agent/AutopilotMonitor.Agent/Program.cs b/src/Agent/AutopilotMonitor.Agent/Program.cs
index f92ec6ac..d6da06b8 100644
--- a/src/Agent/AutopilotMonitor.Agent/Program.cs
+++ b/src/Agent/AutopilotMonitor.Agent/Program.cs
@@ -220,20 +220,20 @@ namespace AutopilotMonitor.Agent
if (consoleMode)
Console.WriteLine($"MDM certificate found: {cert.Thumbprint}");
- // Re-read TenantId from registry — enrollment creates the registry key
- // alongside the certificate, so it should be available now.
+ // Re-read TenantId — enrollment creates the registry key alongside the
+ // certificate, so it should be available now. TenantIdResolver also probes
+ // CloudDomainJoin as a fallback and emits a source-tagged log line on hit.
if (string.IsNullOrEmpty(config.TenantId))
{
- config.TenantId = GetTenantIdFromRegistry();
+ config.TenantId = TenantIdResolver.Resolve(logger);
if (!string.IsNullOrEmpty(config.TenantId))
{
- logger.Info($"Await-enrollment: TenantId discovered from registry: {config.TenantId}");
if (consoleMode)
Console.WriteLine($"Tenant ID: {config.TenantId}");
}
else
{
- logger.Warning("Await-enrollment: MDM certificate found but TenantId not yet in registry");
+ logger.Warning("Await-enrollment: MDM certificate found but TenantId still not resolvable.");
}
}
@@ -257,8 +257,18 @@ namespace AutopilotMonitor.Agent
// Pre-flight: bail cleanly if TenantId is missing instead of crashing inside
// MonitoringService.IsValid(). Mirrors V2's AgentBootstrap guard. Common cause:
- // agent scheduled before MDM enrollment populated HKLM\SOFTWARE\Microsoft\Enrollments
- // and no bootstrap-config.json present. The Scheduled Task will retry on next trigger.
+ // agent scheduled before MDM enrollment / AAD join populated the registry and
+ // no bootstrap-config.json present. The Scheduled Task will retry on next trigger.
+ //
+ // Re-probe with the logger if we never went through the await-enrollment branch
+ // (LoadConfiguration ran without a logger, so its initial probe was silent). The
+ // re-probe either resolves the race (registry caught up between config load and
+ // here) or emits the diagnostic block so the agent log captures registry state.
+ if (string.IsNullOrEmpty(config.TenantId) && !config.AwaitEnrollment)
+ {
+ config.TenantId = TenantIdResolver.Resolve(logger);
+ }
+
if (string.IsNullOrEmpty(config.TenantId))
{
logger.Error("Agent cannot start: TenantId not available (registry empty + no bootstrap config). " +
diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
index 4f720374..fe1b0608 100644
--- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
+++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
@@ -1 +1 @@
-1227
+1228