diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Security/TenantIdResolver.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Security/TenantIdResolver.cs
index eb4b5198..6741eee7 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Security/TenantIdResolver.cs
+++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Security/TenantIdResolver.cs
@@ -1,30 +1,85 @@
using System;
+using System.Collections.Generic;
using AutopilotMonitor.Agent.V2.Core.Logging;
using Microsoft.Win32;
namespace AutopilotMonitor.Agent.V2.Core.Security
{
///
- /// Resolves the Autopilot device's AAD TenantId from the MDM enrollment registry.
+ /// Resolves the device's AAD TenantId from one of several Windows registry signals.
/// Plan §4.x M4.5.b.
///
- /// The MDM enrollment writes one sub-key per enrollment under
- /// HKLM\SOFTWARE\Microsoft\Enrollments. The entry with EnrollmentType==6
- /// (MDM Intune enrollment) carries AADTenantID — the only value that maps the
- /// device back to its Entra ID tenant without a backend round-trip.
+ /// 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";
+
///
- /// Walks the enrollments subtree and returns the first AADTenantID tagged with
- /// the Intune MDM enrollment type. Returns null when the device is not MDM-enrolled
- /// or the registry key is inaccessible. Never throws.
+ /// 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 ResolveFromEnrollmentRegistry(AgentLogger logger = null)
+ 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
{
@@ -32,7 +87,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
{
if (enrollmentsKey == null)
{
- logger?.Warning($"TenantIdResolver: {EnrollmentsKeyPath} not found — device likely not MDM-enrolled.");
+ diag.EnrollmentsRootMissing = true;
return null;
}
@@ -43,33 +98,184 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
if (enrollmentKey == null) continue;
var enrollmentType = enrollmentKey.GetValue("EnrollmentType");
- if (enrollmentType == null) continue;
-
- int typeValue;
- try { typeValue = Convert.ToInt32(enrollmentType); }
- catch { continue; }
-
- if (typeValue != IntuneMdmEnrollmentType) continue;
-
- var tenantId = enrollmentKey.GetValue("AADTenantID");
- var resolved = tenantId?.ToString();
- if (!string.IsNullOrEmpty(resolved))
+ int? typeValue = null;
+ if (enrollmentType != null)
{
- logger?.Debug($"TenantIdResolver: resolved AADTenantID={resolved} from enrollment {enrollmentGuid}.");
- return resolved;
+ 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;
}
}
}
-
- logger?.Warning("TenantIdResolver: no Intune MDM enrollment (EnrollmentType=6) with AADTenantID found.");
- return null;
}
catch (Exception ex)
{
- logger?.Error("TenantIdResolver: registry probe failed.", ex);
- return null;
+ 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 86749a03..6d51cf3d 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2/Program.GatherRulesMode.cs
+++ b/src/Agent/AutopilotMonitor.Agent.V2/Program.GatherRulesMode.cs
@@ -45,7 +45,7 @@ namespace AutopilotMonitor.Agent.V2
try
{
- var tenantId = TenantIdResolver.ResolveFromEnrollmentRegistry(logger);
+ var tenantId = TenantIdResolver.Resolve(logger);
if (string.IsNullOrEmpty(tenantId))
{
logger.Error("--run-gather-rules: TenantId could not be resolved — device is not MDM-enrolled.");
diff --git a/src/Agent/AutopilotMonitor.Agent.V2/Runtime/AgentBootstrap.cs b/src/Agent/AutopilotMonitor.Agent.V2/Runtime/AgentBootstrap.cs
index 55bb4a89..7509e1d1 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2/Runtime/AgentBootstrap.cs
+++ b/src/Agent/AutopilotMonitor.Agent.V2/Runtime/AgentBootstrap.cs
@@ -42,7 +42,7 @@ namespace AutopilotMonitor.Agent.V2.Runtime
var bootstrapConfig = Program.TryReadBootstrapConfig(dataDirectory, logger);
var awaitConfig = Program.TryReadAwaitEnrollmentConfig(dataDirectory, logger);
- var tenantIdFromRegistry = TenantIdResolver.ResolveFromEnrollmentRegistry(logger);
+ var tenantIdFromRegistry = TenantIdResolver.Resolve(logger);
var tenantId = !string.IsNullOrEmpty(tenantIdFromRegistry)
? tenantIdFromRegistry
: bootstrapConfig?.TenantId;
@@ -111,7 +111,7 @@ namespace AutopilotMonitor.Agent.V2.Runtime
// Re-resolve TenantId — enrollment typically writes the registry key alongside the cert.
if (string.IsNullOrEmpty(agentConfig.TenantId))
{
- agentConfig.TenantId = TenantIdResolver.ResolveFromEnrollmentRegistry(logger);
+ agentConfig.TenantId = TenantIdResolver.Resolve(logger);
if (!string.IsNullOrEmpty(agentConfig.TenantId))
logger.Info($"Await-enrollment: TenantId discovered from registry: {agentConfig.TenantId}");
}