mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
feat(agent): V2 TenantIdResolver falls back to CloudDomainJoin registry
Resolver now probes Enrollments → CloudDomainJoin\TenantInfo → CloudDomainJoin\JoinInfo and tags the winning source in the log. On a full miss it dumps a diagnostic block (which roots exist, which enrollment GUIDs were seen with their EnrollmentType/EnrollmentState, which JoinInfo thumbprints). Heals the OOBE race where the agent fires before MDM enrollment is written but AAD join already populated CloudDomainJoin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// <para>
|
||||
/// The MDM enrollment writes one sub-key per enrollment under
|
||||
/// <c>HKLM\SOFTWARE\Microsoft\Enrollments</c>. The entry with <c>EnrollmentType==6</c>
|
||||
/// (MDM Intune enrollment) carries <c>AADTenantID</c> — 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:
|
||||
/// </para>
|
||||
/// <list type="number">
|
||||
/// <item><description>
|
||||
/// <c>HKLM\SOFTWARE\Microsoft\Enrollments\{guid}</c> with <c>EnrollmentType=6</c>
|
||||
/// (Intune MDM) and <c>AADTenantID</c>. Authoritative once MDM enrollment finished.
|
||||
/// </description></item>
|
||||
/// <item><description>
|
||||
/// <c>HKLM\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\TenantInfo\{TenantId}</c>
|
||||
/// — the sub-key name itself is the TenantId GUID. Written by AAD join, which in
|
||||
/// Autopilot/OOBE happens before MDM enrollment.
|
||||
/// </description></item>
|
||||
/// <item><description>
|
||||
/// <c>HKLM\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\JoinInfo\{thumbprint}</c>
|
||||
/// value <c>TenantId</c>. Same AAD-join phase as TenantInfo, used as a secondary
|
||||
/// reading because some flows populate one but not the other.
|
||||
/// </description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Returns <c>null</c> 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".
|
||||
/// </para>
|
||||
/// </summary>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// Walks the enrollments subtree and returns the first <c>AADTenantID</c> tagged with
|
||||
/// the Intune MDM enrollment type. Returns <c>null</c> 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 <c>null</c> when nothing resolves.
|
||||
/// </summary>
|
||||
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() ?? "<missing>")}, " +
|
||||
$"EnrollmentState={e.EnrollmentState ?? "<missing>"}, " +
|
||||
$"AADTenantID={(e.HasAadTenantId ? "<present>" : "<missing>")}");
|
||||
}
|
||||
}
|
||||
|
||||
// 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 ? "<present>" : "<missing>")}");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ResolverDiagnostics
|
||||
{
|
||||
public bool EnrollmentsRootMissing;
|
||||
public string EnrollmentsProbeError;
|
||||
public readonly List<EnrollmentSnapshot> EnrollmentsSeen = new List<EnrollmentSnapshot>();
|
||||
|
||||
public bool TenantInfoRootMissing;
|
||||
public string TenantInfoProbeError;
|
||||
public readonly List<string> TenantInfoSubKeys = new List<string>();
|
||||
|
||||
public bool JoinInfoRootMissing;
|
||||
public string JoinInfoProbeError;
|
||||
public readonly List<JoinInfoSnapshot> JoinInfoEntries = new List<JoinInfoSnapshot>();
|
||||
}
|
||||
|
||||
private struct EnrollmentSnapshot
|
||||
{
|
||||
public string Guid;
|
||||
public int? EnrollmentType;
|
||||
public string EnrollmentState;
|
||||
public bool HasAadTenantId;
|
||||
}
|
||||
|
||||
private struct JoinInfoSnapshot
|
||||
{
|
||||
public string Thumbprint;
|
||||
public bool HasTenantId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user