// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers;
using System.Runtime.Versioning;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Aspire.Cli.DotNet;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Logging;
namespace Aspire.Cli.Telemetry;
/// <summary>
/// Detects whether the current user or machine appears to be Microsoft internal.
/// </summary>
internal interface IInternalMicrosoftDetector
{
/// <summary>
/// Gets whether the current user or machine appears to be Microsoft internal.
/// </summary>
Task<InternalMicrosoftDetectionResult> IsInternalMicrosoftMachineAsync(CancellationToken cancellationToken = default);
}
/// <summary>
/// Caches and runs staged Microsoft-internal probes.
/// </summary>
internal sealed partial class InternalMicrosoftDetector : IInternalMicrosoftDetector
{
private const string MicrosoftGitHubOrg = "microsoft";
private const string MicrosoftTenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
private const string CorpMicrosoftDomainSuffix = ".corp.microsoft.com";
private const string CacheSubdirectoryName = "internal-microsoft";
private const string CacheFileName = "detector.json";
private const string VisualStudioMicrosoftTenantProbeName = "Visual Studio Microsoft tenant";
private const string WslVisualStudioMicrosoftTenantProbeName = "WSL Visual Studio Microsoft tenant";
private const string MacPlatformSsoPath = "/usr/bin/app-sso";
private const int CacheVersion = 6;
private const int MaxGitHubTokenCandidates = 5;
private static readonly TimeSpan s_cacheRefreshInterval = TimeSpan.FromHours(6);
private static readonly TimeSpan s_probeStageTimeout = TimeSpan.FromSeconds(5);
private static readonly TimeSpan s_processProbeTimeout = TimeSpan.FromSeconds(3);
private static readonly TimeSpan s_cancelledProbeDrainTimeout = TimeSpan.FromSeconds(1);
private static readonly TimeSpan s_gitHubHttpTimeout = TimeSpan.FromSeconds(3);
private static readonly TimeSpan s_gitHubCandidateTimeout = TimeSpan.FromSeconds(5);
private readonly string _cacheFilePath;
private readonly CliExecutionContext _executionContext;
private readonly IEnvironment _environment;
private readonly IProcessExecutionFactory _processExecutionFactory;
private readonly HttpMessageHandler? _gitHubHttpMessageHandler;
private readonly TimeSpan _gitHubCandidateTimeout;
private readonly TimeSpan _gitHubHttpTimeout;
private readonly TimeProvider _timeProvider;
private readonly ILogger<InternalMicrosoftDetector> _logger;
private readonly ICIEnvironmentDetector _ciEnvironmentDetector;
private readonly IReadOnlyList<IReadOnlyList<InternalMicrosoftProbe>>? _probeStages;
private readonly TimeSpan _probeStageTimeout;
private readonly string _macPlatformSsoPath;
public InternalMicrosoftDetector(CliExecutionContext executionContext, IEnvironment environment, TimeProvider timeProvider, ILogger<InternalMicrosoftDetector> logger, IProcessExecutionFactory processExecutionFactory, ICIEnvironmentDetector ciEnvironmentDetector)
: this(
executionContext,
environment,
Path.Combine(executionContext.CacheDirectory.FullName, CacheSubdirectoryName, CacheFileName),
timeProvider,
logger,
processExecutionFactory,
ciEnvironmentDetector,
MacPlatformSsoPath,
probeStages: null)
{
}
internal InternalMicrosoftDetector(
CliExecutionContext executionContext,
IEnvironment environment,
string cacheFilePath,
TimeProvider timeProvider,
ILogger<InternalMicrosoftDetector> logger,
IProcessExecutionFactory processExecutionFactory,
ICIEnvironmentDetector ciEnvironmentDetector,
string macPlatformSsoPath,
IReadOnlyList<IReadOnlyList<InternalMicrosoftProbe>>? probeStages,
HttpMessageHandler? gitHubHttpMessageHandler = null,
TimeSpan? gitHubCandidateTimeout = null,
TimeSpan? gitHubHttpTimeout = null,
TimeSpan? probeStageTimeout = null)
{
_cacheFilePath = cacheFilePath;
_executionContext = executionContext;
_environment = environment;
_processExecutionFactory = processExecutionFactory;
_gitHubHttpMessageHandler = gitHubHttpMessageHandler;
_gitHubCandidateTimeout = gitHubCandidateTimeout ?? s_gitHubCandidateTimeout;
_gitHubHttpTimeout = gitHubHttpTimeout ?? s_gitHubHttpTimeout;
_timeProvider = timeProvider;
_logger = logger;
_ciEnvironmentDetector = ciEnvironmentDetector;
_probeStageTimeout = probeStageTimeout ?? s_probeStageTimeout;
_macPlatformSsoPath = macPlatformSsoPath;
_probeStages = probeStages;
}
public async Task<InternalMicrosoftDetectionResult> IsInternalMicrosoftMachineAsync(CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
try
{
var cached = await TryReadCacheAsync(cancellationToken).ConfigureAwait(false);
if (cached.Entry is not null && cached.CacheStatus == InternalMicrosoftDetectorCacheStatus.Hit)
{
stopwatch.Stop();
return new InternalMicrosoftDetectionResult(
cached.Entry.IsInternalMicrosoft,
cached.Entry.Source,
cached.Entry.Alias,
cached.Entry.Domain,
cached.Entry.IsInternalMicrosoft ? InternalMicrosoftDetectorOutcome.Detected : InternalMicrosoftDetectorOutcome.NotDetected,
InternalMicrosoftDetectorCacheStatus.Hit,
stopwatch.Elapsed,
[]);
}
var result = await RunProbeStagesAsync(cached.CacheStatus, cancellationToken).ConfigureAwait(false);
if (result.Outcome is InternalMicrosoftDetectorOutcome.Detected or InternalMicrosoftDetectorOutcome.NotDetected)
{
await TryWriteCacheAsync(result, cancellationToken).ConfigureAwait(false);
}
stopwatch.Stop();
return result with { Duration = stopwatch.Elapsed };
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
stopwatch.Stop();
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(ex, "Internal Microsoft detection failed.");
}
return new InternalMicrosoftDetectionResult(
IsInternalMicrosoft: false,
Source: null,
Alias: null,
Domain: null,
Outcome: InternalMicrosoftDetectorOutcome.Failed,
CacheStatus: InternalMicrosoftDetectorCacheStatus.Miss,
Duration: stopwatch.Elapsed,
ProbeDiagnostics: []);
}
}
private IReadOnlyList<IReadOnlyList<InternalMicrosoftProbe>> CreateDefaultProbeStages()
{
// Probes are ordered by cost and signal quality. Local account stores and OS enrollment
// state come from standard developer-machine tooling: Windows dsregcmd, Visual Studio
// IdentityService, macOS Platform SSO, gh/Copilot CLI auth, and
// GitHub's organization membership API.
// See:
// - https://learn.microsoft.com/entra/identity/devices/troubleshoot-device-dsregcmd
// - https://learn.microsoft.com/entra/identity/devices/macos-platform-single-sign-on
// - https://docs.github.com/rest/orgs/members
// Fastest/strongest signal probes
var stage1 = new List<InternalMicrosoftProbe>();
if (_environment.IsMacOS())
{
// Use the platform SSO service on MacOS as the strongest signal (indicates machine is enrolled in
// Microsoft Intune and user has a Microsoft account in the Microsoft tenant configured in their keychain)
stage1.Add(new("Mac Platform SSO", CheckMacPlatformSsoAsync));
}
// Probes that may involve more extensive process execution/network calls or are a weaker signal, run last
// to avoid delaying detection when faster/better quality signals are available. CI environments can expose
// GitHub tokens that identify automation rather than a human, so skip token-membership probes there.
var stage2 = new List<InternalMicrosoftProbe>();
if (!IsCIEnvironment())
{
// Is there a GitHub token in the environment that has an active membership in the Microsoft GitHub org?
stage2.Add(new("Environment GitHub token membership", CheckEnvironmentGitHubTokenAsync));
// Is there a GitHub token from the gh CLI that has an active membership in the Microsoft GitHub org?
stage2.Add(new("gh CLI GitHub org membership", CheckGhCliAsync));
// Is there a GitHub token from the Copilot CLI that has an active membership in the Microsoft GitHub org?
stage2.Add(new("Copilot CLI GitHub org membership", CheckCopilotCliAsync));
}
if (_environment.IsWindows())
{
// Stage 1
// Check USERDNSDOMAIN for a corp.microsoft.com domain, which is a strong signal of being on a Microsoft corporate machine or VPN.
// This is much faster than checking workplace join status and doesn't require admin privileges, so we check it in stage 1.
stage1.Add(new("Windows USERDNSDOMAIN", CheckWindowsUserDnsDomainAsync));
// Check for a Microsoft tenant in the Visual Studio account store, which is a strong signal of being a Microsoft employee.
// This is also relatively fast and doesn't require admin privileges, so we check it in stage 1.
stage1.Add(new(VisualStudioMicrosoftTenantProbeName, CheckVisualStudioMicrosoftTenantAsync));
// Stage 2
// Check if the machine is workplace joined to the Microsoft tenant, which is a strong signal of being on a Microsoft corporate machine,
// but can be slower to evaluate so we check it in stage 2.
stage2.Add(new("Windows workplace join", CheckWindowsWorkplaceJoinAsync));
}
else if (IsWsl())
{
// Stage 1
// Check USERDNSDOMAIN for a corp.microsoft.com domain on the Windows host, which is a strong signal of being on a Microsoft corporate machine or VPN.
// This is much faster than checking workplace join status and doesn't require admin privileges, so we check it in stage 1.
stage1.Add(new("WSL Windows USERDNSDOMAIN", CheckWslWindowsUserDnsDomainAsync));
// Check for a Microsoft tenant in the Visual Studio account store on the Windows host, which is a strong signal of being a Microsoft employee.
// This is also relatively fast and doesn't require admin privileges, so we check it in stage 1.
stage1.Add(new(WslVisualStudioMicrosoftTenantProbeName, CheckWslVisualStudioMicrosoftTenantAsync));
// Stage 2
// Check if the Windows host machine is workplace joined to the Microsoft tenant, which is a strong signal of being on a Microsoft corporate machine,
// but can be slower to evaluate so we check it in stage 2.
stage2.Add(new("WSL Windows workplace join", CheckWslWindowsWorkplaceJoinAsync));
stage2.Add(new("WSL Windows gh.exe GitHub org membership", CheckWslWindowsGhCliAsync));
}
return [stage1, stage2];
}
private async Task<InternalMicrosoftDetectionResult> RunProbeStagesAsync(
string cacheStatus,
CancellationToken cancellationToken)
{
var diagnostics = new List<InternalMicrosoftProbeDiagnostic>();
var timedOut = false;
var probeStages = _probeStages ?? CreateDefaultProbeStages();
foreach (var stage in probeStages)
{
cancellationToken.ThrowIfCancellationRequested();
if (stage.Count == 0)
{
continue;
}
var stageResult = await RunProbeStageAsync(stage, cancellationToken).ConfigureAwait(false);
diagnostics.AddRange(stageResult.Diagnostics);
timedOut |= stageResult.TimedOut;
if (stageResult.Result is not null)
{
return stageResult.Result with
{
Outcome = InternalMicrosoftDetectorOutcome.Detected,
CacheStatus = cacheStatus,
Duration = TimeSpan.Zero,
ProbeDiagnostics = diagnostics
};
}
}
var anyProbeFailed = diagnostics.Any(diagnostic => diagnostic.Outcome == InternalMicrosoftProbeOutcome.Failed);
return new InternalMicrosoftDetectionResult(
IsInternalMicrosoft: false,
Source: null,
Alias: null,
Domain: null,
Outcome: timedOut
? InternalMicrosoftDetectorOutcome.TimedOut
: anyProbeFailed
? InternalMicrosoftDetectorOutcome.Failed
: InternalMicrosoftDetectorOutcome.NotDetected,
CacheStatus: cacheStatus,
Duration: TimeSpan.Zero,
ProbeDiagnostics: diagnostics);
}
private async Task<InternalMicrosoftProbeStageResult> RunProbeStageAsync(IReadOnlyList<InternalMicrosoftProbe> probes, CancellationToken cancellationToken)
{
var stageStartTimestamp = Stopwatch.GetTimestamp();
var stageDeadlineTimestamp = stageStartTimestamp + (long)(_probeStageTimeout.TotalSeconds * Stopwatch.Frequency);
var stageTimeoutTimestamp = long.MaxValue;
using var stageTimeout = new CancellationTokenSource();
using var stageCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, stageTimeout.Token);
using var stageTimeoutRegistration = stageTimeout.Token.Register(
() => Interlocked.Exchange(ref stageTimeoutTimestamp, Stopwatch.GetTimestamp()));
stageTimeout.CancelAfter(_probeStageTimeout);
var probeTasks = probes.Select(probe => RunProbeAsync(probe, stageCancellation.Token)).ToList();
var timedOut = false;
try
{
await Task.WhenAll(probeTasks).WaitAsync(stageCancellation.Token).ConfigureAwait(false);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested && stageTimeout.IsCancellationRequested)
{
timedOut = true;
}
finally
{
await stageCancellation.CancelAsync().ConfigureAwait(false);
await DrainCancelledProbesAsync(probeTasks).ConfigureAwait(false);
}
// Timers can fire slightly before their nominal deadline on some platforms. Once the
// timeout token is signaled, results completed during cancellation draining are late even
// when their timestamp precedes the originally calculated deadline.
stageDeadlineTimestamp = Math.Min(stageDeadlineTimestamp, Volatile.Read(ref stageTimeoutTimestamp));
var completedResults = new List<InternalMicrosoftProbeRunResult>();
var diagnostics = new List<InternalMicrosoftProbeDiagnostic>();
foreach (var task in probeTasks)
{
if (task.IsCompletedSuccessfully)
{
completedResults.Add(task.Result);
var diagnostic = task.Result.Diagnostic;
var deadlineTriggeredCancellation = timedOut && diagnostic.Outcome == InternalMicrosoftProbeOutcome.Cancelled;
diagnostics.Add(deadlineTriggeredCancellation || task.Result.CompletionTimestamp > stageDeadlineTimestamp
? diagnostic with { Outcome = InternalMicrosoftProbeOutcome.TimedOut }
: diagnostic);
}
}
timedOut |= completedResults.Any(result =>
result.CompletionTimestamp > stageDeadlineTimestamp ||
result.Diagnostic.Outcome == InternalMicrosoftProbeOutcome.TimedOut);
foreach (var probe in probes)
{
if (!completedResults.Any(result => result.Source.Equals(probe.Name, StringComparison.Ordinal)))
{
diagnostics.Add(new InternalMicrosoftProbeDiagnostic(probe.Name, InternalMicrosoftProbeOutcome.TimedOut, _probeStageTimeout, HasAlias: false, HasDomain: false));
}
}
// Probe tasks can ignore cancellation and complete while the cancellation drain runs.
// Compare their recorded completion time with the actual stage deadline so scheduler
// latency between deadline expiry and the timeout continuation cannot admit late results.
var result = completedResults
.Where(result => result.CompletionTimestamp <= stageDeadlineTimestamp)
.Where(result => result.Result.IsInternalMicrosoft)
.OrderByDescending(result => GetProbeResultScore(result.Result))
.ThenBy(result => GetProbeIndex(probes, result.Probe))
.FirstOrDefault();
return new InternalMicrosoftProbeStageResult(
result is { Result.IsInternalMicrosoft: true }
? new InternalMicrosoftDetectionResult(IsInternalMicrosoft: true, Source: result.Source, Alias: result.Result.Alias, Domain: result.Result.Domain, Outcome: InternalMicrosoftDetectorOutcome.Detected, CacheStatus: InternalMicrosoftDetectorCacheStatus.Miss, Duration: TimeSpan.Zero, ProbeDiagnostics: [])
: null,
diagnostics,
timedOut);
}
private Task<InternalMicrosoftProbeRunResult> RunProbeAsync(InternalMicrosoftProbe probe, CancellationToken cancellationToken)
{
return Task.Run(async () =>
{
var stopwatch = Stopwatch.StartNew();
try
{
cancellationToken.ThrowIfCancellationRequested();
var result = await probe.DetectAsync(cancellationToken).ConfigureAwait(false);
stopwatch.Stop();
var outcome = result.Failure?.Code == InternalMicrosoftProbeFailureCode.ProcessTimeout
? InternalMicrosoftProbeOutcome.TimedOut
: result.Failure is not null
? InternalMicrosoftProbeOutcome.Failed
: result.IsInternalMicrosoft
? InternalMicrosoftProbeOutcome.Detected
: InternalMicrosoftProbeOutcome.NotDetected;
return new InternalMicrosoftProbeRunResult(
probe,
probe.Name,
result,
new InternalMicrosoftProbeDiagnostic(probe.Name, outcome, probe.DurationOverride ?? stopwatch.Elapsed, !string.IsNullOrEmpty(result.Alias), !string.IsNullOrEmpty(result.Domain), result.Failure),
Stopwatch.GetTimestamp());
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
stopwatch.Stop();
return new InternalMicrosoftProbeRunResult(
probe,
probe.Name,
InternalMicrosoftProbeResult.NotDetected,
new InternalMicrosoftProbeDiagnostic(probe.Name, InternalMicrosoftProbeOutcome.Cancelled, stopwatch.Elapsed, HasAlias: false, HasDomain: false),
Stopwatch.GetTimestamp());
}
catch (Exception ex)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(ex, "Microsoft internal probe '{ProbeName}' failed.", probe.Name);
}
stopwatch.Stop();
return new InternalMicrosoftProbeRunResult(
probe,
probe.Name,
InternalMicrosoftProbeResult.NotDetected,
new InternalMicrosoftProbeDiagnostic(
probe.Name,
InternalMicrosoftProbeOutcome.Failed,
stopwatch.Elapsed,
HasAlias: false,
HasDomain: false,
Failure: CreateExceptionFailure(ex, InternalMicrosoftProbeFailureStage.Probe)),
Stopwatch.GetTimestamp());
}
}, CancellationToken.None);
}
private static InternalMicrosoftProbeFailure CreateExceptionFailure(Exception exception, string stage)
{
return new(InternalMicrosoftProbeFailureCode.Exception, stage, ExceptionType: GetSafeExceptionType(exception));
}
private static string GetSafeExceptionType(Exception exception)
{
return exception switch
{
UnauthorizedAccessException => InternalMicrosoftProbeExceptionType.UnauthorizedAccess,
IOException => InternalMicrosoftProbeExceptionType.Io,
JsonException => InternalMicrosoftProbeExceptionType.Json,
HttpRequestException => InternalMicrosoftProbeExceptionType.HttpRequest,
TaskCanceledException => InternalMicrosoftProbeExceptionType.TaskCanceled,
InvalidOperationException => InternalMicrosoftProbeExceptionType.InvalidOperation,
_ => InternalMicrosoftProbeExceptionType.Other
};
}
private static int GetProbeResultScore(InternalMicrosoftProbeResult result)
{
var score = 0;
if (!string.IsNullOrEmpty(result.Alias))
{
score += 2;
}
if (!string.IsNullOrEmpty(result.Domain))
{
score += 1;
}
return score;
}
private static int GetProbeIndex(IReadOnlyList<InternalMicrosoftProbe> probes, InternalMicrosoftProbe probe)
{
for (var index = 0; index < probes.Count; index++)
{
if (ReferenceEquals(probes[index], probe))
{
return index;
}
}
return int.MaxValue;
}
private async Task DrainCancelledProbesAsync(IReadOnlyList<Task<InternalMicrosoftProbeRunResult>> probeTasks)
{
try
{
await Task.WhenAll(probeTasks).WaitAsync(s_cancelledProbeDrainTimeout).ConfigureAwait(false);
}
catch (TimeoutException ex)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(ex, "Timed out waiting for cancelled Microsoft internal probes to drain.");
}
}
catch (Exception ex)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(ex, "A cancelled Microsoft internal probe failed while draining.");
}
}
}
private async Task<InternalMicrosoftCacheReadResult> TryReadCacheAsync(CancellationToken cancellationToken)
{
if (!File.Exists(_cacheFilePath))
{
return new InternalMicrosoftCacheReadResult(null, InternalMicrosoftDetectorCacheStatus.Miss);
}
try
{
var json = await File.ReadAllTextAsync(_cacheFilePath, cancellationToken).ConfigureAwait(false);
var entry = JsonSerializer.Deserialize(json, JsonSourceGenerationContext.Default.InternalMicrosoftDetectorCacheEntry);
if (entry is null)
{
return new InternalMicrosoftCacheReadResult(null, InternalMicrosoftDetectorCacheStatus.Stale);
}
var isCIEnvironment = IsCIEnvironment();
if (entry.Version == 0)
{
// Legacy entries do not record whether the probe set ran in CI mode. Treat every
// legacy result as stale so CI automation identity can never be reused as local
// identity and the current probe/privacy rules always run before values are emitted.
return new InternalMicrosoftCacheReadResult(null, InternalMicrosoftDetectorCacheStatus.Stale);
}
else if (entry.Version != CacheVersion || entry.IsCIEnvironment != isCIEnvironment)
{
return new InternalMicrosoftCacheReadResult(null, InternalMicrosoftDetectorCacheStatus.Stale);
}
entry = NormalizeCacheEntry(entry);
var hasRequiredSource = !entry.IsInternalMicrosoft || !string.IsNullOrEmpty(entry.Source);
var isFresh = hasRequiredSource && _timeProvider.GetUtcNow() - entry.LastRunUtc < s_cacheRefreshInterval;
return isFresh
? new InternalMicrosoftCacheReadResult(entry, InternalMicrosoftDetectorCacheStatus.Hit)
: new InternalMicrosoftCacheReadResult(null, InternalMicrosoftDetectorCacheStatus.Stale);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(ex, "Failed to read Microsoft internal detector cache from {CacheFilePath}.", _cacheFilePath);
}
return new InternalMicrosoftCacheReadResult(null, InternalMicrosoftDetectorCacheStatus.Stale);
}
}
private static InternalMicrosoftDetectorCacheEntry NormalizeCacheEntry(InternalMicrosoftDetectorCacheEntry entry)
{
var alias = NormalizeAlias(entry.Alias);
var domain = NormalizeAdDomainName(entry.Domain);
return entry with
{
Alias = alias,
Domain = domain
};
}
private async Task TryWriteCacheAsync(InternalMicrosoftDetectionResult result, CancellationToken cancellationToken)
{
var directory = Path.GetDirectoryName(_cacheFilePath);
if (string.IsNullOrEmpty(directory))
{
return;
}
var tempPath = Path.Combine(directory, $"{Path.GetRandomFileName()}.tmp");
try
{
Directory.CreateDirectory(directory);
var entry = new InternalMicrosoftDetectorCacheEntry
{
Version = CacheVersion,
IsInternalMicrosoft = result.IsInternalMicrosoft,
Source = result.Source,
Alias = result.Alias,
Domain = result.Domain,
IsCIEnvironment = IsCIEnvironment(),
LastRunUtc = _timeProvider.GetUtcNow()
};
var json = JsonSerializer.Serialize(entry, JsonSourceGenerationContext.Default.InternalMicrosoftDetectorCacheEntry);
await File.WriteAllTextAsync(tempPath, json, cancellationToken).ConfigureAwait(false);
File.Move(tempPath, _cacheFilePath, overwrite: true);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(ex, "Failed to write Microsoft internal detector cache to {CacheFilePath}.", _cacheFilePath);
}
}
finally
{
FileDeleteHelper.TryDeleteFile(tempPath);
}
}
internal Task<InternalMicrosoftProbeResult> CheckWindowsUserDnsDomainAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var userDnsDomain = _environment.GetEnvironmentVariable("USERDNSDOMAIN");
var domain = ExtractAdDomainNameFromCorpDnsName(userDnsDomain);
return Task.FromResult(domain is not null
? Detected(_environment.GetEnvironmentVariable("USERNAME"), domain)
: InternalMicrosoftProbeResult.NotDetected);
}
private async Task<InternalMicrosoftProbeResult> CheckWslWindowsUserDnsDomainAsync(CancellationToken cancellationToken)
{
if (!CommandExists("cmd.exe"))
{
return InternalMicrosoftProbeResult.NotDetected;
}
var result = await RunProcessAsync("cmd.exe", ["/c", "echo %USERDNSDOMAIN%&echo %USERNAME%"], cancellationToken).ConfigureAwait(false);
if (GetProcessFailure(result, treatNonZeroExitAsFailure: true) is { } processFailure)
{
return processFailure;
}
var outputLines = result.Stdout.Split('\n', StringSplitOptions.TrimEntries);
var userDnsDomain = outputLines.FirstOrDefault() ?? string.Empty;
var userName = outputLines.Skip(1).FirstOrDefault() ?? string.Empty;
var domain = ExtractAdDomainNameFromCorpDnsName(userDnsDomain);
return result.ExitCode == 0 && domain is not null
? Detected(userName, domain)
: InternalMicrosoftProbeResult.NotDetected;
}
[SupportedOSPlatform("windows")]
private async Task<InternalMicrosoftProbeResult> CheckVisualStudioMicrosoftTenantAsync(CancellationToken cancellationToken)
{
var localAppData = GetSpecialFolderPath(Environment.SpecialFolder.LocalApplicationData, "LOCALAPPDATA");
if (string.IsNullOrWhiteSpace(localAppData))
{
return InternalMicrosoftProbeResult.NotDetected;
}
var accountStore = Path.Combine(localAppData, ".IdentityService", "V3AccountStore.json");
if (!File.Exists(accountStore))
{
return InternalMicrosoftProbeResult.NotDetected;
}
try
{
var text = await File.ReadAllTextAsync(accountStore, cancellationToken).ConfigureAwait(false);
return DetectVisualStudioMicrosoftTenant(text, cancellationToken);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
return InternalMicrosoftProbeResult.Failed(new(
InternalMicrosoftProbeFailureCode.FileUnreadable,
InternalMicrosoftProbeFailureStage.AccountStore,
ExceptionType: GetSafeExceptionType(ex)));
}
}
private async Task<InternalMicrosoftProbeResult> CheckWslVisualStudioMicrosoftTenantAsync(CancellationToken cancellationToken)
{
if (!CommandExists("cmd.exe"))
{
return InternalMicrosoftProbeResult.NotDetected;
}
var result = await RunProcessAsync(
"cmd.exe",
["/c", "if exist \"%LOCALAPPDATA%\\.IdentityService\\V3AccountStore.json\" type \"%LOCALAPPDATA%\\.IdentityService\\V3AccountStore.json\""],
cancellationToken).ConfigureAwait(false);
if (GetProcessFailure(result, treatNonZeroExitAsFailure: true) is { } processFailure)
{
return processFailure;
}
return DetectVisualStudioMicrosoftTenant(result.Stdout, cancellationToken);
}
internal async Task<InternalMicrosoftProbeResult> CheckMacPlatformSsoAsync(CancellationToken cancellationToken)
{
// app-sso is a macOS system utility. Use its fixed location so a restricted PATH does not
// hide Platform SSO registration from non-interactive CLI invocations.
if (!File.Exists(_macPlatformSsoPath))
{
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.CommandMissing, InternalMicrosoftProbeFailureStage.PlatformSso);
}
var result = await RunProcessAsync(_macPlatformSsoPath, ["platform", "-s"], cancellationToken).ConfigureAwait(false);
if (GetProcessFailure(result, treatNonZeroExitAsFailure: true) is { } processFailure)
{
return processFailure;
}
return EvaluateMacPlatformSso($"{result.Stdout}{Environment.NewLine}{result.Stderr}");
}
internal static InternalMicrosoftProbeResult EvaluateMacPlatformSso(string output)
{
// app-sso platform -s emits diagnostic text containing separate JSON objects:
// Time: 2026-08-25 12:34:56 +0000
// Device Configuration:
// { "registrationCompleted" : true, ... }
// Login Configuration:
// { "issuer" : "https://login.microsoftonline.com/<tenant>/v2.0", ... }
// User Configuration:
// { "kerberosStatus" : [{ "realm" : "...", "upn" : "..." }], ... }
// The Platform SSO command has no JSON mode and is explicitly diagnostic, so parse each
// labeled object independently and fail closed if its structure changes.
// See https://developer.apple.com/documentation/authenticationservices/creating-extensions-that-support-platform-sso.
var deviceConfiguration = TryParseMacPlatformSsoSection(output, "Device Configuration");
var loginConfiguration = TryParseMacPlatformSsoSection(output, "Login Configuration");
var userConfiguration = TryParseMacPlatformSsoSection(output, "User Configuration");
if (deviceConfiguration is null || loginConfiguration is null || userConfiguration is null)
{
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.JsonParse, InternalMicrosoftProbeFailureStage.PlatformSso);
}
if (!TryGetBoolean(deviceConfiguration, "registrationCompleted", out var registrationCompleted))
{
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.JsonShape, InternalMicrosoftProbeFailureStage.PlatformSsoRegistration);
}
if (!registrationCompleted)
{
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.RegistrationIncomplete, InternalMicrosoftProbeFailureStage.PlatformSsoRegistration);
}
var issuer = TryGetAbsoluteUri(loginConfiguration, "issuer");
if (issuer is null)
{
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.JsonShape, InternalMicrosoftProbeFailureStage.PlatformSsoIssuer);
}
if (!IsMicrosoftTenantEndpoint(issuer, "/v2.0"))
{
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.TenantMismatch, InternalMicrosoftProbeFailureStage.PlatformSsoIssuer);
}
var keyEndpoint = TryGetAbsoluteUri(loginConfiguration, "keyEndpointURL");
if (keyEndpoint is null)
{
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.JsonShape, InternalMicrosoftProbeFailureStage.PlatformSsoKeyEndpoint);
}
if (!IsMicrosoftTenantEndpoint(keyEndpoint, "/getkeydata"))
{
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.TenantMismatch, InternalMicrosoftProbeFailureStage.PlatformSsoKeyEndpoint);
}
var tokenEndpoint = TryGetAbsoluteUri(loginConfiguration, "tokenEndpointURL");
if (tokenEndpoint is null)
{
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.JsonShape, InternalMicrosoftProbeFailureStage.PlatformSsoTokenEndpoint);
}
if (!IsMicrosoftTenantEndpoint(tokenEndpoint, "/oauth2/v2.0/token"))
{
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.TenantMismatch, InternalMicrosoftProbeFailureStage.PlatformSsoTokenEndpoint);
}
if (!userConfiguration.TryGetPropertyValue("kerberosStatus", out var kerberosStatusNode) ||
kerberosStatusNode is null)
{
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.RegistrationIncomplete, InternalMicrosoftProbeFailureStage.PlatformSsoIdentity);
}
if (kerberosStatusNode is not JsonArray kerberosStatuses)
{
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.JsonShape, InternalMicrosoftProbeFailureStage.PlatformSsoIdentity);
}
if (kerberosStatuses.Count == 0)
{
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.RegistrationIncomplete, InternalMicrosoftProbeFailureStage.PlatformSsoIdentity);
}
foreach (var kerberosStatusNodeEntry in kerberosStatuses)
{
if (kerberosStatusNodeEntry is not JsonObject kerberosStatus)
{
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.JsonShape, InternalMicrosoftProbeFailureStage.PlatformSsoIdentity);
}
var upn = TryGetString(kerberosStatus, "upn");
var realmDomain = ExtractAdDomainNameFromCorpDnsName(TryGetString(kerberosStatus, "realm"));
var upnDomain = ExtractAdDomainNameFromAccountIdentifier(upn);
if (realmDomain is not null &&
upnDomain?.Equals(realmDomain, StringComparison.OrdinalIgnoreCase) == true &&
ExtractAliasFromAccountIdentifier(upn) is { } alias)
{
return Detected(alias, realmDomain);
}
}
return PlatformSsoFailure(InternalMicrosoftProbeFailureCode.IdentityMismatch, InternalMicrosoftProbeFailureStage.PlatformSsoIdentity);
}
private static InternalMicrosoftProbeResult PlatformSsoFailure(string code, string stage)
=> InternalMicrosoftProbeResult.Failed(new(code, stage));
internal async Task<InternalMicrosoftProbeResult> CheckWindowsWorkplaceJoinAsync(CancellationToken cancellationToken)
{
if (!CommandExists("dsregcmd"))
{
return InternalMicrosoftProbeResult.NotDetected;
}
var result = await RunProcessAsync("dsregcmd", ["/status"], cancellationToken).ConfigureAwait(false);
if (GetProcessFailure(result, treatNonZeroExitAsFailure: true) is { } processFailure)
{
return processFailure;
}
return EvaluateWindowsWorkplaceJoin(
result.Stdout,
_environment.GetEnvironmentVariable("USERDNSDOMAIN"));
}
private async Task<InternalMicrosoftProbeResult> CheckWslWindowsWorkplaceJoinAsync(CancellationToken cancellationToken)
{
if (!CommandExists("cmd.exe"))
{
return InternalMicrosoftProbeResult.NotDetected;
}
var result = await RunProcessAsync("cmd.exe", ["/c", "dsregcmd /status"], cancellationToken).ConfigureAwait(false);
if (GetProcessFailure(result, treatNonZeroExitAsFailure: true) is { } processFailure)
{
return processFailure;
}
return EvaluateWindowsWorkplaceJoin(result.Stdout, fallbackDomain: null);
}
private Task<InternalMicrosoftProbeResult> CheckGhCliAsync(CancellationToken cancellationToken)
=> CheckGhCliExecutableAsync("gh", cancellationToken);
private Task<InternalMicrosoftProbeResult> CheckWslWindowsGhCliAsync(CancellationToken cancellationToken)
=> CheckGhCliExecutableAsync("gh.exe", cancellationToken);
internal async Task<InternalMicrosoftProbeResult> CheckGhCliExecutableAsync(string executable, CancellationToken cancellationToken)
{
if (IsCIEnvironment())
{
return InternalMicrosoftProbeResult.NotDetected;
}
if (!CommandExists(executable))
{
return InternalMicrosoftProbeResult.NotDetected;
}
var tokenResult = await RunProcessAsync(executable, ["auth", "token", "--hostname", "github.com"], cancellationToken).ConfigureAwait(false);
// gh reserves exit code 4 for an expected authentication-required state.
// Treat that as a clean negative without inspecting or reporting command output.
// https://cli.github.com/manual/gh_help_exit-codes
var authenticationRequired = tokenResult.ExitCode == 4;
if (GetProcessFailure(tokenResult, treatNonZeroExitAsFailure: !authenticationRequired) is { } processFailure)
{
return processFailure;
}
if (authenticationRequired || string.IsNullOrWhiteSpace(tokenResult.Stdout))
{
return InternalMicrosoftProbeResult.NotDetected;
}
using var http = CreateGitHubHttpClient();
return ToProbeResult(await CheckGitHubMembershipWithTokenAsync(http, tokenResult.Stdout.Trim(), cancellationToken).ConfigureAwait(false));
}
private async Task<InternalMicrosoftProbeResult> CheckEnvironmentGitHubTokenAsync(CancellationToken cancellationToken)
{
if (IsCIEnvironment())
{
return InternalMicrosoftProbeResult.NotDetected;
}
var tokenCandidates = DeduplicateTokenCandidates(GetGitHubTokenEnvironmentCandidates(cancellationToken));
if (tokenCandidates.Count == 0)
{
return InternalMicrosoftProbeResult.NotDetected;
}
return ToProbeResult(await CheckAnyGitHubMembershipCandidateAsync(tokenCandidates, cancellationToken).ConfigureAwait(false));
}
internal async Task<InternalMicrosoftProbeResult> CheckCopilotCliAsync(CancellationToken cancellationToken)
{
if (IsCIEnvironment())
{
return InternalMicrosoftProbeResult.NotDetected;
}
var tokenCandidates = new List<TokenCandidate>();
foreach (var (name, value) in GetEnvironmentVariables())
{
cancellationToken.ThrowIfCancellationRequested();
if (name.StartsWith("COPILOT_GH_ACCOUNT_", StringComparison.OrdinalIgnoreCase) && value is not null && LooksLikeGitHubToken(value))
{
tokenCandidates.Add(new TokenCandidate(value));
}
}
var copilotHome = Path.Combine(_executionContext.HomeDirectory.FullName, ".copilot");
foreach (var path in EnumerateExistingFiles(copilotHome, cancellationToken, "config.json", "settings.json"))
{
tokenCandidates.AddRange(ExtractGitHubTokenCandidates(path, cancellationToken));
}
tokenCandidates = DeduplicateTokenCandidates(tokenCandidates);
if (tokenCandidates.Count == 0)
{
return InternalMicrosoftProbeResult.NotDetected;
}
return ToProbeResult(await CheckAnyGitHubMembershipCandidateAsync(tokenCandidates, cancellationToken).ConfigureAwait(false));
}
private async Task<GitHubMembershipCheckResult> CheckAnyGitHubMembershipCandidateAsync(IReadOnlyList<TokenCandidate> candidates, CancellationToken cancellationToken)
{
var candidatesToCheck = candidates.Take(MaxGitHubTokenCandidates).ToArray();
if (candidatesToCheck.Length == 0)
{
return GitHubMembershipCheckResult.NotMember;
}
using var timeoutSource = new CancellationTokenSource(_gitHubCandidateTimeout);
using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutSource.Token);
var candidateTasks = candidatesToCheck
.Select(candidate => CheckGitHubMembershipCandidateAsync(candidate, linkedSource.Token))
.ToList();
InternalMicrosoftProbeFailure? failure = null;
try
{
while (candidateTasks.Count > 0)
{
var completedTask = await Task.WhenAny(candidateTasks).WaitAsync(linkedSource.Token).ConfigureAwait(false);
candidateTasks.Remove(completedTask);
var result = await completedTask.ConfigureAwait(false);
if (result.IsMember)
{
await linkedSource.CancelAsync().ConfigureAwait(false);
await DrainGitHubCandidateTasksAsync(candidateTasks).ConfigureAwait(false);
return result;
}
failure ??= result.Failure;
}
return failure is null
? GitHubMembershipCheckResult.NotMember
: new GitHubMembershipCheckResult(IsMember: false, Failure: failure);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested && timeoutSource.IsCancellationRequested)
{
return new GitHubMembershipCheckResult(
IsMember: false,
new InternalMicrosoftProbeFailure(
InternalMicrosoftProbeFailureCode.RequestFailed,
InternalMicrosoftProbeFailureStage.GitHubCandidates,
ExceptionType: InternalMicrosoftProbeExceptionType.TaskCanceled));
}
finally
{
await linkedSource.CancelAsync().ConfigureAwait(false);
await DrainGitHubCandidateTasksAsync(candidateTasks).ConfigureAwait(false);
}
}
private async Task<GitHubMembershipCheckResult> CheckGitHubMembershipCandidateAsync(TokenCandidate candidate, CancellationToken cancellationToken)
{
try
{
using var http = CreateGitHubHttpClient();
return await CheckGitHubMembershipWithTokenAsync(http, candidate.Token, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex) when (ex is HttpRequestException or JsonException or InvalidOperationException or TaskCanceledException)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(ex, "GitHub token membership probe failed.");
}
return new GitHubMembershipCheckResult(
IsMember: false,
new InternalMicrosoftProbeFailure(
InternalMicrosoftProbeFailureCode.RequestFailed,
InternalMicrosoftProbeFailureStage.GitHubCandidates,
ExceptionType: GetSafeExceptionType(ex)));
}
}
private async Task DrainGitHubCandidateTasksAsync(IReadOnlyList<Task<GitHubMembershipCheckResult>> candidateTasks)
{
if (candidateTasks.Count == 0)
{
return;
}
try
{
await Task.WhenAll(candidateTasks).WaitAsync(s_cancelledProbeDrainTimeout).ConfigureAwait(false);
}
catch (Exception ex) when (ex is OperationCanceledException or TimeoutException or HttpRequestException or JsonException or InvalidOperationException or TaskCanceledException)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(ex, "A cancelled GitHub token membership probe failed while draining.");
}
}
}
internal async Task<bool> CheckGitHubMembershipWithTokenAsync(string token, CancellationToken cancellationToken)
{
using var http = CreateGitHubHttpClient();
return (await CheckGitHubMembershipWithTokenAsync(http, token, cancellationToken).ConfigureAwait(false)).IsMember;
}
internal async Task<InternalMicrosoftProbeResult> CheckGitHubMembershipWithTokenResultForTestingAsync(string token, CancellationToken cancellationToken)
{
using var http = CreateGitHubHttpClient();
return ToProbeResult(await CheckGitHubMembershipWithTokenAsync(http, token, cancellationToken).ConfigureAwait(false));
}
private static async Task<GitHubMembershipCheckResult> CheckGitHubMembershipWithTokenAsync(HttpClient http, string token, CancellationToken cancellationToken)
{
using var userRequest = NewGitHubRequest(HttpMethod.Get, "https://api.github.com/user", token);
using var userResponse = await http.SendAsync(userRequest, cancellationToken).ConfigureAwait(false);
if (!userResponse.IsSuccessStatusCode)
{
return HttpFailure(userResponse.StatusCode, InternalMicrosoftProbeFailureStage.GitHubUser);
}
string? login;
try
{
login = await ReadJsonPropertyAsync(userResponse, "login", cancellationToken).ConfigureAwait(false);
}
catch (JsonException)
{
return JsonFailure(InternalMicrosoftProbeFailureStage.GitHubUser, parseFailure: true);
}
if (string.IsNullOrWhiteSpace(login))
{
return JsonFailure(InternalMicrosoftProbeFailureStage.GitHubUser, parseFailure: false);
}
using var membershipRequest = NewGitHubRequest(HttpMethod.Get, $"https://api.github.com/user/memberships/orgs/{MicrosoftGitHubOrg}", token);
using var membershipResponse = await http.SendAsync(membershipRequest, cancellationToken).ConfigureAwait(false);
if (membershipResponse.IsSuccessStatusCode)
{
try
{
using var doc = JsonDocument.Parse(await membershipResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
var state = TryGetString(doc.RootElement, "state");
return state is null
? JsonFailure(InternalMicrosoftProbeFailureStage.GitHubMembership, parseFailure: false)
: new GitHubMembershipCheckResult(state.Equals("active", StringComparison.OrdinalIgnoreCase));
}
catch (JsonException)
{
return JsonFailure(InternalMicrosoftProbeFailureStage.GitHubMembership, parseFailure: true);
}
}
if (membershipResponse.StatusCode != HttpStatusCode.NotFound)
{
return HttpFailure(membershipResponse.StatusCode, InternalMicrosoftProbeFailureStage.GitHubMembership);
}
using var publicMemberRequest = NewGitHubRequest(HttpMethod.Get, $"https://api.github.com/orgs/{MicrosoftGitHubOrg}/public_members/{login}", token);
using var publicMemberResponse = await http.SendAsync(publicMemberRequest, cancellationToken).ConfigureAwait(false);
return publicMemberResponse.StatusCode == HttpStatusCode.NoContent
? new GitHubMembershipCheckResult(IsMember: true)
: publicMemberResponse.StatusCode == HttpStatusCode.NotFound
? GitHubMembershipCheckResult.NotMember
: HttpFailure(publicMemberResponse.StatusCode, InternalMicrosoftProbeFailureStage.GitHubPublicMembership);
}
private static InternalMicrosoftProbeResult ToProbeResult(GitHubMembershipCheckResult result)
=> result.IsMember
? Detected(alias: null)
: result.Failure is not null
? InternalMicrosoftProbeResult.Failed(result.Failure)
: InternalMicrosoftProbeResult.NotDetected;
private static GitHubMembershipCheckResult HttpFailure(HttpStatusCode statusCode, string stage)
=> new(
IsMember: false,
new InternalMicrosoftProbeFailure(
InternalMicrosoftProbeFailureCode.HttpStatus,
stage,
HttpStatusCode: (int)statusCode));
private static GitHubMembershipCheckResult JsonFailure(string stage, bool parseFailure)
=> new(
IsMember: false,
new InternalMicrosoftProbeFailure(
parseFailure ? InternalMicrosoftProbeFailureCode.JsonParse : InternalMicrosoftProbeFailureCode.JsonShape,
stage,
ExceptionType: parseFailure ? InternalMicrosoftProbeExceptionType.Json : null));
private HttpClient CreateGitHubHttpClient()
{
var http = _gitHubHttpMessageHandler is null
? new HttpClient()
: new HttpClient(_gitHubHttpMessageHandler, disposeHandler: false);
http.Timeout = _gitHubHttpTimeout;
http.DefaultRequestHeaders.UserAgent.ParseAdd("aspire-cli-internal-microsoft-detector/1.0");
return http;
}
private static HttpRequestMessage NewGitHubRequest(HttpMethod method, string url, string token)
{
var request = new HttpRequestMessage(method, url);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Headers.Accept.ParseAdd("application/vnd.github+json");
request.Headers.Add("X-GitHub-Api-Version", "2022-11-28");
return request;
}
private static async Task<string?> ReadJsonPropertyAsync(HttpResponseMessage response, string propertyName, CancellationToken cancellationToken)
{
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
return TryGetString(doc.RootElement, propertyName);
}
private async Task<ProcessResult> RunProcessAsync(string fileName, string[] arguments, CancellationToken cancellationToken)
{
var stdout = new StringBuilder();
var stderr = new StringBuilder();
var options = new ProcessInvocationOptions
{
SuppressLogging = true,
StandardOutputCallback = line => stdout.AppendLine(line),
StandardErrorCallback = line => stderr.AppendLine(line)
};
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(s_processProbeTimeout);
await using var execution = _processExecutionFactory.CreateExecution(
fileName,
arguments,
env: null,
_executionContext.WorkingDirectory,
options);
var started = false;
try
{
if (!await execution.StartAsync(timeoutCts.Token).ConfigureAwait(false))
{
return new ProcessResult(
ExitCode: -1,
stdout.ToString(),
stderr.ToString(),
new InternalMicrosoftProbeFailure(
InternalMicrosoftProbeFailureCode.ProcessStart,
InternalMicrosoftProbeFailureStage.ProcessStart));
}
started = true;
var exitCode = await execution.WaitForExitAsync(timeoutCts.Token).ConfigureAwait(false);
return new ProcessResult(exitCode, stdout.ToString(), stderr.ToString());
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
return new ProcessResult(
ExitCode: -1,
stdout.ToString(),
stderr.ToString(),
new InternalMicrosoftProbeFailure(
InternalMicrosoftProbeFailureCode.ProcessTimeout,
started ? InternalMicrosoftProbeFailureStage.ProcessExit : InternalMicrosoftProbeFailureStage.ProcessStart));
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
return new ProcessResult(
ExitCode: -1,
stdout.ToString(),
stderr.ToString(),
CreateExceptionFailure(
ex,
started ? InternalMicrosoftProbeFailureStage.ProcessExit : InternalMicrosoftProbeFailureStage.ProcessStart));
}
}
private static InternalMicrosoftProbeResult? GetProcessFailure(ProcessResult result, bool treatNonZeroExitAsFailure)
{
if (result.Failure is not null)
{
return InternalMicrosoftProbeResult.Failed(result.Failure);
}
return treatNonZeroExitAsFailure && result.ExitCode != 0
? InternalMicrosoftProbeResult.Failed(new(
InternalMicrosoftProbeFailureCode.ProcessExit,
InternalMicrosoftProbeFailureStage.ProcessExit,
ProcessExitCode: result.ExitCode))
: null;
}
private static InternalMicrosoftProbeResult EvaluateWindowsWorkplaceJoin(string output, string? fallbackDomain)
{
if (string.IsNullOrWhiteSpace(output))
{
return InternalMicrosoftProbeResult.NotDetected;
}
var values = ParseColonSeparatedFields(output);
var tenantId = values.GetValueOrDefault("TenantId");
var azureAdJoined = IsYes(values.GetValueOrDefault("AzureAdJoined"));
var workplaceJoined = IsYes(values.GetValueOrDefault("WorkplaceJoined"));
var alias = ExtractAliasFromAccountIdentifier(GetFirstValue(values, "UserEmail", "User Email", "UserPrincipalName", "User Principal Name", "UPN"));
var domain = ExtractAdDomainNameFromDsReg(values) ?? ExtractAdDomainNameFromCorpDnsName(fallbackDomain);
return (azureAdJoined || workplaceJoined) && tenantId?.Equals(MicrosoftTenantId, StringComparison.OrdinalIgnoreCase) == true
? Detected(alias, domain)
: InternalMicrosoftProbeResult.NotDetected;
static bool IsYes(string? value)
{
return value?.Equals("YES", StringComparison.OrdinalIgnoreCase) == true;
}
}
private static Dictionary<string, string> ParseColonSeparatedFields(string text)
{
// dsregcmd /status writes colon-separated sections, e.g.:
// AzureAdJoined : YES
// TenantId : 72f988bf-86f1-41af-91ab-2d7cd011db47
// User Email : alias@microsoft.com
// Values can contain additional ':' characters, so split only on the first delimiter.
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var line in text.Split('\n'))
{
var index = line.IndexOf(':', StringComparison.Ordinal);
if (index <= 0)
{
continue;
}
var key = line[..index].Trim();
var value = line[(index + 1)..].Trim();
if (!string.IsNullOrWhiteSpace(key))
{
values[key] = value;
}
}
return values;
}
private static InternalMicrosoftProbeResult DetectVisualStudioMicrosoftTenant(string? text, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(text))
{
return InternalMicrosoftProbeResult.NotDetected;
}
try
{
using var store = JsonDocument.Parse(text);
if (store.RootElement.ValueKind != JsonValueKind.Array)
{
return JsonShapeFailure(InternalMicrosoftProbeFailureStage.AccountStore);
}
var personalizationResults = new List<InternalMicrosoftProbeResult>();
var fallbackResults = new List<InternalMicrosoftProbeResult>();
var hasMalformedPersonalizationAccount = false;
var hasMalformedFallbackAccount = false;
InternalMicrosoftProbeResult? failure = null;
foreach (var account in store.RootElement.EnumerateArray())
{
cancellationToken.ThrowIfCancellationRequested();
var result = TryDetectVisualStudioMicrosoftTenantAccount(account);
if (result.Failure is not null)
{
failure ??= result;
if (HasMicrosoftIdentityProvider(account))
{
if (TryGetBoolean(account, "IsPersonalizationAccount") == true)
{
hasMalformedPersonalizationAccount = true;
}
else
{
hasMalformedFallbackAccount = true;
}
}
continue;
}
if (!result.IsInternalMicrosoft)
{
continue;
}
if (TryGetBoolean(account, "IsPersonalizationAccount") == true)
{
personalizationResults.Add(result);
}
else
{
fallbackResults.Add(result);
}
}
if (personalizationResults.Count > 0)
{
return hasMalformedPersonalizationAccount
? Detected(alias: null)
: SelectUnambiguousIdentity(personalizationResults);
}
if (fallbackResults.Count > 0)
{
return hasMalformedPersonalizationAccount || hasMalformedFallbackAccount
? Detected(alias: null)
: SelectUnambiguousIdentity(fallbackResults);
}
return failure ?? InternalMicrosoftProbeResult.NotDetected;
}
catch (JsonException)
{
return InternalMicrosoftProbeResult.Failed(new(
InternalMicrosoftProbeFailureCode.JsonParse,
InternalMicrosoftProbeFailureStage.AccountStore,
ExceptionType: InternalMicrosoftProbeExceptionType.Json));
}
}
private static bool HasMicrosoftIdentityProvider(JsonElement account)
{
return account.ValueKind == JsonValueKind.Object &&
account.TryGetProperty("Properties", out var properties) &&
properties.ValueKind == JsonValueKind.Object &&
TryGetString(properties, "IdentityProvider")?.Equals(MicrosoftTenantId, StringComparison.OrdinalIgnoreCase) == true;
}
private static InternalMicrosoftProbeResult SelectUnambiguousIdentity(IReadOnlyList<InternalMicrosoftProbeResult> candidates)
{
var first = candidates[0];
return candidates.Skip(1).All(candidate =>
string.Equals(candidate.Alias, first.Alias, StringComparison.Ordinal) &&
string.Equals(candidate.Domain, first.Domain, StringComparison.Ordinal))
? first
: Detected(alias: null);
}
private static InternalMicrosoftProbeResult TryDetectVisualStudioMicrosoftTenantAccount(JsonElement account)
{
// Visual Studio's internal V3 account store currently contains records shaped like:
// [{
// "Stale": false,
// "IsPersonalizationAccount": true,
// "Properties": {
// "IdentityProvider": "<tenant-guid>",
// "HomeTenant": "<tenant-guid>",
// "IdTokenPayload": "{\"tid\":\"<tenant-guid>\",\"iss\":\"https://login.microsoftonline.com/<tenant-guid>/v2.0\",\"preferred_username\":\"alias@microsoft.com\"}"
// }
// }]
// The format is not a supported Visual Studio contract, so require every piece of tenant
// and alias evidence to be structurally bound to one non-stale record and fail closed when
// the shape changes. IsPersonalizationAccount is used only to rank matching records.
if (account.ValueKind != JsonValueKind.Object)
{
return JsonShapeFailure(InternalMicrosoftProbeFailureStage.AccountStoreRecord);
}
var stale = TryGetBoolean(account, "Stale");
if (stale is null)
{
return JsonShapeFailure(InternalMicrosoftProbeFailureStage.AccountStoreRecordStale);
}
if (stale.Value)
{
return InternalMicrosoftProbeResult.NotDetected;
}
if (!account.TryGetProperty("Properties", out var properties) ||
properties.ValueKind != JsonValueKind.Object)
{
return JsonShapeFailure(InternalMicrosoftProbeFailureStage.AccountStoreRecordProperties);
}
var identityProvider = TryGetString(properties, "IdentityProvider");
if (identityProvider is null)
{
return JsonShapeFailure(InternalMicrosoftProbeFailureStage.AccountStoreRecordIdentityProvider);
}
if (!identityProvider.Equals(MicrosoftTenantId, StringComparison.OrdinalIgnoreCase))
{
return InternalMicrosoftProbeResult.NotDetected;
}
var homeTenant = TryGetString(properties, "HomeTenant");
if (homeTenant is null)
{
return JsonShapeFailure(InternalMicrosoftProbeFailureStage.AccountStoreRecordHomeTenant);
}
if (!homeTenant.Equals(MicrosoftTenantId, StringComparison.OrdinalIgnoreCase))
{
return InternalMicrosoftProbeResult.NotDetected;
}
if (TryGetString(properties, "IdTokenPayload") is not { } idTokenPayload)
{
return JsonShapeFailure(InternalMicrosoftProbeFailureStage.IdTokenPayload);
}
try
{
using var token = JsonDocument.Parse(idTokenPayload);
var expectedIssuer = $"https://login.microsoftonline.com/{MicrosoftTenantId}/v2.0";
if (token.RootElement.ValueKind != JsonValueKind.Object)
{
return JsonShapeFailure(InternalMicrosoftProbeFailureStage.IdTokenPayload);
}
var tenant = TryGetString(token.RootElement, "tid");
if (tenant is null)
{
return JsonShapeFailure(InternalMicrosoftProbeFailureStage.IdTokenTenant);
}
if (!tenant.Equals(MicrosoftTenantId, StringComparison.OrdinalIgnoreCase))
{
return InternalMicrosoftProbeResult.NotDetected;
}
var issuer = TryGetString(token.RootElement, "iss");
if (issuer is null)
{
return JsonShapeFailure(InternalMicrosoftProbeFailureStage.IdTokenIssuer);
}
if (!issuer.Equals(expectedIssuer, StringComparison.OrdinalIgnoreCase))
{
return InternalMicrosoftProbeResult.NotDetected;
}
if (ExtractAliasFromAccountIdentifier(TryGetString(token.RootElement, "preferred_username")) is not { } alias)
{
return JsonShapeFailure(InternalMicrosoftProbeFailureStage.IdTokenUsername);
}
return Detected(alias);
}
catch (JsonException)
{
return InternalMicrosoftProbeResult.Failed(new(
InternalMicrosoftProbeFailureCode.JsonParse,
InternalMicrosoftProbeFailureStage.IdTokenPayload,
ExceptionType: InternalMicrosoftProbeExceptionType.Json));
}
}
private static InternalMicrosoftProbeResult JsonShapeFailure(string stage)
=> InternalMicrosoftProbeResult.Failed(new(InternalMicrosoftProbeFailureCode.JsonShape, stage));
internal static InternalMicrosoftProbeResult DetectVisualStudioMicrosoftTenantForTesting(string text, CancellationToken cancellationToken)
=> DetectVisualStudioMicrosoftTenant(text, cancellationToken);
private static bool? TryGetBoolean(JsonElement element, string propertyName)
{
return element.TryGetProperty(propertyName, out var property) &&
property.ValueKind is JsonValueKind.True or JsonValueKind.False
? property.GetBoolean()
: null;
}
private static string? TryGetString(JsonElement element, string propertyName)
{
return element.ValueKind == JsonValueKind.Object &&
element.TryGetProperty(propertyName, out var property) &&
property.ValueKind == JsonValueKind.String
? property.GetString()
: null;
}
private IEnumerable<TokenCandidate> GetGitHubTokenEnvironmentCandidates(CancellationToken cancellationToken)
{
var exactNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"GH_TOKEN",
"GITHUB_TOKEN",
"GITHUB_PAT",
"GITHUB_OAUTH_TOKEN",
"GITHUB_ACCESS_TOKEN"
};
foreach (var (name, value) in GetEnvironmentVariables())
{
cancellationToken.ThrowIfCancellationRequested();
if (exactNames.Contains(name) && value is not null && LooksLikeGitHubToken(value))
{
yield return new TokenCandidate(value);
}
}
}
private static List<TokenCandidate> DeduplicateTokenCandidates(IEnumerable<TokenCandidate> candidates)
{
var seen = new HashSet<string>(StringComparer.Ordinal);
var result = new List<TokenCandidate>();
foreach (var candidate in candidates)
{
if (seen.Add(candidate.Token))
{
result.Add(candidate);
}
}
return result;
}
private static IEnumerable<TokenCandidate> ExtractGitHubTokenCandidates(string filePath, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (!File.Exists(filePath))
{
yield break;
}
string text;
try
{
text = File.ReadAllText(filePath);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
yield break;
}
if (string.IsNullOrWhiteSpace(text))
{
yield break;
}
foreach (Match match in GitHubTokenRegex().Matches(text))
{
cancellationToken.ThrowIfCancellationRequested();
var token = match.Value;
if (LooksLikeGitHubToken(token))
{
yield return new TokenCandidate(token);
}
}
}
private static IEnumerable<string> EnumerateExistingFiles(string directory, CancellationToken cancellationToken, params string[] fileNames)
{
cancellationToken.ThrowIfCancellationRequested();
if (!Directory.Exists(directory))
{
yield break;
}
foreach (var fileName in fileNames)
{
cancellationToken.ThrowIfCancellationRequested();
var path = Path.Combine(directory, fileName);
if (File.Exists(path))
{
yield return path;
}
}
}
private bool IsWsl()
{
if (!_environment.IsLinux())
{
return false;
}
if (!string.IsNullOrWhiteSpace(_environment.GetEnvironmentVariable("WSL_DISTRO_NAME")) ||
!string.IsNullOrWhiteSpace(_environment.GetEnvironmentVariable("WSL_INTEROP")))
{
return true;
}
try
{
if (!File.Exists("/proc/sys/kernel/osrelease"))
{
return false;
}
var osRelease = File.ReadAllText("/proc/sys/kernel/osrelease");
return osRelease.Contains("microsoft", StringComparison.OrdinalIgnoreCase) ||
osRelease.Contains("wsl", StringComparison.OrdinalIgnoreCase);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
return false;
}
}
private bool IsCIEnvironment()
=> _ciEnvironmentDetector.IsCIEnvironment();
private bool CommandExists(string command)
{
var path = _environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
var extensions = _environment.IsWindows() && string.IsNullOrEmpty(Path.GetExtension(command))
? (_environment.GetEnvironmentVariable("PATHEXT") ?? ".EXE;.CMD;.BAT;.COM").Split(';', StringSplitOptions.RemoveEmptyEntries)
: [string.Empty];
foreach (var directory in path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
{
foreach (var extension in extensions)
{
var candidate = Path.Combine(directory, command + extension);
if (File.Exists(candidate))
{
return true;
}
}
}
return false;
}
private string GetSpecialFolderPath(Environment.SpecialFolder folder, string environmentVariableName)
{
return _environment.GetEnvironmentVariable(environmentVariableName) ??
Environment.GetFolderPath(folder);
}
private IEnumerable<(string Name, string? Value)> GetEnvironmentVariables()
{
return _environment.GetEnvironmentVariables();
}
private static JsonObject? TryParseMacPlatformSsoSection(string output, string sectionName)
{
var sectionHeader = $"{sectionName}:";
var headerIndex = -1;
var searchIndex = 0;
while (searchIndex < output.Length)
{
var candidateIndex = output.IndexOf(sectionHeader, searchIndex, StringComparison.Ordinal);
if (candidateIndex < 0)
{
return null;
}
if (candidateIndex == 0 || output[candidateIndex - 1] is '\r' or '\n')
{
headerIndex = candidateIndex;
break;
}
searchIndex = candidateIndex + sectionHeader.Length;
}
if (headerIndex < 0)
{
return null;
}
var objectStart = headerIndex + sectionHeader.Length;
while (objectStart < output.Length && char.IsWhiteSpace(output[objectStart]))
{
objectStart++;
}
if (objectStart >= output.Length || output[objectStart] != '{')
{
return null;
}
var objectEnd = FindJsonObjectEnd(output, objectStart);
if (objectEnd < 0)
{
return null;
}
try
{
return JsonNode.Parse(
output[objectStart..(objectEnd + 1)],
documentOptions: new JsonDocumentOptions { AllowTrailingCommas = true }) as JsonObject;
}
catch (JsonException)
{
return null;
}
}
private static int FindJsonObjectEnd(string text, int objectStart)
{
var depth = 0;
var insideString = false;
var escaped = false;
for (var index = objectStart; index < text.Length; index++)
{
var character = text[index];
if (insideString)
{
if (escaped)
{
escaped = false;
}
else if (character == '\\')
{
escaped = true;
}
else if (character == '"')
{
insideString = false;
}
continue;
}
if (character == '"')
{
insideString = true;
}
else if (character == '{')
{
depth++;
}
else if (character == '}')
{
depth--;
if (depth == 0)
{
return index;
}
if (depth < 0)
{
return -1;
}
}
}
return -1;
}
private static InternalMicrosoftProbeResult Detected(string? alias, string? domain = null)
{
return new InternalMicrosoftProbeResult(IsInternalMicrosoft: true, Alias: NormalizeAlias(alias), Domain: NormalizeAdDomainName(domain));
}
private static string? GetFirstValue(IReadOnlyDictionary<string, string> values, params string[] keys)
{
foreach (var key in keys)
{
if (values.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
{
return value;
}
}
return null;
}
private static string? ExtractAdDomainNameFromDsReg(IReadOnlyDictionary<string, string> values)
{
var domain = GetFirstValue(
values,
"DomainName",
"Domain Name",
"OnPremisesDomainName",
"On Premises Domain Name",
"OnPremDomainName",
"UserDnsDomain",
"User DNS Domain");
return ExtractAdDomainNameFromCorpDnsName(domain) ?? NormalizeAdDomainName(domain);
}
private static string? ExtractAdDomainNameFromCorpDnsName(string? dnsDomain)
{
if (string.IsNullOrWhiteSpace(dnsDomain))
{
return null;
}
var trimmed = dnsDomain.Trim().TrimEnd('.');
if (!trimmed.EndsWith(CorpMicrosoftDomainSuffix, StringComparison.OrdinalIgnoreCase))
{
return null;
}
return NormalizeAdDomainName(trimmed[..^CorpMicrosoftDomainSuffix.Length]);
}
private static string? NormalizeAdDomainName(string? domain)
{
if (string.IsNullOrWhiteSpace(domain))
{
return null;
}
var normalized = domain.Trim().TrimEnd('.');
if (normalized.EndsWith(CorpMicrosoftDomainSuffix, StringComparison.OrdinalIgnoreCase))
{
return ExtractAdDomainNameFromCorpDnsName(normalized);
}
return normalized.All(c => char.IsLetterOrDigit(c) || c is '.' or '_' or '-')
? normalized.ToUpperInvariant()
: null;
}
private static string? ExtractAliasFromAccountIdentifier(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var match = MicrosoftAccountRegex().Match(value);
return match.Success && match.Index == 0 && match.Length == value.Length
? NormalizeAlias(match.Groups["alias"].Value)
: null;
}
private static string? ExtractAdDomainNameFromAccountIdentifier(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var atIndex = value.LastIndexOf('@');
return atIndex >= 0 && atIndex < value.Length - 1
? ExtractAdDomainNameFromCorpDnsName(value[(atIndex + 1)..])
: null;
}
private static string? NormalizeAlias(string? alias)
{
if (string.IsNullOrWhiteSpace(alias))
{
return null;
}
var normalized = alias.Trim();
return normalized.All(c => char.IsAsciiLetterOrDigit(c) || c is '.' or '_' or '-')
? normalized.ToLowerInvariant()
: null;
}
private static bool TryGetBoolean(JsonObject json, string propertyName, out bool value)
{
value = false;
return json.TryGetPropertyValue(propertyName, out var node) &&
node is JsonValue jsonValue &&
jsonValue.TryGetValue(out value);
}
private static Uri? TryGetAbsoluteUri(JsonObject json, string propertyName)
{
return Uri.TryCreate(TryGetString(json, propertyName), UriKind.Absolute, out var uri)
? uri
: null;
}
private static bool IsMicrosoftTenantEndpoint(Uri uri, string expectedSuffix)
{
var expectedPath = $"/{MicrosoftTenantId}{expectedSuffix}";
return uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
uri.Host.Equals("login.microsoftonline.com", StringComparison.OrdinalIgnoreCase) &&
uri.IsDefaultPort &&
string.IsNullOrEmpty(uri.UserInfo) &&
string.IsNullOrEmpty(uri.Query) &&
string.IsNullOrEmpty(uri.Fragment) &&
uri.AbsolutePath.TrimEnd('/').Equals(expectedPath, StringComparison.OrdinalIgnoreCase);
}
private static string? TryGetString(JsonObject json, string propertyName)
{
return json.TryGetPropertyValue(propertyName, out var value) &&
value is JsonValue jsonValue &&
jsonValue.TryGetValue<string>(out var text)
? text
: null;
}
private static bool LooksLikeGitHubToken(string token)
{
return GitHubTokenRegex().IsMatch(token);
}
[GeneratedRegex(@"(?:github_pat_[A-Za-z0-9_]{20,}|gh[opsru]_[A-Za-z0-9_]{20,})")]
private static partial Regex GitHubTokenRegex();
[GeneratedRegex(@"(?<![A-Za-z0-9._%+\-\\])(?<alias>[A-Za-z0-9._%+-]+)@(?<domain>(?:[A-Za-z0-9-]+\.)*microsoft\.com)(?![A-Za-z0-9._%+-])", RegexOptions.IgnoreCase)]
private static partial Regex MicrosoftAccountRegex();
private readonly record struct ProcessResult(int ExitCode, string Stdout, string Stderr, InternalMicrosoftProbeFailure? Failure = null);
private readonly record struct TokenCandidate(string Token);
private readonly record struct GitHubMembershipCheckResult(bool IsMember, InternalMicrosoftProbeFailure? Failure = null)
{
public static GitHubMembershipCheckResult NotMember { get; } = new(IsMember: false);
}
private sealed record InternalMicrosoftCacheReadResult(InternalMicrosoftDetectorCacheEntry? Entry, string CacheStatus);
private sealed record InternalMicrosoftProbeStageResult(InternalMicrosoftDetectionResult? Result, IReadOnlyList<InternalMicrosoftProbeDiagnostic> Diagnostics, bool TimedOut);
private sealed record InternalMicrosoftProbeRunResult(InternalMicrosoftProbe Probe, string Source, InternalMicrosoftProbeResult Result, InternalMicrosoftProbeDiagnostic Diagnostic, long CompletionTimestamp);
}
internal sealed record InternalMicrosoftProbe(
string Name,
Func<CancellationToken, Task<InternalMicrosoftProbeResult>> DetectAsync,
TimeSpan? DurationOverride = null);
internal readonly record struct InternalMicrosoftProbeResult(bool IsInternalMicrosoft, string? Alias, string? Domain, InternalMicrosoftProbeFailure? Failure = null)
{
public static InternalMicrosoftProbeResult NotDetected { get; } = new(IsInternalMicrosoft: false, Alias: null, Domain: null);
public static InternalMicrosoftProbeResult Failed(InternalMicrosoftProbeFailure failure)
=> new(IsInternalMicrosoft: false, Alias: null, Domain: null, Failure: failure);
}
internal static class InternalMicrosoftDetectorOutcome
{
public const string Detected = "detected";
public const string NotDetected = "not_detected";
public const string Failed = "failed";
public const string TimedOut = "timed_out";
}
internal static class InternalMicrosoftDetectorCacheStatus
{
public const string Hit = "hit";
public const string Miss = "miss";
public const string Stale = "stale";
}
internal static class InternalMicrosoftProbeOutcome
{
public const string Detected = "detected";
public const string NotDetected = "not_detected";
public const string Failed = "failed";
public const string Cancelled = "cancelled";
public const string TimedOut = "timed_out";
}
internal sealed record InternalMicrosoftProbeDiagnostic(string Source, string Outcome, TimeSpan Duration, bool HasAlias, bool HasDomain, InternalMicrosoftProbeFailure? Failure = null);
internal sealed record InternalMicrosoftProbeFailure(
string Code,
string Stage,
string? ExceptionType = null,
int? ProcessExitCode = null,
int? HttpStatusCode = null);
internal static class InternalMicrosoftProbeFailureCode
{
public const string CommandMissing = "command_missing";
public const string Exception = "exception";
public const string FileUnreadable = "file_unreadable";
public const string HttpStatus = "http_status";
public const string IdentityMismatch = "identity_mismatch";
public const string JsonParse = "json_parse";
public const string JsonShape = "json_shape";
public const string ProcessExit = "process_exit";
public const string ProcessStart = "process_start";
public const string ProcessTimeout = "process_timeout";
public const string RegistrationIncomplete = "registration_incomplete";
public const string RequestFailed = "request_failed";
public const string TenantMismatch = "tenant_mismatch";
}
internal static class InternalMicrosoftProbeFailureStage
{
public const string AccountStore = "account_store";
public const string AccountStoreRecord = "account_store_record";
public const string AccountStoreRecordHomeTenant = "account_store_record.home_tenant";
public const string AccountStoreRecordIdentityProvider = "account_store_record.identity_provider";
public const string AccountStoreRecordProperties = "account_store_record.properties";
public const string AccountStoreRecordStale = "account_store_record.stale";
public const string GitHubCandidates = "github_candidates";
public const string GitHubMembership = "github_membership";
public const string GitHubPublicMembership = "github_public_membership";
public const string GitHubUser = "github_user";
public const string IdTokenPayload = "id_token_payload";
public const string IdTokenIssuer = "id_token_payload.iss";
public const string IdTokenTenant = "id_token_payload.tid";
public const string IdTokenUsername = "id_token_payload.preferred_username";
public const string PlatformSso = "platform_sso";
public const string PlatformSsoIdentity = "platform_sso.identity";
public const string PlatformSsoIssuer = "platform_sso.issuer";
public const string PlatformSsoKeyEndpoint = "platform_sso.key_endpoint";
public const string PlatformSsoRegistration = "platform_sso.registration";
public const string PlatformSsoTokenEndpoint = "platform_sso.token_endpoint";
public const string ProcessExit = "process_exit";
public const string ProcessStart = "process_start";
public const string Probe = "probe";
}
internal static class InternalMicrosoftProbeExceptionType
{
public const string HttpRequest = "HttpRequestException";
public const string InvalidOperation = "InvalidOperationException";
public const string Io = "IOException";
public const string Json = "JsonException";
public const string Other = "Other";
public const string TaskCanceled = "TaskCanceledException";
public const string UnauthorizedAccess = "UnauthorizedAccessException";
}
internal sealed record InternalMicrosoftDetectionResult(
bool IsInternalMicrosoft,
string? Source,
string? Alias,
string? Domain,
string Outcome,
string CacheStatus,
TimeSpan Duration,
IReadOnlyList<InternalMicrosoftProbeDiagnostic> ProbeDiagnostics)
{
public InternalMicrosoftDetectionResult(bool IsInternalMicrosoft, string? Source, string? Alias, string? Domain)
: this(
IsInternalMicrosoft,
Source,
Alias,
Domain,
IsInternalMicrosoft ? InternalMicrosoftDetectorOutcome.Detected : InternalMicrosoftDetectorOutcome.NotDetected,
InternalMicrosoftDetectorCacheStatus.Miss,
TimeSpan.Zero,
[])
{
}
}
internal sealed record InternalMicrosoftDetectorCacheEntry
{
public int Version { get; init; }
public bool IsInternalMicrosoft { get; init; }
public string? Source { get; init; }
public string? Alias { get; init; }
public string? Domain { get; init; }
public bool IsCIEnvironment { get; init; }
public DateTimeOffset LastRunUtc { get; init; }
}