backend+web: one agent-config derivation (AgentConfigResolver) served on agent/config and GET config/{tenantId}/effective-agent-config; wire-type initializer defaults in the shared manifest; the Tenant Config Report reads both (D-216)

The report still carried two hand copies after D-215: a typed map of C#
initializer defaults for the "custom" highlight, and a TypeScript mirror of
GetAgentConfigFunction's resolution rules for the runtime column.

Backend: the derivation moves verbatim into Services/AgentConfigResolver
(Build = pure, ResolveAsync loads tenant + admin config and the two rule
catalogs). GetAgentConfigFunction keeps the kill verdict, the migrate logging
and the device fields on top; ParseAgentMajor, ResolveDiagnosticsUploadEnabled,
MergeDiagnosticsLogPaths and ResolveMigrateTarget move with it (call sites and
tests retargeted; the QueryParams ratchet baseline shrinks by the file). New
GlobalAdminOnly route config/{tenantId}/effective-agent-config returns the same
AgentConfigResponse without a device (optional ?agentVersion=, default the V2
line). AgentConfigResolverTests pin the rules that used to live in the web:
upload 10, Hosted destination without SAS, SAS regardless of mode, unrestricted
only with entitlement + gate + toggle, operator knobs from AdminConfiguration,
analyzer defaults, hash line per major, migrate override and rejection.

Manifest: SharedManifestParityTests emits "defaults" next to "fields" for
TenantConfiguration and AdminConfiguration and an "agentConfig" section with
the AgentConfigResponse / CollectorConfiguration / AnalyzerConfiguration class
defaults (fresh-instance values; nested objects null). TenantConfiguration.
NtpServer gets the initializer "time.windows.com" the table read already
applies, so the manifest default and a stored row agree.

Web: the catalog rows lose value() and default; the runtime column renders
the endpoint's response, the highlight compares against the manifest defaults,
informational rows (reasons, timestamps, identities, secrets) never read as
custom. Previously excluded fields are visible (configVersion, catalog counts,
sealing pattern ids, hash oracle, migrate target); the collector class
defaults sit in a collapsed "Collector Internals" section. Fail-soft: while the
backend is not deployed the runtime column shows a notice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-09-06 21:59:34 +02:00
parent f4e2088b69
commit d5d1d7bfbd
20 changed files with 1513 additions and 674 deletions
@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using AutopilotMonitor.Functions.Services;
using AutopilotMonitor.Shared.Models;
using Xunit;
namespace AutopilotMonitor.Functions.Tests;
/// <summary>
/// Pins the derivation shared by the agent channel (GET agent/config) and the Global-Admin
/// report route (GET config/{tenantId}/effective-agent-config): every rule the Tenant Config
/// Report used to re-implement in the web now has exactly one implementation, and these
/// facts are its contract.
/// </summary>
public class AgentConfigResolverTests
{
private const string TenantId = "11111111-2222-3333-4444-555555555555";
private static readonly DateTime Now = new(2026, 9, 6, 12, 0, 0, DateTimeKind.Utc);
private static TenantConfiguration Tenant(Action<TenantConfiguration>? mutate = null)
{
var c = new TenantConfiguration { TenantId = TenantId, DomainName = "contoso.example", UpdatedBy = "ga@operator.example" };
mutate?.Invoke(c);
return c;
}
private static AdminConfiguration Admin(Action<AdminConfiguration>? mutate = null)
{
var a = new AdminConfiguration();
mutate?.Invoke(a);
return a;
}
private static AgentConfigResponse Build(TenantConfiguration tenant, AdminConfiguration? admin = null, int agentMajor = AgentConfigResolver.CurrentAgentMajor)
=> AgentConfigResolver.Build(tenant, admin ?? Admin(), new List<GatherRule>(), new List<ImeLogPattern>(), agentMajor, Now).Response;
[Fact]
public void Fresh_tenant_gets_the_agent_defaults()
{
var r = Build(Tenant());
Assert.Equal(AgentConfigResolver.ConfigVersion, r.ConfigVersion);
Assert.Equal(AutopilotMonitor.Shared.Constants.DefaultUploadIntervalSeconds, r.UploadIntervalSeconds);
Assert.Equal(10, r.UploadIntervalSeconds); // the report showed 30 for a month — pinned
Assert.True(r.SelfDestructOnComplete);
Assert.False(r.KeepLogFile);
Assert.True(r.EnableGeoLocation);
Assert.Equal(5, r.MaxAuthFailures);
Assert.Equal(0, r.AuthFailureTimeoutMinutes);
Assert.Equal("Info", r.LogLevel);
Assert.Equal(10, r.RebootDelaySeconds);
Assert.Equal(60, r.EnrollmentSummaryTimeoutSeconds);
Assert.Equal(120, r.EnrollmentSummaryLaunchRetrySeconds);
Assert.Equal(100, r.MaxBatchSize);
Assert.Equal("time.windows.com", r.NtpServer);
Assert.Equal("Off", r.DiagnosticsUploadMode);
Assert.False(r.DiagnosticsUploadEnabled);
Assert.False(r.UnrestrictedMode);
Assert.False(r.DeviceBlocked);
Assert.False(r.DeviceKillSignal);
Assert.Null(r.UnblockAt);
Assert.Null(r.MigrateToApiBaseUrl);
Assert.Equal(360, r.Collectors.AgentMaxLifetimeMinutes);
Assert.Equal(15, r.Collectors.CollectorIdleTimeoutMinutes);
Assert.Equal(10, r.Collectors.DesktopDetectorNoCandidateTimeoutMinutes);
Assert.Equal(new[] { 100, 1005, 1010 }, r.Collectors.ModernDeploymentHarmlessEventIds);
Assert.True(r.Analyzers.EnableLocalAdminAnalyzer);
Assert.Empty(r.Analyzers.LocalAdminAllowedAccounts);
Assert.False(r.Analyzers.EnableSoftwareInventoryAnalyzer);
Assert.True(r.Analyzers.EnableIntegrityBypassAnalyzer);
Assert.False(r.Analyzers.EnableRealmJoinWatcher);
Assert.False(r.Analyzers.KeepAwakeDuringUserEsp);
Assert.True(r.Analyzers.EnableConsoleBypassDetection);
}
[Theory]
[InlineData(null, "CustomerSas", "Off", false)]
[InlineData(null, "Hosted", "Off", true)] // hosted destination needs no SAS
[InlineData("https://acct.blob.core.windows.net/c?sv=1", "CustomerSas", "Off", true)] // SAS enables regardless of mode
public void Diagnostics_upload_enabled_follows_sas_or_hosted_not_the_mode(string? sas, string destination, string mode, bool expected)
{
var r = Build(Tenant(t =>
{
t.DiagnosticsBlobSasUrl = sas!;
t.DiagnosticsUploadDestination = destination;
t.DiagnosticsUploadMode = mode;
}));
Assert.Equal(expected, r.DiagnosticsUploadEnabled);
}
[Theory]
[InlineData("free", true, true, false)] // Community: entitlement missing
[InlineData("pro", false, true, false)] // GA gate off
[InlineData("pro", true, false, false)] // tenant toggle off
[InlineData("pro", true, true, true)]
public void Unrestricted_mode_needs_entitlement_and_gate_and_toggle(string planTier, bool gaGate, bool toggle, bool expected)
{
var r = Build(Tenant(t =>
{
t.PlanTier = planTier;
t.UnrestrictedModeEnabled = gaGate;
t.UnrestrictedMode = toggle;
}));
Assert.Equal(expected, r.UnrestrictedMode);
}
[Fact]
public void Operator_knobs_come_from_the_admin_configuration()
{
var admin = Admin(a =>
{
a.CollectorIdleTimeoutMinutes = 25;
a.DesktopDetectorNoCandidateTimeoutMinutes = 7;
a.AllowAgentDowngrade = true;
a.ModernDeploymentHarmlessEventIdsJson = "[100, 42]";
a.LatestAgentV2Sha256 = "zip-v2";
a.LatestAgentV2ExeSha256 = "exe-v2";
});
var r = Build(Tenant(), admin);
Assert.Equal(25, r.Collectors.CollectorIdleTimeoutMinutes);
Assert.Equal(7, r.Collectors.DesktopDetectorNoCandidateTimeoutMinutes);
Assert.True(r.AllowAgentDowngrade);
Assert.Equal(new[] { 100, 42 }, r.Collectors.ModernDeploymentHarmlessEventIds);
Assert.Equal("zip-v2", r.LatestAgentSha256);
Assert.Equal("exe-v2", r.LatestAgentExeSha256);
}
[Fact]
public void Legacy_major_gets_empty_hashes_the_current_major_gets_v2()
{
var admin = Admin(a => { a.LatestAgentV2Sha256 = "zip-v2"; a.LatestAgentV2ExeSha256 = "exe-v2"; });
var legacy = Build(Tenant(), admin, agentMajor: 1);
Assert.Equal(string.Empty, legacy.LatestAgentSha256);
var current = Build(Tenant(), admin, agentMajor: AgentConfigResolver.CurrentAgentMajor);
Assert.Equal("zip-v2", current.LatestAgentSha256);
}
[Fact]
public void Tenant_overrides_win_over_defaults()
{
var r = Build(Tenant(t =>
{
t.MaxBatchSize = 25;
t.LogLevel = "Debug";
t.NtpServer = "";
t.LocalAdminAllowedAccountsJson = "[\"Admin\",\"Helpdesk\"]";
t.AgentMaxLifetimeMinutes = 120;
t.EnableConsoleBypassDetection = false;
}));
Assert.Equal(25, r.MaxBatchSize);
Assert.Equal("Debug", r.LogLevel);
Assert.Equal("time.windows.com", r.NtpServer); // empty string falls back like null
Assert.Equal(new[] { "Admin", "Helpdesk" }, r.Analyzers.LocalAdminAllowedAccounts);
Assert.Equal(120, r.Collectors.AgentMaxLifetimeMinutes);
Assert.False(r.Analyzers.EnableConsoleBypassDetection);
}
[Fact]
public void Migration_target_is_served_when_valid_and_reported_when_rejected()
{
const string Us = "https://autopilotmonitor-api-us.azurewebsites.net";
var served = AgentConfigResolver.Build(Tenant(), Admin(a => a.AgentMigrateApiBaseUrl = Us),
new List<GatherRule>(), new List<ImeLogPattern>(), AgentConfigResolver.CurrentAgentMajor, Now);
Assert.Equal(Us, served.Response.MigrateToApiBaseUrl);
Assert.Null(served.RejectedMigrateCandidate);
var rejected = AgentConfigResolver.Build(Tenant(), Admin(a => a.AgentMigrateApiBaseUrl = "https://evil.example"),
new List<GatherRule>(), new List<ImeLogPattern>(), AgentConfigResolver.CurrentAgentMajor, Now);
Assert.Null(rejected.Response.MigrateToApiBaseUrl);
Assert.Equal("https://evil.example", rejected.RejectedMigrateCandidate);
}
[Fact]
public void Catalogs_pass_through_unchanged()
{
var rules = new List<GatherRule> { new() { RuleId = "GATHER-TEST-001" } };
var patterns = new List<ImeLogPattern> { new() { PatternId = "IME-TEST-001" } };
var r = AgentConfigResolver.Build(Tenant(), Admin(), rules, patterns, AgentConfigResolver.CurrentAgentMajor, Now).Response;
Assert.Same(rules, r.GatherRules);
Assert.Same(patterns, r.ImeLogPatterns);
}
}
@@ -1,4 +1,4 @@
using AutopilotMonitor.Functions.Functions.Config;
using AutopilotMonitor.Functions.Services;
using AutopilotMonitor.Shared.Models;
using AutopilotMonitor.Shared.Services;
using Xunit;
@@ -8,7 +8,7 @@ namespace AutopilotMonitor.Functions.Tests;
/// <summary>
/// Pins the endpoint-migration serving contract: the shared allowlist rules
/// (<see cref="AgentEndpointMigrationRules"/>, used verbatim by the agent too) and the
/// backend-side per-tenant resolution (<see cref="GetAgentConfigFunction.ResolveMigrateTarget"/> —
/// backend-side per-tenant resolution (<see cref="AgentConfigResolver.ResolveMigrateTarget"/> —
/// tenant override wins, empty override pins, invalid values are never served).
/// </summary>
public class AgentEndpointMigrationTests
@@ -75,7 +75,7 @@ public class AgentEndpointMigrationTests
{
var config = new AdminConfiguration();
Assert.Null(GetAgentConfigFunction.ResolveMigrateTarget(config, TenantA, out var rejected));
Assert.Null(AgentConfigResolver.ResolveMigrateTarget(config, TenantA, out var rejected));
Assert.Null(rejected);
}
@@ -84,8 +84,8 @@ public class AgentEndpointMigrationTests
{
var config = new AdminConfiguration { AgentMigrateApiBaseUrl = UsUrl };
Assert.Equal(UsUrl, GetAgentConfigFunction.ResolveMigrateTarget(config, TenantA, out _));
Assert.Equal(UsUrl, GetAgentConfigFunction.ResolveMigrateTarget(config, TenantB, out _));
Assert.Equal(UsUrl, AgentConfigResolver.ResolveMigrateTarget(config, TenantA, out _));
Assert.Equal(UsUrl, AgentConfigResolver.ResolveMigrateTarget(config, TenantB, out _));
}
[Fact]
@@ -97,8 +97,8 @@ public class AgentEndpointMigrationTests
AgentMigrateTenantOverridesJson = $"{{\"{TenantA}\": \"{EuUrl}\"}}"
};
Assert.Equal(EuUrl, GetAgentConfigFunction.ResolveMigrateTarget(config, TenantA, out _));
Assert.Equal(UsUrl, GetAgentConfigFunction.ResolveMigrateTarget(config, TenantB, out _));
Assert.Equal(EuUrl, AgentConfigResolver.ResolveMigrateTarget(config, TenantA, out _));
Assert.Equal(UsUrl, AgentConfigResolver.ResolveMigrateTarget(config, TenantB, out _));
}
[Fact]
@@ -110,9 +110,9 @@ public class AgentEndpointMigrationTests
AgentMigrateTenantOverridesJson = $"{{\"{TenantA}\": \"\"}}"
};
Assert.Null(GetAgentConfigFunction.ResolveMigrateTarget(config, TenantA, out var rejected));
Assert.Null(AgentConfigResolver.ResolveMigrateTarget(config, TenantA, out var rejected));
Assert.Null(rejected); // pinned, not rejected
Assert.Equal(UsUrl, GetAgentConfigFunction.ResolveMigrateTarget(config, TenantB, out _));
Assert.Equal(UsUrl, AgentConfigResolver.ResolveMigrateTarget(config, TenantB, out _));
}
[Fact]
@@ -123,7 +123,7 @@ public class AgentEndpointMigrationTests
AgentMigrateTenantOverridesJson = $"{{\"{TenantA.ToUpperInvariant()}\": \"{UsUrl}\"}}"
};
Assert.Equal(UsUrl, GetAgentConfigFunction.ResolveMigrateTarget(config, TenantA, out _));
Assert.Equal(UsUrl, AgentConfigResolver.ResolveMigrateTarget(config, TenantA, out _));
}
[Fact]
@@ -131,7 +131,7 @@ public class AgentEndpointMigrationTests
{
var config = new AdminConfiguration { AgentMigrateApiBaseUrl = "https://attacker.example.com" };
Assert.Null(GetAgentConfigFunction.ResolveMigrateTarget(config, TenantA, out var rejected));
Assert.Null(AgentConfigResolver.ResolveMigrateTarget(config, TenantA, out var rejected));
Assert.Equal("https://attacker.example.com", rejected);
}
@@ -144,7 +144,7 @@ public class AgentEndpointMigrationTests
AgentMigrateTenantOverridesJson = "{not json"
};
Assert.Equal(UsUrl, GetAgentConfigFunction.ResolveMigrateTarget(config, TenantA, out _));
Assert.Equal(UsUrl, AgentConfigResolver.ResolveMigrateTarget(config, TenantA, out _));
}
[Fact]
@@ -155,6 +155,6 @@ public class AgentEndpointMigrationTests
AgentMigrateApiBaseUrl = "https://AutopilotMonitor-API-US.azurewebsites.net/"
};
Assert.Equal(UsUrl, GetAgentConfigFunction.ResolveMigrateTarget(config, TenantA, out _));
Assert.Equal(UsUrl, AgentConfigResolver.ResolveMigrateTarget(config, TenantA, out _));
}
}
@@ -182,6 +182,7 @@ public class CrossTenantAccessTests
[Theory]
[InlineData("PATCH", "/api/config/{0}/plan")]
[InlineData("GET", "/api/config/{0}/effective-agent-config")]
[InlineData("POST", "/api/preview/whitelist/{0}")]
[InlineData("DELETE", "/api/preview/whitelist/{0}")]
[InlineData("POST", "/api/preview/send-welcome-email/{0}")]
@@ -1,10 +1,10 @@
using AutopilotMonitor.Functions.Functions.Config;
using AutopilotMonitor.Functions.Services;
using AutopilotMonitor.Shared.Models;
namespace AutopilotMonitor.Functions.Tests;
/// <summary>
/// Tests for <see cref="GetAgentConfigFunction.ParseAgentMajor"/> — the pure decision logic that picks
/// Tests for <see cref="AgentConfigResolver.ParseAgentMajor"/> — the pure decision logic that picks
/// the per-line hash-oracle field set based on the X-Agent-Version header value.
/// </summary>
public class GetAgentConfigFunctionTests
@@ -29,7 +29,7 @@ public class GetAgentConfigFunctionTests
[InlineData("vNEXT", 1)]
public void ParseAgentMajor_ExtractsMajor(string? agentVersion, int expectedMajor)
{
var actual = GetAgentConfigFunction.ParseAgentMajor(agentVersion);
var actual = AgentConfigResolver.ParseAgentMajor(agentVersion);
Assert.Equal(expectedMajor, actual);
}
@@ -49,7 +49,7 @@ public class GetAgentConfigFunctionTests
P(@"C:\Windows\SetupDiag\*.log", "tenant"),
};
var merged = GetAgentConfigFunction.MergeDiagnosticsLogPaths(global, tenant);
var merged = AgentConfigResolver.MergeDiagnosticsLogPaths(global, tenant);
Assert.Equal(new[]
{
@@ -63,7 +63,7 @@ public class GetAgentConfigFunctionTests
[Fact]
public void MergeDiagnosticsLogPaths_DropsBlankEntries()
{
var merged = GetAgentConfigFunction.MergeDiagnosticsLogPaths(
var merged = AgentConfigResolver.MergeDiagnosticsLogPaths(
new[] { P(""), P(" "), P(@"C:\Install\Log\*.log") },
new DiagnosticsLogPath[] { null!, P(@"C:\Install\Log\*.log") });
@@ -90,7 +90,7 @@ public class GetAgentConfigFunctionTests
public void ResolveDiagnosticsUploadEnabled_GatesOnSasOrHosted(
string? diagnosticsBlobSasUrl, string? destination, bool expected)
{
var actual = GetAgentConfigFunction.ResolveDiagnosticsUploadEnabled(diagnosticsBlobSasUrl, destination);
var actual = AgentConfigResolver.ResolveDiagnosticsUploadEnabled(diagnosticsBlobSasUrl, destination);
Assert.Equal(expected, actual);
}
}
@@ -9,7 +9,8 @@ namespace AutopilotMonitor.Functions.Tests;
/// is a new private parser copy — the state the 2026-09-04 API audit found six times over for
/// <c>days</c> alone, each with its own default, cap and out-of-range rule. The baseline lists
/// the sites that parse something other than a query parameter (a header, a stored cell, a
/// version string); it only shrinks.
/// version string); it only shrinks. The X-Agent-Version major parse moved to
/// <c>Services/AgentConfigResolver.cs</c> (outside the scanned roots) with D-216.
/// </summary>
public class QueryParamGuardTests
{
@@ -19,7 +20,6 @@ public class QueryParamGuardTests
/// <summary>Non-query parses, reviewed: file → why it is allowed.</summary>
private static readonly IReadOnlyDictionary<string, string> Baseline = new Dictionary<string, string>(StringComparer.Ordinal)
{
["Functions/Config/GetAgentConfigFunction.cs"] = "major of the X-Agent-Version header",
["Functions/Ingest/IngestTelemetryFunction.cs"] = "X-Send-Time-Utc header (RoundtripKind, deliberately not a query instant)",
["Functions/Raw/QueryRawEventsFunction.cs"] = "Severity / Sequence cells of raw table rows",
["Functions/Diagnostics/GetDiagnosticsUploadUrlFunction.cs"] = "se= expiry inside the tenant's stored SAS URL",
@@ -60,13 +60,26 @@ public sealed class SharedManifestParityTests
// v2: adds the "types" section (full wire-type graph) and drops v1's
// "sessionSummary" (it had no TS consumer; SessionSummary now rides in "types").
["schemaVersion"] = 2,
// "defaults" = the value of every wire field on a freshly constructed instance (the C#
// initializer): the web's "custom" highlight compares stored values against these
// instead of a hand-copied map. Nested objects appear as null — they have their own section.
["adminConfiguration"] = new Dictionary<string, object?>
{
["fields"] = WireFieldNames(typeof(AdminConfiguration)),
["defaults"] = WireDefaults(typeof(AdminConfiguration)),
},
["tenantConfiguration"] = new Dictionary<string, object?>
{
["fields"] = WireFieldNames(typeof(TenantConfiguration)),
["defaults"] = WireDefaults(typeof(TenantConfiguration)),
},
// What an agent receives (GET agent/config) at its class defaults — the runtime column of
// the Tenant Config Report highlights every effective value that differs from these.
["agentConfig"] = new Dictionary<string, object?>
{
["responseDefaults"] = WireDefaults(typeof(AgentConfigResponse)),
["collectorDefaults"] = WireDefaults(typeof(CollectorConfiguration)),
["analyzerDefaults"] = WireDefaults(typeof(AnalyzerConfiguration)),
},
// Enum member order is load-bearing (append-only ordinals) — declaration order kept.
["sessionStatuses"] = Enum.GetNames(typeof(SessionStatus)),
@@ -116,6 +129,29 @@ public sealed class SharedManifestParityTests
private static string[] WireFieldNames(Type type)
=> WireProperties(type).Select(p => JsonNamingPolicy.CamelCase.ConvertName(p.Name)).ToArray();
/// <summary>
/// Wire name → initializer value of a fresh instance. Scalars, strings, enums (by name),
/// DateTime (round-trip) and arrays/lists of those travel as-is; a nested object is null.
/// </summary>
private static Dictionary<string, object?> WireDefaults(Type type)
{
var instance = Activator.CreateInstance(type)!;
return WireProperties(type).ToDictionary(
p => JsonNamingPolicy.CamelCase.ConvertName(p.Name),
p => DefaultValue(p.GetValue(instance)));
}
private static object? DefaultValue(object? value) => value switch
{
null => null,
string s => s,
bool or int or long or double or float or decimal => value,
Enum e => e.ToString(),
DateTime d => d.ToString("O"),
System.Collections.IEnumerable list => list.Cast<object?>().Select(DefaultValue).ToArray(),
_ => null,
};
private static IEnumerable<PropertyInfo> WireProperties(Type type)
=> type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.GetMethod != null && p.GetIndexParameters().Length == 0);
@@ -20,8 +20,7 @@ namespace AutopilotMonitor.Functions.Functions.Config
private readonly ILogger<GetAgentConfigFunction> _logger;
private readonly TenantConfigurationService _configService;
private readonly AdminConfigurationService _adminConfigService;
private readonly GatherRuleService _gatherRuleService;
private readonly ImeLogPatternService _imeLogPatternService;
private readonly AgentConfigResolver _resolver;
private readonly RateLimitService _rateLimitService;
private readonly AutopilotDeviceValidator _autopilotDeviceValidator;
private readonly CorporateIdentifierValidator _corporateIdentifierValidator;
@@ -35,8 +34,7 @@ namespace AutopilotMonitor.Functions.Functions.Config
ILogger<GetAgentConfigFunction> logger,
TenantConfigurationService configService,
AdminConfigurationService adminConfigService,
GatherRuleService gatherRuleService,
ImeLogPatternService imeLogPatternService,
AgentConfigResolver resolver,
RateLimitService rateLimitService,
AutopilotDeviceValidator autopilotDeviceValidator,
CorporateIdentifierValidator corporateIdentifierValidator,
@@ -49,8 +47,7 @@ namespace AutopilotMonitor.Functions.Functions.Config
_logger = logger;
_configService = configService;
_adminConfigService = adminConfigService;
_gatherRuleService = gatherRuleService;
_imeLogPatternService = imeLogPatternService;
_resolver = resolver;
_rateLimitService = rateLimitService;
_autopilotDeviceValidator = autopilotDeviceValidator;
_corporateIdentifierValidator = corporateIdentifierValidator;
@@ -105,108 +102,8 @@ namespace AutopilotMonitor.Functions.Functions.Config
}
/// <summary>
/// Parses the major-version from an X-Agent-Version header value.
/// Accepts SemVer-ish strings like "2.0.114" or "2.0.114+abc123".
/// Missing/unparsable → returns 1 (backward-compat: very old agents may omit the
/// header). The V1 line is retired, so major 1 resolves to empty hashes via
/// GetAgentLine's default arm — legacy stragglers just skip their integrity check.
/// Deliberately NOT defaulting to 2: that would hand V2 hashes to V1 binaries and
/// could trigger the runtime_hash_mismatch force-update path against the wrong line.
/// </summary>
internal static int ParseAgentMajor(string? agentVersion)
{
if (string.IsNullOrWhiteSpace(agentVersion))
return 1;
var dot = agentVersion.IndexOf('.');
var majorStr = dot > 0 ? agentVersion.Substring(0, dot) : agentVersion;
return int.TryParse(majorStr, out var major) ? major : 1;
}
/// <summary>
/// Decides whether the agent should perform diagnostics uploads at all.
/// The CustomerSas destination is gated on a per-tenant SAS URL being present, but the
/// Hosted destination has no such URL (the platform owns the storage) — so gating purely
/// on the SAS URL silently disabled uploads for every Hosted-destination tenant. Enable
/// when either a customer SAS is configured OR the destination is Hosted.
/// </summary>
internal static bool ResolveDiagnosticsUploadEnabled(string? diagnosticsBlobSasUrl, string? destination)
{
return !string.IsNullOrEmpty(diagnosticsBlobSasUrl)
|| string.Equals(destination, "Hosted", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Global ∪ tenant diagnostics paths: global first, order preserved, blank paths dropped,
/// duplicates (trimmed, case-insensitive) collapsed onto the first occurrence. The agent
/// keys ZIP entries on the path, so a path present in both lists would otherwise produce
/// duplicate entry names inside the archive.
/// </summary>
internal static List<DiagnosticsLogPath> MergeDiagnosticsLogPaths(
IEnumerable<DiagnosticsLogPath> global,
IEnumerable<DiagnosticsLogPath> tenant)
{
return global.Concat(tenant)
.Where(p => !string.IsNullOrWhiteSpace(p?.Path))
.GroupBy(p => p.Path.Trim(), StringComparer.OrdinalIgnoreCase)
.Select(g => g.First())
.ToList();
}
/// <summary>
/// Returns the (ZipSha256, ExeSha256) pair appropriate for the calling agent.
/// Reads the X-Agent-Version header, parses the major, and dispatches to the
/// corresponding per-line field set on <see cref="AdminConfiguration"/> via
/// <see cref="AdminConfiguration.GetAgentLine(int)"/>. Future V3 = no change here;
/// add a switch arm in GetAgentLine and a field set on AdminConfiguration.
/// </summary>
internal static (string ZipSha256, string ExeSha256) SelectAgentHashesForClient(
HttpRequestData req,
AutopilotMonitor.Shared.Models.AdminConfiguration adminConfig)
{
var agentVersion = req.Headers.Contains("X-Agent-Version")
? req.Headers.GetValues("X-Agent-Version").FirstOrDefault()
: null;
var line = adminConfig.GetAgentLine(ParseAgentMajor(agentVersion));
return (line.ZipSha256, line.ExeSha256);
}
/// <summary>
/// Resolves the endpoint-migration target for a tenant: per-tenant override wins over
/// the global value; an override entry with an empty value pins the tenant (no
/// migration even while the global target is set — staged rollout). The winning value
/// is validated against <see cref="AutopilotMonitor.Shared.Services.AgentEndpointMigrationRules"/>;
/// invalid values resolve to null (fail-safe: better to strand an agent on the old
/// backend than to serve a broken or non-allowlisted URL).
/// </summary>
internal static string? ResolveMigrateTarget(
AutopilotMonitor.Shared.Models.AdminConfiguration adminConfig,
string tenantId,
out string? rejectedCandidate)
{
rejectedCandidate = null;
string? candidate = adminConfig.AgentMigrateApiBaseUrl;
var overrides = adminConfig.GetAgentMigrateTenantOverrides();
if (!string.IsNullOrEmpty(tenantId) && overrides.TryGetValue(tenantId, out var tenantTarget))
candidate = tenantTarget; // empty string = pinned, handled below
if (string.IsNullOrWhiteSpace(candidate))
return null;
if (AutopilotMonitor.Shared.Services.AgentEndpointMigrationRules
.TryNormalizeTarget(candidate, out var normalized))
{
return normalized;
}
rejectedCandidate = candidate;
return null;
}
/// <summary>
/// Core config logic: fetch tenant + admin config, gather rules, IME patterns.
/// Core config logic: kill verdict for this device, then the shared derivation
/// (<see cref="AgentConfigResolver"/>) with the device verdict applied on top.
/// Called by both the cert-auth Run() method and the bootstrap wrapper.
/// <paramref name="intuneDeviceId"/> is the certificate identity from the cert Subject CN
/// (cert-auth callers); the bootstrap wrapper has none and leaves it null.
@@ -236,114 +133,30 @@ namespace AutopilotMonitor.Functions.Functions.Config
intuneDeviceId: intuneDeviceId);
DeviceIdentityBinding.Stamp(req, killVerdict.IdentityBinding);
// Get tenant configuration
var tenantConfig = await _configService.GetConfigurationAsync(tenantId);
// Select the per-line hash oracle from the X-Agent-Version header (parametric per major).
var resolution = await _resolver.ResolveAsync(tenantId, AgentConfigResolver.ParseAgentMajor(agentVersionHeader));
var response = resolution.Response;
// Get global admin config for platform-wide policy settings
var adminConfig = await _adminConfigService.GetConfigurationAsync();
// Build collector configuration from tenant settings + global policy
var collectors = new CollectorConfiguration
{
EnablePerformanceCollector = tenantConfig.EnablePerformanceCollector,
PerformanceIntervalSeconds = tenantConfig.PerformanceCollectorIntervalSeconds,
CollectorIdleTimeoutMinutes = adminConfig.CollectorIdleTimeoutMinutes,
DesktopDetectorNoCandidateTimeoutMinutes = adminConfig.DesktopDetectorNoCandidateTimeoutMinutes,
HelloWaitTimeoutSeconds = tenantConfig.HelloWaitTimeoutSeconds,
AgentMaxLifetimeMinutes = tenantConfig.AgentMaxLifetimeMinutes ?? 360,
ModernDeploymentHarmlessEventIds = adminConfig.GetModernDeploymentHarmlessEventIds().ToArray()
};
// Active gather rules + active IME log patterns for this tenant. Independent reads,
// both served from per-instance catalog caches — fetch them concurrently.
var gatherRulesTask = _gatherRuleService.GetActiveRulesForTenantAsync(tenantId);
var imeLogPatternsTask = _imeLogPatternService.GetActivePatternsForTenantAsync(tenantId);
await Task.WhenAll(gatherRulesTask, imeLogPatternsTask);
var gatherRules = await gatherRulesTask;
var imeLogPatterns = await imeLogPatternsTask;
// Merge global + tenant-specific diagnostics log paths (the built-in sections are
// compiled into the agent — DiagnosticsBuiltInSections — and never travel here)
var diagLogPaths = MergeDiagnosticsLogPaths(
adminConfig.GetDiagnosticsGlobalLogPaths(), tenantConfig.GetDiagnosticsLogPaths());
// Select per-line hash oracle from the X-Agent-Version header (parametric per major).
// The wire response keeps generic field names (LatestAgentSha256 / LatestAgentExeSha256)
// so agent code is unchanged across all major lines.
var (latestAgentSha256, latestAgentExeSha256) = SelectAgentHashesForClient(req, adminConfig);
// Endpoint migration on the control channel: serve the (validated) re-home target
// so agents still bound to this backend's compiled-in URL move themselves to the
// new deployment at their next start. LogWarning (not Information) because worker
// logs below Warning never reach App Insights — this line is the delivery evidence
// during a migration window.
var migrateTarget = ResolveMigrateTarget(adminConfig, tenantId, out var rejectedMigrateCandidate);
if (migrateTarget != null)
// LogWarning (not Information) because worker logs below Warning never reach App
// Insights — this line is the delivery evidence during a migration window.
if (response.MigrateToApiBaseUrl != null)
{
_logger.LogWarning(
"AgentMigrateServed: tenant={TenantId} serial={Serial} agentVersion={AgentVersion} target={Target}",
tenantId, serialNumberHeader, agentVersionHeader, migrateTarget);
tenantId, serialNumberHeader, agentVersionHeader, response.MigrateToApiBaseUrl);
}
else if (rejectedMigrateCandidate != null)
else if (resolution.RejectedMigrateCandidate != null)
{
_logger.LogWarning(
"AgentMigrateRejected: configured migration target failed validation and is NOT served. tenant={TenantId} candidate={Candidate}",
tenantId, rejectedMigrateCandidate);
tenantId, resolution.RejectedMigrateCandidate);
}
var response = await req.OkAsync(new AgentConfigResponse
{
ConfigVersion = 40, // EnableDoGroupIdAutoSet (Delivery Optimization group ID from network fingerprint)
UploadIntervalSeconds = Shared.Constants.DefaultUploadIntervalSeconds,
SelfDestructOnComplete = tenantConfig.SelfDestructOnComplete ?? true,
KeepLogFile = tenantConfig.KeepLogFile ?? false,
EnableGeoLocation = tenantConfig.EnableGeoLocation ?? true,
EnableImeMatchLog = tenantConfig.EnableImeMatchLog ?? false,
EnableGatherRuleDebugLog = tenantConfig.EnableGatherRuleDebugLog ?? false,
EnableEspContinueAnywayObservation = tenantConfig.EnableEspContinueAnywayObservation ?? false,
MaxAuthFailures = tenantConfig.MaxAuthFailures ?? 5,
AuthFailureTimeoutMinutes = tenantConfig.AuthFailureTimeoutMinutes ?? 0,
LogLevel = tenantConfig.LogLevel ?? "Info",
RebootOnComplete = tenantConfig.RebootOnComplete ?? false,
RebootDelaySeconds = tenantConfig.RebootDelaySeconds ?? 10,
ShowEnrollmentSummary = tenantConfig.ShowEnrollmentSummary ?? false,
EnrollmentSummaryTimeoutSeconds = tenantConfig.EnrollmentSummaryTimeoutSeconds ?? 60,
EnrollmentSummaryBrandingImageUrl = tenantConfig.EnrollmentSummaryBrandingImageUrl,
EnrollmentSummaryLaunchRetrySeconds = tenantConfig.EnrollmentSummaryLaunchRetrySeconds ?? 120,
MaxBatchSize = tenantConfig.MaxBatchSize ?? 100,
DiagnosticsUploadEnabled = ResolveDiagnosticsUploadEnabled(
tenantConfig.DiagnosticsBlobSasUrl, tenantConfig.DiagnosticsUploadDestination),
DiagnosticsUploadMode = tenantConfig.DiagnosticsUploadMode ?? "Off",
DiagnosticsLogPaths = diagLogPaths,
Collectors = collectors,
Analyzers = new AnalyzerConfiguration
{
EnableLocalAdminAnalyzer = tenantConfig.EnableLocalAdminAnalyzer ?? true,
LocalAdminAllowedAccounts = tenantConfig.GetLocalAdminAllowedAccounts(),
EnableSoftwareInventoryAnalyzer = tenantConfig.EnableSoftwareInventoryAnalyzer ?? false,
EnableIntegrityBypassAnalyzer = tenantConfig.EnableIntegrityBypassAnalyzer ?? true,
EnableRealmJoinWatcher = tenantConfig.EnableRealmJoinWatcher ?? false,
KeepAwakeDuringUserEsp = tenantConfig.KeepAwakeDuringUserEsp ?? false,
EnableConsoleBypassDetection = tenantConfig.EnableConsoleBypassDetection ?? true
},
LatestAgentSha256 = latestAgentSha256,
LatestAgentExeSha256 = latestAgentExeSha256,
AllowAgentDowngrade = adminConfig.AllowAgentDowngrade,
NtpServer = string.IsNullOrEmpty(tenantConfig.NtpServer) ? "time.windows.com" : tenantConfig.NtpServer,
EnableTimezoneAutoSet = tenantConfig.EnableTimezoneAutoSet ?? false,
EnableDoGroupIdAutoSet = tenantConfig.EnableDoGroupIdAutoSet ?? false,
SendTraceEvents = tenantConfig.SendTraceEvents,
UnrestrictedMode = TenantEntitlementService.IsUnrestrictedModeActive(tenantConfig, DateTime.UtcNow),
GatherRules = gatherRules,
ImeLogPatterns = imeLogPatterns,
WhiteGloveSealingPatternIds = adminConfig.GetWhiteGloveSealingPatternIds(),
DeviceBlocked = killVerdict.IsBlocked,
DeviceKillSignal = killVerdict.IsKill,
UnblockAt = killVerdict.UnblockAt,
MigrateToApiBaseUrl = migrateTarget,
});
response.DeviceBlocked = killVerdict.IsBlocked;
response.DeviceKillSignal = killVerdict.IsKill;
response.UnblockAt = killVerdict.UnblockAt;
return response;
return await req.OkAsync(response);
}
}
}
@@ -0,0 +1,58 @@
using System;
using System.Threading.Tasks;
using AutopilotMonitor.Functions.Helpers;
using AutopilotMonitor.Functions.Services;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Config
{
/// <summary>
/// Global-Admin view of what an agent of the tenant receives from GET agent/config right
/// now: the same <see cref="AgentConfigResolver"/> derivation the agent channel uses, minus
/// the per-device kill verdict (no device here). Drives the runtime column of the Tenant
/// Config Report, so the report cannot drift from the backend rules. Read-only, no
/// side effects, no ops event.
/// </summary>
public class GetEffectiveAgentConfigFunction
{
private readonly ILogger<GetEffectiveAgentConfigFunction> _logger;
private readonly AgentConfigResolver _resolver;
public GetEffectiveAgentConfigFunction(
ILogger<GetEffectiveAgentConfigFunction> logger,
AgentConfigResolver resolver)
{
_logger = logger;
_resolver = resolver;
}
[Function("GetEffectiveAgentConfig")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "config/{tenantId}/effective-agent-config")] HttpRequestData req,
string tenantId)
{
try
{
// GlobalAdminOnly + RouteParam scoping enforced by PolicyEnforcementMiddleware.
var requestCtx = req.GetRequestContext();
// Optional ?agentVersion= selects the hash-oracle line like the agent's
// X-Agent-Version header would; absent → the current (V2) line, never the
// legacy fallback an absent header means on the agent channel.
var agentVersion = System.Web.HttpUtility.ParseQueryString(req.Url.Query)["agentVersion"];
var agentMajor = string.IsNullOrWhiteSpace(agentVersion)
? AgentConfigResolver.CurrentAgentMajor
: AgentConfigResolver.ParseAgentMajor(agentVersion);
var resolution = await _resolver.ResolveAsync(requestCtx.TargetTenantId, agentMajor);
return await req.OkAsync(resolution.Response);
}
catch (Exception ex)
{
return await req.InternalServerErrorAsync(_logger, ex, "GetEffectiveAgentConfig");
}
}
}
}
@@ -310,6 +310,8 @@ builder.Services.AddSingleton<BlockedDeviceService>();
builder.Services.AddSingleton<HardwareRejectionThrottleService>();
builder.Services.AddSingleton<BlockedVersionService>();
builder.Services.AddSingleton<KillSwitchEvaluator>();
// Shared agent-config derivation for the agent channel and the GA effective-config route.
builder.Services.AddSingleton<AgentConfigResolver>();
builder.Services.AddSingleton<BlobStorageService>();
builder.Services.AddSingleton<AutopilotMonitor.Functions.Services.Diagnostics.HostedDiagnosticsBlobService>();
builder.Services.AddSingleton<AutopilotMonitor.Functions.Services.Diagnostics.DiagnosticsBlobStreamer>();
@@ -458,6 +458,8 @@ public static class EndpointAccessPolicyCatalog
// config writes remain a future policy decision.
new("PATCH", "config/{tenantId}/fields", EndpointPolicy.TenantAdminOrGA, TenantScoping.RouteParam),
new("GET", "config/{tenantId}/backups", EndpointPolicy.GlobalAdminOnly, TenantScoping.RouteParam),
// Operator view of the AgentConfigResponse an agent of the tenant receives (Tenant Config Report).
new("GET", "config/{tenantId}/effective-agent-config", EndpointPolicy.GlobalAdminOnly, TenantScoping.RouteParam),
new("POST", "config/{tenantId}/revert", EndpointPolicy.GlobalAdminOnly, TenantScoping.RouteParam),
// Field schema for the write surface above: tenant-independent reflection over the model
// + deny-lists (no tenant data), but GA-only to match the surface it describes. Literal
@@ -0,0 +1,252 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AutopilotMonitor.Shared.Models;
namespace AutopilotMonitor.Functions.Services
{
/// <summary>
/// Outcome of <see cref="AgentConfigResolver.Build"/>: the response an agent of the tenant
/// receives, plus the migration candidate that failed validation (null when none) so the
/// agent channel can log it as delivery evidence.
/// </summary>
public sealed class AgentConfigResolution
{
public AgentConfigResponse Response { get; }
public string? RejectedMigrateCandidate { get; }
public AgentConfigResolution(AgentConfigResponse response, string? rejectedMigrateCandidate)
{
Response = response;
RejectedMigrateCandidate = rejectedMigrateCandidate;
}
}
/// <summary>
/// The one place that derives an <see cref="AgentConfigResponse"/> from a tenant's stored
/// configuration, the global admin configuration and the tenant's active rule catalogs.
/// Serves two callers with the same result: the cert-authenticated agent channel
/// (GetAgentConfigFunction, which adds the per-device kill verdict on top) and the
/// Global-Admin report route (GetEffectiveAgentConfigFunction), so the operator sees
/// exactly what an agent would receive. Every default applied here IS the agent-side
/// default; the class initializers of <see cref="AgentConfigResponse"/> are the source
/// of the "custom" highlight in the web report via the shared manifest.
/// </summary>
public class AgentConfigResolver
{
/// <summary>Bumped when the wire shape gains a field the agent must understand.</summary>
public const int ConfigVersion = 40; // EnableDoGroupIdAutoSet (Delivery Optimization group ID from network fingerprint)
/// <summary>
/// The agent line whose hash oracle is served when no X-Agent-Version is available
/// (operator report). V2 is the only wired line — see AdminConfiguration.GetAgentLine.
/// </summary>
public const int CurrentAgentMajor = 2;
private readonly TenantConfigurationService _configService;
private readonly AdminConfigurationService _adminConfigService;
private readonly GatherRuleService _gatherRuleService;
private readonly ImeLogPatternService _imeLogPatternService;
public AgentConfigResolver(
TenantConfigurationService configService,
AdminConfigurationService adminConfigService,
GatherRuleService gatherRuleService,
ImeLogPatternService imeLogPatternService)
{
_configService = configService;
_adminConfigService = adminConfigService;
_gatherRuleService = gatherRuleService;
_imeLogPatternService = imeLogPatternService;
}
/// <summary>
/// Loads the four inputs and builds the response. Rule and pattern reads are independent
/// and served from per-instance catalog caches — fetched concurrently.
/// </summary>
public async Task<AgentConfigResolution> ResolveAsync(string tenantId, int agentMajor)
{
var tenantConfig = await _configService.GetConfigurationAsync(tenantId);
var adminConfig = await _adminConfigService.GetConfigurationAsync();
var gatherRulesTask = _gatherRuleService.GetActiveRulesForTenantAsync(tenantId);
var imeLogPatternsTask = _imeLogPatternService.GetActivePatternsForTenantAsync(tenantId);
await Task.WhenAll(gatherRulesTask, imeLogPatternsTask);
return Build(tenantConfig, adminConfig, await gatherRulesTask, await imeLogPatternsTask, agentMajor, DateTime.UtcNow);
}
/// <summary>
/// Pure derivation: tenant config + admin config + catalogs → response. Device-specific
/// fields (DeviceBlocked, DeviceKillSignal, UnblockAt) stay at their defaults; the agent
/// channel sets them from its kill verdict.
/// </summary>
public static AgentConfigResolution Build(
TenantConfiguration tenantConfig,
AdminConfiguration adminConfig,
List<GatherRule> gatherRules,
List<ImeLogPattern> imeLogPatterns,
int agentMajor,
DateTime nowUtc)
{
// Collector configuration from tenant settings + global policy
var collectors = new CollectorConfiguration
{
EnablePerformanceCollector = tenantConfig.EnablePerformanceCollector,
PerformanceIntervalSeconds = tenantConfig.PerformanceCollectorIntervalSeconds,
CollectorIdleTimeoutMinutes = adminConfig.CollectorIdleTimeoutMinutes,
DesktopDetectorNoCandidateTimeoutMinutes = adminConfig.DesktopDetectorNoCandidateTimeoutMinutes,
HelloWaitTimeoutSeconds = tenantConfig.HelloWaitTimeoutSeconds,
AgentMaxLifetimeMinutes = tenantConfig.AgentMaxLifetimeMinutes ?? 360,
ModernDeploymentHarmlessEventIds = adminConfig.GetModernDeploymentHarmlessEventIds().ToArray()
};
// Merge global + tenant-specific diagnostics log paths (the built-in sections are
// compiled into the agent — DiagnosticsBuiltInSections — and never travel here)
var diagLogPaths = MergeDiagnosticsLogPaths(
adminConfig.GetDiagnosticsGlobalLogPaths(), tenantConfig.GetDiagnosticsLogPaths());
// Per-line hash oracle (parametric per major). The wire response keeps generic field
// names (LatestAgentSha256 / LatestAgentExeSha256) so agent code is unchanged across lines.
var line = adminConfig.GetAgentLine(agentMajor);
// Endpoint migration on the control channel: the (validated) re-home target, so agents
// still bound to this backend's compiled-in URL move themselves at their next start.
var migrateTarget = ResolveMigrateTarget(adminConfig, tenantConfig.TenantId, out var rejectedMigrateCandidate);
var response = new AgentConfigResponse
{
ConfigVersion = ConfigVersion,
UploadIntervalSeconds = Shared.Constants.DefaultUploadIntervalSeconds,
SelfDestructOnComplete = tenantConfig.SelfDestructOnComplete ?? true,
KeepLogFile = tenantConfig.KeepLogFile ?? false,
EnableGeoLocation = tenantConfig.EnableGeoLocation ?? true,
EnableImeMatchLog = tenantConfig.EnableImeMatchLog ?? false,
EnableGatherRuleDebugLog = tenantConfig.EnableGatherRuleDebugLog ?? false,
EnableEspContinueAnywayObservation = tenantConfig.EnableEspContinueAnywayObservation ?? false,
MaxAuthFailures = tenantConfig.MaxAuthFailures ?? 5,
AuthFailureTimeoutMinutes = tenantConfig.AuthFailureTimeoutMinutes ?? 0,
LogLevel = tenantConfig.LogLevel ?? "Info",
RebootOnComplete = tenantConfig.RebootOnComplete ?? false,
RebootDelaySeconds = tenantConfig.RebootDelaySeconds ?? 10,
ShowEnrollmentSummary = tenantConfig.ShowEnrollmentSummary ?? false,
EnrollmentSummaryTimeoutSeconds = tenantConfig.EnrollmentSummaryTimeoutSeconds ?? 60,
EnrollmentSummaryBrandingImageUrl = tenantConfig.EnrollmentSummaryBrandingImageUrl,
EnrollmentSummaryLaunchRetrySeconds = tenantConfig.EnrollmentSummaryLaunchRetrySeconds ?? 120,
MaxBatchSize = tenantConfig.MaxBatchSize ?? 100,
DiagnosticsUploadEnabled = ResolveDiagnosticsUploadEnabled(
tenantConfig.DiagnosticsBlobSasUrl, tenantConfig.DiagnosticsUploadDestination),
DiagnosticsUploadMode = tenantConfig.DiagnosticsUploadMode ?? "Off",
DiagnosticsLogPaths = diagLogPaths,
Collectors = collectors,
Analyzers = new AnalyzerConfiguration
{
EnableLocalAdminAnalyzer = tenantConfig.EnableLocalAdminAnalyzer ?? true,
LocalAdminAllowedAccounts = tenantConfig.GetLocalAdminAllowedAccounts(),
EnableSoftwareInventoryAnalyzer = tenantConfig.EnableSoftwareInventoryAnalyzer ?? false,
EnableIntegrityBypassAnalyzer = tenantConfig.EnableIntegrityBypassAnalyzer ?? true,
EnableRealmJoinWatcher = tenantConfig.EnableRealmJoinWatcher ?? false,
KeepAwakeDuringUserEsp = tenantConfig.KeepAwakeDuringUserEsp ?? false,
EnableConsoleBypassDetection = tenantConfig.EnableConsoleBypassDetection ?? true
},
LatestAgentSha256 = line.ZipSha256,
LatestAgentExeSha256 = line.ExeSha256,
AllowAgentDowngrade = adminConfig.AllowAgentDowngrade,
NtpServer = string.IsNullOrEmpty(tenantConfig.NtpServer) ? "time.windows.com" : tenantConfig.NtpServer,
EnableTimezoneAutoSet = tenantConfig.EnableTimezoneAutoSet ?? false,
EnableDoGroupIdAutoSet = tenantConfig.EnableDoGroupIdAutoSet ?? false,
SendTraceEvents = tenantConfig.SendTraceEvents,
UnrestrictedMode = TenantEntitlementService.IsUnrestrictedModeActive(tenantConfig, nowUtc),
GatherRules = gatherRules,
ImeLogPatterns = imeLogPatterns,
WhiteGloveSealingPatternIds = adminConfig.GetWhiteGloveSealingPatternIds(),
MigrateToApiBaseUrl = migrateTarget,
};
return new AgentConfigResolution(response, rejectedMigrateCandidate);
}
/// <summary>
/// Parses the major-version from an X-Agent-Version header value.
/// Accepts SemVer-ish strings like "2.0.114" or "2.0.114+abc123".
/// Missing/unparsable → returns 1 (backward-compat: very old agents may omit the
/// header). The V1 line is retired, so major 1 resolves to empty hashes via
/// GetAgentLine's default arm — legacy stragglers just skip their integrity check.
/// Deliberately NOT defaulting to 2: that would hand V2 hashes to V1 binaries and
/// could trigger the runtime_hash_mismatch force-update path against the wrong line.
/// </summary>
internal static int ParseAgentMajor(string? agentVersion)
{
if (string.IsNullOrWhiteSpace(agentVersion))
return 1;
var dot = agentVersion.IndexOf('.');
var majorStr = dot > 0 ? agentVersion.Substring(0, dot) : agentVersion;
return int.TryParse(majorStr, out var major) ? major : 1;
}
/// <summary>
/// Decides whether the agent should perform diagnostics uploads at all.
/// The CustomerSas destination is gated on a per-tenant SAS URL being present, but the
/// Hosted destination has no such URL (the platform owns the storage) — so gating purely
/// on the SAS URL silently disabled uploads for every Hosted-destination tenant. Enable
/// when either a customer SAS is configured OR the destination is Hosted.
/// </summary>
internal static bool ResolveDiagnosticsUploadEnabled(string? diagnosticsBlobSasUrl, string? destination)
{
return !string.IsNullOrEmpty(diagnosticsBlobSasUrl)
|| string.Equals(destination, "Hosted", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Global ∪ tenant diagnostics paths: global first, order preserved, blank paths dropped,
/// duplicates (trimmed, case-insensitive) collapsed onto the first occurrence. The agent
/// keys ZIP entries on the path, so a path present in both lists would otherwise produce
/// duplicate entry names inside the archive.
/// </summary>
internal static List<DiagnosticsLogPath> MergeDiagnosticsLogPaths(
IEnumerable<DiagnosticsLogPath> global,
IEnumerable<DiagnosticsLogPath> tenant)
{
return global.Concat(tenant)
.Where(p => !string.IsNullOrWhiteSpace(p?.Path))
.GroupBy(p => p.Path.Trim(), StringComparer.OrdinalIgnoreCase)
.Select(g => g.First())
.ToList();
}
/// <summary>
/// Resolves the endpoint-migration target for a tenant: per-tenant override wins over
/// the global value; an override entry with an empty value pins the tenant (no
/// migration even while the global target is set — staged rollout). The winning value
/// is validated against <see cref="AutopilotMonitor.Shared.Services.AgentEndpointMigrationRules"/>;
/// invalid values resolve to null (fail-safe: better to strand an agent on the old
/// backend than to serve a broken or non-allowlisted URL).
/// </summary>
internal static string? ResolveMigrateTarget(
AdminConfiguration adminConfig,
string tenantId,
out string? rejectedCandidate)
{
rejectedCandidate = null;
string? candidate = adminConfig.AgentMigrateApiBaseUrl;
var overrides = adminConfig.GetAgentMigrateTenantOverrides();
if (!string.IsNullOrEmpty(tenantId) && overrides.TryGetValue(tenantId, out var tenantTarget))
candidate = tenantTarget; // empty string = pinned, handled below
if (string.IsNullOrWhiteSpace(candidate))
return null;
if (AutopilotMonitor.Shared.Services.AgentEndpointMigrationRules
.TryNormalizeTarget(candidate, out var normalized))
{
return normalized;
}
rejectedCandidate = candidate;
return null;
}
}
}
@@ -1,5 +1,4 @@
using System;
using AutopilotMonitor.Functions.Functions.Config;
using AutopilotMonitor.Shared.Models;
namespace AutopilotMonitor.Functions.Services
@@ -33,7 +32,7 @@ namespace AutopilotMonitor.Functions.Services
{
if (config == null) return false;
return !string.Equals(config.DiagnosticsUploadMode ?? "Off", "Off", StringComparison.OrdinalIgnoreCase)
&& GetAgentConfigFunction.ResolveDiagnosticsUploadEnabled(
&& AgentConfigResolver.ResolveDiagnosticsUploadEnabled(
config.DiagnosticsBlobSasUrl, config.DiagnosticsUploadDestination);
}
@@ -449,10 +449,11 @@ namespace AutopilotMonitor.Shared.Models
public bool? EnableGeoLocation { get; set; }
/// <summary>
/// NTP server address for time check during enrollment.
/// null = use agent default ("time.windows.com").
/// NTP server address for time check during enrollment. The initializer is the same
/// value the table read maps a blank cell to and the agent falls back to, so a fresh
/// configuration, a stored one and the manifest default agree (D-216).
/// </summary>
public string NtpServer { get; set; } = default!;
public string NtpServer { get; set; } = "time.windows.com";
/// <summary>
/// Whether to automatically set the device timezone based on IP geolocation.
@@ -5,17 +5,16 @@ import { useAuth } from '../../../../contexts/AuthContext';
import { api } from '@/lib/api';
import { apiErrorText, fetchJson } from "@/lib/apiClient";
import type {
AdminConfiguration,
AgentConfigResponse,
TenantConfiguration,
TenantFeatureFlagsResponse,
} from "@/utils/wire-types.generated";
import {
isNonDefault,
RUNTIME_REPORT_SECTIONS,
RUNTIME_SECTIONS_COLLAPSED,
TENANT_REPORT_SECTIONS,
UNRESOLVED,
type RowKind,
type RuntimeContext,
type RuntimeSource,
} from './tenantConfigReportCatalog';
@@ -39,14 +38,13 @@ const WEBHOOK_PROVIDERS: Record<number, string> = {
const SOURCE_LABELS: Record<RuntimeSource, string> = {
tenant: 'tenant',
admin: 'global',
flags: 'entitlement',
constant: 'fixed',
global: 'global',
entitlement: 'entitlement',
fixed: 'fixed',
};
function formatValue(val: unknown): string {
if (val === null || val === undefined) return '—';
if (val === UNRESOLVED) return 'n/a (source not loaded)';
if (typeof val === 'boolean') return val ? 'Yes' : 'No';
if (typeof val === 'string') return val || '—';
if (Array.isArray(val)) return val.length ? val.join(', ') : '—';
@@ -80,6 +78,8 @@ function formatByKind(kind: RowKind | undefined, value: unknown): string {
return maskSasUrl(value as string);
case 'secret':
return value ? 'configured (hidden)' : '—';
case 'count':
return Array.isArray(value) ? String(value.length) : '—';
default:
return formatValue(value);
}
@@ -111,18 +111,35 @@ function ConfigRow({ label, display, highlight, sourceTag }: ConfigRowProps) {
interface SectionProps {
title: string;
/** Rendered as a closed <details> — for rows that are rarely relevant (agent class defaults). */
collapsed?: boolean;
children: React.ReactNode;
}
function Section({ title, children }: SectionProps) {
function Section({ title, collapsed, children }: SectionProps) {
const heading = <h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">{title}</h3>;
const table = (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<table className="w-full">
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">{children}</tbody>
</table>
</div>
);
if (collapsed) {
return (
<details className="mb-6 group">
<summary className="cursor-pointer list-none mb-2 flex items-center gap-2">
<span className="text-xs text-gray-400 dark:text-gray-500 group-open:rotate-90 transition-transform">▶</span>
{heading}
</summary>
{table}
</details>
);
}
return (
<div className="mb-6">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-2">{title}</h3>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<table className="w-full">
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">{children}</tbody>
</table>
</div>
<div className="mb-2">{heading}</div>
{table}
</div>
);
}
@@ -195,7 +212,7 @@ export function SectionTenantConfigReport() {
const [selectedTenantId, setSelectedTenantId] = useState('');
const [config, setConfig] = useState<TenantConfiguration | null>(null);
const [flags, setFlags] = useState<TenantFeatureFlagsResponse | null>(null);
const [adminConfig, setAdminConfig] = useState<AdminConfiguration | null>(null);
const [effective, setEffective] = useState<AgentConfigResponse | null>(null);
const [loading, setLoading] = useState(false);
const [loadingTenants, setLoadingTenants] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -223,34 +240,34 @@ export function SectionTenantConfigReport() {
fetchTenants();
}, [user?.isGlobalAdmin, getAccessToken]);
// Fetch config plus its two runtime sources for the selected tenant. The full config is
// the report; feature-flags (entitlement) and global config (operator knobs) are fail-soft —
// their rows show "n/a" and a notice instead of blocking the whole report.
// Fetch the stored config plus the two backend-resolved views of it. The stored config is
// the report; feature-flags (effective entitlement, header) and the effective agent config
// (runtime column) are fail-soft — a notice replaces them instead of blocking the page.
const fetchConfig = useCallback(async () => {
if (!selectedTenantId) return;
try {
setLoading(true);
setError(null);
setSideError(null);
const [configResult, flagsResult, adminResult] = await Promise.allSettled([
const [configResult, flagsResult, effectiveResult] = await Promise.allSettled([
fetchJson<TenantConfiguration>(api.config.tenant(selectedTenantId), getAccessToken),
fetchJson<TenantFeatureFlagsResponse>(api.config.featureFlags(selectedTenantId), getAccessToken),
fetchJson<AdminConfiguration>(api.globalConfig.get(), getAccessToken),
fetchJson<AgentConfigResponse>(api.config.effectiveAgentConfig(selectedTenantId), getAccessToken),
]);
if (configResult.status === 'rejected') throw configResult.reason;
setConfig(configResult.value);
setFlags(flagsResult.status === 'fulfilled' ? flagsResult.value : null);
setAdminConfig(adminResult.status === 'fulfilled' ? adminResult.value : null);
setEffective(effectiveResult.status === 'fulfilled' ? effectiveResult.value : null);
const sideFailures = [
flagsResult.status === 'rejected' ? `feature flags: ${apiErrorText(flagsResult.reason)}` : null,
adminResult.status === 'rejected' ? `global config: ${apiErrorText(adminResult.reason)}` : null,
effectiveResult.status === 'rejected' ? `effective agent config: ${apiErrorText(effectiveResult.reason)}` : null,
].filter((s): s is string => s !== null);
setSideError(sideFailures.length ? `Runtime sources unavailable — ${sideFailures.join('; ')}` : null);
setSideError(sideFailures.length ? `Backend-resolved views unavailable — ${sideFailures.join('; ')}` : null);
} catch (err) {
setError(apiErrorText(err));
setConfig(null);
setFlags(null);
setAdminConfig(null);
setEffective(null);
} finally {
setLoading(false);
}
@@ -263,8 +280,6 @@ export function SectionTenantConfigReport() {
void run();
}, [fetchConfig]);
const runtimeCtx: RuntimeContext | null = config ? { config, adminConfig, flags } : null;
return (
<div className="max-w-5xl mx-auto px-4 py-8">
{/* Header */}
@@ -322,7 +337,7 @@ export function SectionTenantConfigReport() {
)}
{/* Config report */}
{!loading && config && runtimeCtx && (
{!loading && config && (
<>
{/* Tenant header card */}
<div className="mb-6 p-4 bg-gradient-to-r from-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
@@ -405,14 +420,14 @@ export function SectionTenantConfigReport() {
{TENANT_REPORT_SECTIONS.map(({ section, rows }) => (
<Section key={section} title={section}>
{section === 'Webhooks' && <ChannelRows config={config} />}
{rows.map(({ key, row }) => {
const value = config[key as keyof TenantConfiguration];
{rows.map(({ key, row, default: def }) => {
const value = config[key];
return (
<ConfigRow
key={key}
label={row.label}
display={formatByKind(row.kind, value)}
highlight={'default' in row && isNonDefault(value, row.default)}
highlight={!row.informational && isNonDefault(value, def)}
/>
);
})}
@@ -424,21 +439,27 @@ export function SectionTenantConfigReport() {
<div>
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">Runtime Parameters (Agent)</h2>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
The effective values the agent receives at runtime, with defaults applied for unset fields.
The tag says where a value is resolved from: tenant config, global config, the tenant&apos;s entitlement, or a fixed backend constant.
What an agent of this tenant receives from the backend right now, resolved server-side.
The tag says which knob a value comes from: tenant config, global config, the tenant&apos;s entitlement, or a fixed agent default.
</p>
{RUNTIME_REPORT_SECTIONS.map(({ section, rows }) => (
<Section key={section} title={section}>
{rows.map(({ key, row }) => {
const value = row.value(runtimeCtx);
{!effective && (
<div className="text-sm text-gray-500 dark:text-gray-400 italic">
Effective agent configuration not available.
</div>
)}
{effective && RUNTIME_REPORT_SECTIONS.map(({ section, rows }) => (
<Section key={section} title={section} collapsed={RUNTIME_SECTIONS_COLLAPSED.has(section)}>
{rows.map(({ key, row, value: read, default: def }) => {
const value = read(effective);
return (
<ConfigRow
key={key}
label={row.label}
sourceTag={SOURCE_LABELS[row.source]}
display={formatValue(value)}
highlight={'default' in row && value !== UNRESOLVED && isNonDefault(value, row.default)}
display={formatByKind(row.kind, value)}
highlight={!row.informational && isNonDefault(value, def)}
/>
);
})}
@@ -2,28 +2,29 @@
* Parity pins for the Tenant Config Report catalog.
*
* The catalog is compile-time exhaustive over the generated wire types; this file adds the
* runtime half against SHARED_MANIFEST.tenantConfiguration.fields (the C#-reflected field
* list, independent of the TS type) and pins the runtime resolution rules that mirror
* GetAgentConfigFunction.cs with test-local fixtures.
* runtime half against the shared manifest (the C#-reflected field lists and initializer
* defaults, independent of the TS types): every field is catalogued, every catalogued field
* has a default to compare against, and the value accessors read the effective agent config
* the backend returns. The derivation rules themselves are pinned in the backend
* (AgentConfigResolverTests) — nothing is re-computed here.
*/
import { describe, expect, it } from "vitest";
import { SHARED_MANIFEST } from "@/utils/shared-manifests.generated";
import type { AdminConfiguration, TenantConfiguration, TenantFeatureFlagsResponse } from "@/utils/wire-types.generated";
import type { AgentConfigResponse } from "@/utils/wire-types.generated";
import {
ANALYZER_FIELDS,
COLLECTOR_FIELDS,
DEFAULT_UPLOAD_INTERVAL_SECONDS,
isExcluded,
isNonDefault,
RUNTIME_FIELDS,
RUNTIME_REPORT_SECTIONS,
RUNTIME_SECTIONS_COLLAPSED,
TENANT_FIELDS,
TENANT_REPORT_SECTIONS,
UNRESOLVED,
type RuntimeContext,
} from "../tenantConfigReportCatalog";
const manifestFields = [...SHARED_MANIFEST.tenantConfiguration.fields] as string[];
const manifestDefaults = SHARED_MANIFEST.tenantConfiguration.defaults as Record<string, unknown>;
describe("TENANT_FIELDS parity with the C# field manifest", () => {
it("catalogues every wire field of TenantConfiguration (rendered or excluded with a reason)", () => {
@@ -36,6 +37,11 @@ describe("TENANT_FIELDS parity with the C# field manifest", () => {
expect(stale).toEqual([]);
});
it("the manifest carries an initializer default for every field", () => {
const withoutDefault = manifestFields.filter((f) => !(f in manifestDefaults));
expect(withoutDefault).toEqual([]);
});
it("gives every exclusion a reason and every row a non-empty label", () => {
for (const [key, spec] of Object.entries(TENANT_FIELDS)) {
if (isExcluded(spec)) expect(spec.excluded, key).not.toBe("");
@@ -44,7 +50,7 @@ describe("TENANT_FIELDS parity with the C# field manifest", () => {
});
it("renders the fields the operator asked for by name", () => {
const rendered = new Set(TENANT_REPORT_SECTIONS.flatMap((s) => s.rows.map((r) => r.key)));
const rendered = new Set(TENANT_REPORT_SECTIONS.flatMap((s) => s.rows.map((r) => r.key as string)));
for (const key of [
"payingCustomer", "maxDelegatedTenantsOverride", "mcpUsagePlanOverride", "managedByProTenantId",
"trialConsumed", "proDowngradedUtc", "validateDeviceAssociation", "validateCloudPcDevice",
@@ -56,154 +62,94 @@ describe("TENANT_FIELDS parity with the C# field manifest", () => {
}
});
it("renders each field in exactly one section", () => {
it("renders each field in exactly one section, with its manifest default attached", () => {
const seen = new Map<string, number>();
for (const { rows } of TENANT_REPORT_SECTIONS) {
for (const { key } of rows) seen.set(key, (seen.get(key) ?? 0) + 1);
for (const { key, default: def } of rows) {
seen.set(key, (seen.get(key) ?? 0) + 1);
expect(def, key).toEqual(manifestDefaults[key]);
}
}
expect([...seen.entries()].filter(([, n]) => n !== 1)).toEqual([]);
});
it("secrets, masked values and timestamps are informational (never marked custom)", () => {
for (const [key, spec] of Object.entries(TENANT_FIELDS)) {
if (isExcluded(spec)) continue;
if (spec.kind === "secret" || spec.kind === "masked" || spec.kind === "date") {
expect(spec.informational, key).toBe(true);
}
}
});
it("pins a few manifest defaults the report relies on", () => {
expect(manifestDefaults.planTier).toBe("free");
expect(manifestDefaults.dataRetentionDays).toBe(90);
expect(manifestDefaults.diagnosticsUploadDestination).toBe("CustomerSas");
expect(manifestDefaults.payingCustomer).toBe(false);
// The one string default the table read also applies to a blank cell — a stored
// "time.windows.com" must not read as custom.
expect(manifestDefaults.ntpServer).toBe("time.windows.com");
});
});
// ── Runtime rules ────────────────────────────────────────────────────────────
// ── Runtime catalog ──────────────────────────────────────────────────────────
/** Minimal stored config: everything nullable unset, non-nullables at their C# initializers. */
function storedConfig(overrides: Partial<TenantConfiguration> = {}): TenantConfiguration {
return {
tenantId: "00000000-0000-0000-0000-000000000001",
domainName: "contoso.example",
lastUpdated: "2026-09-06T00:00:00Z",
updatedBy: "ga@operator.example",
disabled: false,
mcpDisabled: false,
planTier: "free",
trialConsumed: false,
payingCustomer: false,
manufacturerWhitelist: "Dell*,HP*,Lenovo*,Microsoft Corporation",
modelWhitelist: "*",
validateAutopilotDevice: false,
validateCorporateIdentifier: false,
validateDeviceAssociation: false,
validateCloudPcDevice: false,
validateIntuneDeviceBinding: false,
allowInsecureAgentRequests: false,
dataRetentionDays: 90,
sessionTimeoutHours: 5,
sessionGraceHours: 0,
maxNdjsonPayloadSizeMB: 5,
enablePerformanceCollector: true,
performanceCollectorIntervalSeconds: 30,
helloWaitTimeoutSeconds: 30,
ntpServer: "time.windows.com",
logLevel: "",
enrollmentSummaryBrandingImageUrl: "",
localAdminAllowedAccountsJson: "",
bootstrapTokenEnabled: false,
unrestrictedModeEnabled: false,
unrestrictedMode: false,
entraAppRolesEnabled: false,
diagnosticsLogPathsJson: "",
diagnosticsBlobSasUrl: "",
diagnosticsUploadMode: "Off",
diagnosticsUploadDestination: "CustomerSas",
sendTraceEvents: true,
teamsWebhookUrl: "",
teamsNotifyOnSuccess: true,
teamsNotifyOnFailure: true,
teamsNotifyOnStart: false,
webhookProviderType: 0,
webhookUrl: "",
webhookNotifyOnSuccess: true,
webhookNotifyOnFailure: true,
webhookNotifyOnHardwareRejection: false,
webhookNotifyOnStart: false,
webhookCustomHeadersJson: "",
notificationChannelsJson: "",
slaNotifyOnSuccessRateBreach: false,
slaNotifyOnDurationBreach: false,
slaNotifyOnAppInstallBreach: false,
slaNotifyOnConsecutiveFailures: false,
slaConsecutiveFailureThreshold: 5,
...overrides,
};
}
const agentDefaults = SHARED_MANIFEST.agentConfig;
const adminConfig = { collectorIdleTimeoutMinutes: 25, desktopDetectorNoCandidateTimeoutMinutes: 7, allowAgentDowngrade: true, modernDeploymentHarmlessEventIdsJson: "[100, 42]" } as AdminConfiguration;
const flags = (unrestrictedMode: boolean) => ({ unrestrictedMode }) as TenantFeatureFlagsResponse;
const ctx = (overrides: Partial<TenantConfiguration> = {}, extra: Partial<RuntimeContext> = {}): RuntimeContext => ({
config: storedConfig(overrides),
adminConfig: null,
flags: null,
...extra,
});
const runtimeValue = (record: typeof RUNTIME_FIELDS | typeof COLLECTOR_FIELDS | typeof ANALYZER_FIELDS, key: string, c: RuntimeContext) => {
const spec = (record as Record<string, (typeof RUNTIME_FIELDS)[keyof typeof RUNTIME_FIELDS]>)[key];
if (!spec || isExcluded(spec)) throw new Error(`${key} is not a rendered runtime row`);
return spec.value(c);
};
describe("runtime rows mirror GetAgentConfigFunction", () => {
it("upload interval is the backend constant (10), not the old 30", () => {
expect(DEFAULT_UPLOAD_INTERVAL_SECONDS).toBe(10);
expect(runtimeValue(RUNTIME_FIELDS, "uploadIntervalSeconds", ctx())).toBe(10);
});
it("diagnostics upload is enabled by a SAS URL or the hosted destination, regardless of mode", () => {
expect(runtimeValue(RUNTIME_FIELDS, "diagnosticsUploadEnabled", ctx())).toBe(false);
expect(runtimeValue(RUNTIME_FIELDS, "diagnosticsUploadEnabled", ctx({ diagnosticsUploadDestination: "Hosted" }))).toBe(true);
expect(runtimeValue(RUNTIME_FIELDS, "diagnosticsUploadEnabled", ctx({ diagnosticsBlobSasUrl: "https://x.blob.core.windows.net/c?sig=1", diagnosticsUploadMode: "Off" }))).toBe(true);
});
it("unrestricted mode comes from the entitlement flags, never from the stored toggle alone", () => {
expect(runtimeValue(RUNTIME_FIELDS, "unrestrictedMode", ctx({ unrestrictedModeEnabled: true, unrestrictedMode: true }))).toBe(UNRESOLVED);
expect(runtimeValue(RUNTIME_FIELDS, "unrestrictedMode", ctx({ unrestrictedModeEnabled: true, unrestrictedMode: true }, { flags: flags(false) }))).toBe(false);
expect(runtimeValue(RUNTIME_FIELDS, "unrestrictedMode", ctx({}, { flags: flags(true) }))).toBe(true);
});
it("operator knobs resolve from the global config and are unresolved without it", () => {
expect(runtimeValue(COLLECTOR_FIELDS, "collectorIdleTimeoutMinutes", ctx())).toBe(UNRESOLVED);
const c = ctx({}, { adminConfig });
expect(runtimeValue(COLLECTOR_FIELDS, "collectorIdleTimeoutMinutes", c)).toBe(25);
expect(runtimeValue(COLLECTOR_FIELDS, "desktopDetectorNoCandidateTimeoutMinutes", c)).toBe(7);
expect(runtimeValue(COLLECTOR_FIELDS, "modernDeploymentHarmlessEventIds", c)).toEqual([100, 42]);
expect(runtimeValue(RUNTIME_FIELDS, "allowAgentDowngrade", c)).toBe(true);
});
it("applies the analyzer defaults of the agent config builder", () => {
const c = ctx();
expect(runtimeValue(ANALYZER_FIELDS, "enableLocalAdminAnalyzer", c)).toBe(true);
expect(runtimeValue(ANALYZER_FIELDS, "enableIntegrityBypassAnalyzer", c)).toBe(true);
expect(runtimeValue(ANALYZER_FIELDS, "enableConsoleBypassDetection", c)).toBe(true);
expect(runtimeValue(ANALYZER_FIELDS, "enableSoftwareInventoryAnalyzer", c)).toBe(false);
expect(runtimeValue(ANALYZER_FIELDS, "enableRealmJoinWatcher", c)).toBe(false);
expect(runtimeValue(ANALYZER_FIELDS, "keepAwakeDuringUserEsp", c)).toBe(false);
expect(runtimeValue(ANALYZER_FIELDS, "localAdminAllowedAccounts", ctx({ localAdminAllowedAccountsJson: '["Admin","Helpdesk"]' }))).toEqual(["Admin", "Helpdesk"]);
expect(runtimeValue(ANALYZER_FIELDS, "localAdminAllowedAccounts", ctx({ localAdminAllowedAccountsJson: "not json" }))).toEqual([]);
});
it("nullable agent knobs fall back to the agent defaults", () => {
const c = ctx();
expect(runtimeValue(RUNTIME_FIELDS, "maxAuthFailures", c)).toBe(5);
expect(runtimeValue(RUNTIME_FIELDS, "authFailureTimeoutMinutes", c)).toBe(0);
expect(runtimeValue(RUNTIME_FIELDS, "logLevel", ctx({ logLevel: null as unknown as string }))).toBe("Info");
expect(runtimeValue(RUNTIME_FIELDS, "ntpServer", ctx({ ntpServer: "" }))).toBe("time.windows.com");
expect(runtimeValue(COLLECTOR_FIELDS, "agentMaxLifetimeMinutes", c)).toBe(360);
expect(runtimeValue(RUNTIME_FIELDS, "rebootDelaySeconds", ctx({ rebootDelaySeconds: 30 }))).toBe(30);
describe("runtime catalogs parity with the agent-config defaults manifest", () => {
it.each([
["responseDefaults", RUNTIME_FIELDS],
["collectorDefaults", COLLECTOR_FIELDS],
["analyzerDefaults", ANALYZER_FIELDS],
] as const)("%s ↔ catalog: same key set", (section, record) => {
const keys = Object.keys(agentDefaults[section]);
expect(keys.filter((k) => !(k in record))).toEqual([]);
expect(Object.keys(record).filter((k) => !keys.includes(k))).toEqual([]);
});
it("renders every runtime row in exactly one section", () => {
const keys = RUNTIME_REPORT_SECTIONS.flatMap((s) => s.rows.map((r) => r.key));
expect(new Set(keys).size).toBe(keys.length);
expect(keys).toContain("uploadIntervalSeconds");
expect(keys).toContain("collectorIdleTimeoutMinutes");
expect(keys).toContain("enableConsoleBypassDetection");
expect(keys).toContain("collectors.collectorIdleTimeoutMinutes");
expect(keys).toContain("analyzers.enableConsoleBypassDetection");
expect(keys).toContain("configVersion");
expect(keys).not.toContain("deviceKillSignal");
});
it("collapsed sections exist in the section list", () => {
const sections = new Set(RUNTIME_REPORT_SECTIONS.map((s) => s.section));
for (const s of RUNTIME_SECTIONS_COLLAPSED) expect(sections.has(s), s).toBe(true);
});
it("value accessors read the top level and the two sub-objects of the response", () => {
const response = {
uploadIntervalSeconds: 10,
collectors: { collectorIdleTimeoutMinutes: 25 },
analyzers: { enableConsoleBypassDetection: false },
diagnosticsLogPaths: [{ path: "a" }, { path: "b" }],
} as unknown as AgentConfigResponse;
const byKey = new Map(RUNTIME_REPORT_SECTIONS.flatMap((s) => s.rows.map((r) => [r.key, r] as const)));
expect(byKey.get("uploadIntervalSeconds")!.value(response)).toBe(10);
expect(byKey.get("collectors.collectorIdleTimeoutMinutes")!.value(response)).toBe(25);
expect(byKey.get("analyzers.enableConsoleBypassDetection")!.value(response)).toBe(false);
expect(byKey.get("diagnosticsLogPaths")!.row.kind).toBe("count");
expect(byKey.get("diagnosticsLogPaths")!.value(response)).toHaveLength(2);
});
it("pins the agent defaults the report highlights against", () => {
expect(agentDefaults.responseDefaults.uploadIntervalSeconds).toBe(10);
expect(agentDefaults.responseDefaults.maxBatchSize).toBe(100);
expect(agentDefaults.collectorDefaults.collectorIdleTimeoutMinutes).toBe(15);
expect(agentDefaults.analyzerDefaults.enableIntegrityBypassAnalyzer).toBe(true);
expect(agentDefaults.analyzerDefaults.enableRealmJoinWatcher).toBe(false);
});
});
describe("isNonDefault", () => {
it("treats null and undefined as one value and compares arrays by content", () => {
it("treats null, undefined and the empty string as one value and compares arrays by content", () => {
expect(isNonDefault(undefined, null)).toBe(false);
expect(isNonDefault("", null)).toBe(false);
expect(isNonDefault("", "Off")).toBe(true);
@@ -4,39 +4,56 @@
* Both halves of the report are exhaustive over the generated wire types: `TENANT_FIELDS`
* over every key of `TenantConfiguration` (GET config/{tenantId}) and `RUNTIME_FIELDS` /
* `COLLECTOR_FIELDS` / `ANALYZER_FIELDS` over every key of `AgentConfigResponse` and its two
* sub-objects (what GET agent/config serves). A key is either rendered (a row with section
* and label) or excluded with a reason — nothing can be silently forgotten: a new C# property
* forces the wire-type regeneration (SharedManifestParityTests) and `tsc` then fails here
* until the key is catalogued. The vitest next to this module pins the same thing at runtime
* against `SHARED_MANIFEST.tenantConfiguration.fields`.
* sub-objects (GET config/{tenantId}/effective-agent-config — the backend's own derivation,
* nothing is re-computed here). A key is either rendered (a row with section and label) or
* excluded with a reason — nothing can be silently forgotten: a new C# property forces the
* wire-type regeneration (SharedManifestParityTests) and `tsc` then fails here until the key
* is catalogued.
*
* Runtime rows mirror the resolution rules of GetAgentConfigFunction.cs (backend); every
* rule carries the source it reads from so the operator sees which knob to turn.
* The "custom" highlight compares a value against the C# initializer of the same property,
* which the shared manifest carries as `defaults` (generated by the same test) — no
* hand-copied default anywhere in this file.
*/
import { SHARED_MANIFEST } from "@/utils/shared-manifests.generated";
import type {
AdminConfiguration,
AgentConfigResponse,
AnalyzerConfiguration,
CollectorConfiguration,
TenantConfiguration,
TenantFeatureFlagsResponse,
} from "@/utils/wire-types.generated";
// ── Defaults from the manifest (compile-time complete over the wire types) ────
export const TENANT_DEFAULTS: Record<keyof TenantConfiguration, unknown> = SHARED_MANIFEST.tenantConfiguration.defaults;
export const RUNTIME_DEFAULTS: Record<keyof AgentConfigResponse, unknown> = SHARED_MANIFEST.agentConfig.responseDefaults;
export const COLLECTOR_DEFAULTS: Record<keyof CollectorConfiguration, unknown> = SHARED_MANIFEST.agentConfig.collectorDefaults;
export const ANALYZER_DEFAULTS: Record<keyof AnalyzerConfiguration, unknown> = SHARED_MANIFEST.agentConfig.analyzerDefaults;
// ── Row model ────────────────────────────────────────────────────────────────
/** How a value is rendered. `secret` never shows the value, only whether one is set. */
export type RowKind = "value" | "date" | "masked" | "secret";
/**
* How a value is rendered. `secret` never shows the value, only whether one is set;
* `count` shows the length of a list.
*/
export type RowKind = "value" | "date" | "masked" | "secret" | "count";
export interface TenantRow {
section: TenantSection;
interface RowBase {
label: string;
kind?: RowKind;
/**
* The C# initializer of TenantConfiguration.cs. A row WITH this key (even `null`) is
* compared against it and highlighted as "custom" when the stored value differs; a row
* without it is informational and never highlighted.
*/
default?: unknown;
/** Informational rows (reasons, timestamps, identities, secrets) are never marked "custom". */
informational?: true;
}
export interface TenantRow extends RowBase {
section: TenantSection;
}
/** Which knob a runtime value is resolved from (AgentConfigResolver.Build). */
export type RuntimeSource = "tenant" | "global" | "entitlement" | "fixed";
export interface RuntimeRow extends RowBase {
section: RuntimeSection;
source: RuntimeSource;
}
export interface Excluded {
@@ -65,8 +82,6 @@ export const TENANT_SECTIONS = [
] as const;
export type TenantSection = (typeof TENANT_SECTIONS)[number];
const NTP_DEFAULT = "time.windows.com";
// ── Tenant configuration (left column) ───────────────────────────────────────
const HEADER = { excluded: "shown in the tenant header card" } as const;
@@ -79,98 +94,97 @@ export const TENANT_FIELDS: Record<keyof TenantConfiguration, TenantRow | Exclud
updatedBy: HEADER,
// Tenant Status
disabled: { section: "Tenant Status", label: "Disabled", default: false },
disabledReason: { section: "Tenant Status", label: "Disabled Reason" },
disabledUntil: { section: "Tenant Status", label: "Disabled Until", kind: "date" },
mcpDisabled: { section: "Tenant Status", label: "MCP Disabled", default: false },
mcpDisabledReason: { section: "Tenant Status", label: "MCP Disabled Reason" },
onboardedAt: { section: "Tenant Status", label: "Onboarded At", kind: "date" },
onboardedBy: { section: "Tenant Status", label: "Onboarded By" },
contactEmail: { section: "Tenant Status", label: "Contact Email" },
companyName: { section: "Tenant Status", label: "Company" },
disabled: { section: "Tenant Status", label: "Disabled" },
disabledReason: { section: "Tenant Status", label: "Disabled Reason", informational: true },
disabledUntil: { section: "Tenant Status", label: "Disabled Until", kind: "date", informational: true },
mcpDisabled: { section: "Tenant Status", label: "MCP Disabled" },
mcpDisabledReason: { section: "Tenant Status", label: "MCP Disabled Reason", informational: true },
onboardedAt: { section: "Tenant Status", label: "Onboarded At", kind: "date", informational: true },
onboardedBy: { section: "Tenant Status", label: "Onboarded By", informational: true },
contactEmail: { section: "Tenant Status", label: "Contact Email", informational: true },
companyName: { section: "Tenant Status", label: "Company", informational: true },
// Plan & Billing (stored values; the effective edition comes from feature-flags, header card)
planTier: { section: "Plan & Billing", label: "Plan Tier (stored)", default: "free" },
trialExpiresUtc: { section: "Plan & Billing", label: "Trial Expires", kind: "date" },
trialStartedUtc: { section: "Plan & Billing", label: "Trial Started", kind: "date" },
trialGrantedBy: { section: "Plan & Billing", label: "Trial Granted By" },
trialConsumed: { section: "Plan & Billing", label: "Trial Consumed", default: false },
proDowngradedUtc: { section: "Plan & Billing", label: "Pro Downgraded At", kind: "date" },
payingCustomer: { section: "Plan & Billing", label: "Paying Customer", default: false },
maxDelegatedTenantsOverride: { section: "Plan & Billing", label: "Delegated Tenant Slots (override)", default: null },
mcpUsagePlanOverride: { section: "Plan & Billing", label: "MCP Usage Plan (override)", default: null },
managedByProTenantId: { section: "Plan & Billing", label: "Managed By Pro Tenant", default: null },
planTier: { section: "Plan & Billing", label: "Plan Tier (stored)" },
trialExpiresUtc: { section: "Plan & Billing", label: "Trial Expires", kind: "date", informational: true },
trialStartedUtc: { section: "Plan & Billing", label: "Trial Started", kind: "date", informational: true },
trialGrantedBy: { section: "Plan & Billing", label: "Trial Granted By", informational: true },
trialConsumed: { section: "Plan & Billing", label: "Trial Consumed" },
proDowngradedUtc: { section: "Plan & Billing", label: "Pro Downgraded At", kind: "date", informational: true },
payingCustomer: { section: "Plan & Billing", label: "Paying Customer" },
maxDelegatedTenantsOverride: { section: "Plan & Billing", label: "Delegated Tenant Slots (override)" },
mcpUsagePlanOverride: { section: "Plan & Billing", label: "MCP Usage Plan (override)" },
managedByProTenantId: { section: "Plan & Billing", label: "Managed By Pro Tenant" },
// Identity & App Homing
homedAppClientId: { section: "Identity & App Homing", label: "Homed App Client ID" },
lastAuthClientId: { section: "Identity & App Homing", label: "Last Auth Client ID" },
lastAuthClientIdSince: { section: "Identity & App Homing", label: "Last Auth Client ID Since", kind: "date" },
entraAppRolesEnabled: { section: "Identity & App Homing", label: "Entra App Roles Enabled", default: false },
homedAppClientId: { section: "Identity & App Homing", label: "Homed App Client ID", informational: true },
lastAuthClientId: { section: "Identity & App Homing", label: "Last Auth Client ID", informational: true },
lastAuthClientIdSince: { section: "Identity & App Homing", label: "Last Auth Client ID Since", kind: "date", informational: true },
entraAppRolesEnabled: { section: "Identity & App Homing", label: "Entra App Roles Enabled" },
// Security & Validation
validateAutopilotDevice: { section: "Security & Validation", label: "Validate Autopilot Device", default: false },
validateCorporateIdentifier: { section: "Security & Validation", label: "Validate Corporate Identifier", default: false },
validateDeviceAssociation: { section: "Security & Validation", label: "Validate Device Association", default: false },
validateCloudPcDevice: { section: "Security & Validation", label: "Validate Cloud PC Device", default: false },
validateIntuneDeviceBinding: { section: "Security & Validation", label: "Validate Intune Device Binding", default: false },
allowInsecureAgentRequests: { section: "Security & Validation", label: "Allow Insecure Agent Requests", default: false },
customRateLimitRequestsPerMinute: { section: "Security & Validation", label: "Device API Rate Limit Override", default: null },
customUserRateLimitRequestsPerMinute: { section: "Security & Validation", label: "MCP & Integrations API Rate Limit Override", default: null },
validateAutopilotDevice: { section: "Security & Validation", label: "Validate Autopilot Device" },
validateCorporateIdentifier: { section: "Security & Validation", label: "Validate Corporate Identifier" },
validateDeviceAssociation: { section: "Security & Validation", label: "Validate Device Association" },
validateCloudPcDevice: { section: "Security & Validation", label: "Validate Cloud PC Device" },
validateIntuneDeviceBinding: { section: "Security & Validation", label: "Validate Intune Device Binding" },
allowInsecureAgentRequests: { section: "Security & Validation", label: "Allow Insecure Agent Requests" },
customRateLimitRequestsPerMinute: { section: "Security & Validation", label: "Device API Rate Limit Override" },
customUserRateLimitRequestsPerMinute: { section: "Security & Validation", label: "MCP & Integrations API Rate Limit Override" },
// Hardware Whitelist
manufacturerWhitelist: { section: "Hardware Whitelist", label: "Manufacturers", default: "Dell*,HP*,Lenovo*,Microsoft Corporation" },
modelWhitelist: { section: "Hardware Whitelist", label: "Models", default: "*" },
webhookNotifyOnHardwareRejection: { section: "Hardware Whitelist", label: "Notify On Hardware Rejection", default: false },
manufacturerWhitelist: { section: "Hardware Whitelist", label: "Manufacturers" },
modelWhitelist: { section: "Hardware Whitelist", label: "Models" },
webhookNotifyOnHardwareRejection: { section: "Hardware Whitelist", label: "Notify On Hardware Rejection" },
// Data Management
dataRetentionDays: { section: "Data Management", label: "Data Retention (days)", default: 90 },
sessionTimeoutHours: { section: "Data Management", label: "Session Timeout (hours)", default: 5 },
sessionGraceHours: { section: "Data Management", label: "Session Grace (hours)", default: 0 },
absoluteMaxSessionHours: { section: "Data Management", label: "Absolute Max Session (hours)", default: null },
maxNdjsonPayloadSizeMB: { section: "Data Management", label: "Max NDJSON Payload (MB)", default: 5 },
dataRetentionDays: { section: "Data Management", label: "Data Retention (days)" },
sessionTimeoutHours: { section: "Data Management", label: "Session Timeout (hours)" },
sessionGraceHours: { section: "Data Management", label: "Session Grace (hours)" },
absoluteMaxSessionHours: { section: "Data Management", label: "Absolute Max Session (hours)" },
maxNdjsonPayloadSizeMB: { section: "Data Management", label: "Max NDJSON Payload (MB)" },
// Agent Collectors
enablePerformanceCollector: { section: "Agent Collectors", label: "Performance Collector", default: true },
performanceCollectorIntervalSeconds: { section: "Agent Collectors", label: "Perf. Interval (sec)", default: 30 },
helloWaitTimeoutSeconds: { section: "Agent Collectors", label: "Hello Wait Timeout (sec)", default: 30 },
enablePerformanceCollector: { section: "Agent Collectors", label: "Performance Collector" },
performanceCollectorIntervalSeconds: { section: "Agent Collectors", label: "Perf. Interval (sec)" },
helloWaitTimeoutSeconds: { section: "Agent Collectors", label: "Hello Wait Timeout (sec)" },
// Auth Circuit Breaker
maxAuthFailures: { section: "Auth Circuit Breaker", label: "Max Auth Failures", default: null },
authFailureTimeoutMinutes: { section: "Auth Circuit Breaker", label: "Auth Failure Timeout (min)", default: null },
agentMaxLifetimeMinutes: { section: "Auth Circuit Breaker", label: "Agent Max Lifetime (min)", default: null },
maxAuthFailures: { section: "Auth Circuit Breaker", label: "Max Auth Failures" },
authFailureTimeoutMinutes: { section: "Auth Circuit Breaker", label: "Auth Failure Timeout (min)" },
agentMaxLifetimeMinutes: { section: "Auth Circuit Breaker", label: "Agent Max Lifetime (min)" },
// Agent Behavior
selfDestructOnComplete: { section: "Agent Behavior", label: "Self-Destruct On Complete", default: true },
keepLogFile: { section: "Agent Behavior", label: "Keep Log File", default: false },
rebootOnComplete: { section: "Agent Behavior", label: "Reboot On Complete", default: null },
rebootDelaySeconds: { section: "Agent Behavior", label: "Reboot Delay (sec)", default: null },
enableGeoLocation: { section: "Agent Behavior", label: "Geo-Location", default: null },
enableTimezoneAutoSet: { section: "Agent Behavior", label: "Timezone Auto-Set", default: null },
enableDoGroupIdAutoSet: { section: "Agent Behavior", label: "DO GroupId Auto-Set", default: null },
// TableConfigRepository maps an empty stored value to the default, so the wire never carries null here.
ntpServer: { section: "Agent Behavior", label: "NTP Server", default: NTP_DEFAULT },
keepAwakeDuringUserEsp: { section: "Agent Behavior", label: "Keep Awake During User ESP", default: null },
enableImeMatchLog: { section: "Agent Behavior", label: "IME Match Log", default: null },
enableGatherRuleDebugLog: { section: "Agent Behavior", label: "Gather Rule Debug Log", default: null },
enableEspContinueAnywayObservation: { section: "Agent Behavior", label: "Continue-Anyway Observation", default: null },
logLevel: { section: "Agent Behavior", label: "Log Level", default: null },
maxBatchSize: { section: "Agent Behavior", label: "Max Batch Size", default: null },
showScriptOutput: { section: "Agent Behavior", label: "Show Script Output", default: true },
sendTraceEvents: { section: "Agent Behavior", label: "Send Trace Events", default: true },
selfDestructOnComplete: { section: "Agent Behavior", label: "Self-Destruct On Complete" },
keepLogFile: { section: "Agent Behavior", label: "Keep Log File" },
rebootOnComplete: { section: "Agent Behavior", label: "Reboot On Complete" },
rebootDelaySeconds: { section: "Agent Behavior", label: "Reboot Delay (sec)" },
enableGeoLocation: { section: "Agent Behavior", label: "Geo-Location" },
enableTimezoneAutoSet: { section: "Agent Behavior", label: "Timezone Auto-Set" },
enableDoGroupIdAutoSet: { section: "Agent Behavior", label: "DO GroupId Auto-Set" },
ntpServer: { section: "Agent Behavior", label: "NTP Server" },
keepAwakeDuringUserEsp: { section: "Agent Behavior", label: "Keep Awake During User ESP" },
enableImeMatchLog: { section: "Agent Behavior", label: "IME Match Log" },
enableGatherRuleDebugLog: { section: "Agent Behavior", label: "Gather Rule Debug Log" },
enableEspContinueAnywayObservation: { section: "Agent Behavior", label: "Continue-Anyway Observation" },
logLevel: { section: "Agent Behavior", label: "Log Level" },
maxBatchSize: { section: "Agent Behavior", label: "Max Batch Size" },
showScriptOutput: { section: "Agent Behavior", label: "Show Script Output" },
sendTraceEvents: { section: "Agent Behavior", label: "Send Trace Events" },
// Enrollment Summary
showEnrollmentSummary: { section: "Enrollment Summary", label: "Show Summary", default: null },
enrollmentSummaryTimeoutSeconds: { section: "Enrollment Summary", label: "Timeout (sec)", default: null },
enrollmentSummaryBrandingImageUrl: { section: "Enrollment Summary", label: "Branding Image URL", default: null },
enrollmentSummaryLaunchRetrySeconds: { section: "Enrollment Summary", label: "Launch Retry (sec)", default: null },
showEnrollmentSummary: { section: "Enrollment Summary", label: "Show Summary" },
enrollmentSummaryTimeoutSeconds: { section: "Enrollment Summary", label: "Timeout (sec)" },
enrollmentSummaryBrandingImageUrl: { section: "Enrollment Summary", label: "Branding Image URL" },
enrollmentSummaryLaunchRetrySeconds: { section: "Enrollment Summary", label: "Launch Retry (sec)" },
// Analyzers
enableLocalAdminAnalyzer: { section: "Analyzers", label: "Local Admin Analyzer", default: null },
localAdminAllowedAccountsJson: { section: "Analyzers", label: "Allowed Local Admin Accounts", default: null },
enableSoftwareInventoryAnalyzer: { section: "Analyzers", label: "Software Inventory Analyzer", default: null },
enableIntegrityBypassAnalyzer: { section: "Analyzers", label: "Integrity Bypass Analyzer", default: null },
enableConsoleBypassDetection: { section: "Analyzers", label: "Console Bypass Detection", default: null },
enableRealmJoinWatcher: { section: "Analyzers", label: "Realm Join Watcher", default: null },
enableLocalAdminAnalyzer: { section: "Analyzers", label: "Local Admin Analyzer" },
localAdminAllowedAccountsJson: { section: "Analyzers", label: "Allowed Local Admin Accounts" },
enableSoftwareInventoryAnalyzer: { section: "Analyzers", label: "Software Inventory Analyzer" },
enableIntegrityBypassAnalyzer: { section: "Analyzers", label: "Integrity Bypass Analyzer" },
enableConsoleBypassDetection: { section: "Analyzers", label: "Console Bypass Detection" },
enableRealmJoinWatcher: { section: "Analyzers", label: "Realm Join Watcher" },
// Webhooks — channel list plus the legacy single-webhook fields it falls back to
notificationChannelsJson: CHANNEL_BLOCK,
@@ -183,225 +197,193 @@ export const TENANT_FIELDS: Record<keyof TenantConfiguration, TenantRow | Exclud
teamsNotifyOnSuccess: CHANNEL_BLOCK,
teamsNotifyOnFailure: CHANNEL_BLOCK,
teamsNotifyOnStart: CHANNEL_BLOCK,
webhookCustomHeadersJson: { section: "Webhooks", label: "Custom Headers", kind: "secret" },
webhookCustomHeadersJson: { section: "Webhooks", label: "Custom Headers", kind: "secret", informational: true },
// SLA Targets
slaTargetSuccessRate: { section: "SLA Targets", label: "Target Success Rate (%)", default: null },
slaTargetMaxDurationMinutes: { section: "SLA Targets", label: "Target Max Duration (min)", default: null },
slaTargetAppInstallSuccessRate: { section: "SLA Targets", label: "Target App Install Success Rate (%)", default: null },
slaNotifyOnSuccessRateBreach: { section: "SLA Targets", label: "Notify On Success Rate Breach", default: false },
slaSuccessRateNotifyThreshold: { section: "SLA Targets", label: "Success Rate Notify Threshold (%)", default: null },
slaNotifyOnDurationBreach: { section: "SLA Targets", label: "Notify On Duration Breach", default: false },
slaNotifyOnAppInstallBreach: { section: "SLA Targets", label: "Notify On App Install Breach", default: false },
slaNotifyOnConsecutiveFailures: { section: "SLA Targets", label: "Notify On Consecutive Failures", default: false },
slaConsecutiveFailureThreshold: { section: "SLA Targets", label: "Consecutive Failure Threshold", default: 5 },
slaTargetSuccessRate: { section: "SLA Targets", label: "Target Success Rate (%)" },
slaTargetMaxDurationMinutes: { section: "SLA Targets", label: "Target Max Duration (min)" },
slaTargetAppInstallSuccessRate: { section: "SLA Targets", label: "Target App Install Success Rate (%)" },
slaNotifyOnSuccessRateBreach: { section: "SLA Targets", label: "Notify On Success Rate Breach" },
slaSuccessRateNotifyThreshold: { section: "SLA Targets", label: "Success Rate Notify Threshold (%)" },
slaNotifyOnDurationBreach: { section: "SLA Targets", label: "Notify On Duration Breach" },
slaNotifyOnAppInstallBreach: { section: "SLA Targets", label: "Notify On App Install Breach" },
slaNotifyOnConsecutiveFailures: { section: "SLA Targets", label: "Notify On Consecutive Failures" },
slaConsecutiveFailureThreshold: { section: "SLA Targets", label: "Consecutive Failure Threshold" },
// Diagnostics
diagnosticsUploadMode: { section: "Diagnostics", label: "Upload Mode", default: "Off" },
diagnosticsUploadDestination: { section: "Diagnostics", label: "Upload Destination", default: "CustomerSas" },
diagnosticsBlobSasUrl: { section: "Diagnostics", label: "Blob SAS URL", kind: "masked" },
diagnosticsLogPathsJson: { section: "Diagnostics", label: "Custom Log Paths", default: null },
diagnosticsUploadMode: { section: "Diagnostics", label: "Upload Mode" },
diagnosticsUploadDestination: { section: "Diagnostics", label: "Upload Destination" },
diagnosticsBlobSasUrl: { section: "Diagnostics", label: "Blob SAS URL", kind: "masked", informational: true },
diagnosticsLogPathsJson: { section: "Diagnostics", label: "Custom Log Paths" },
// Feature Flags (Global)
bootstrapTokenEnabled: { section: "Feature Flags (Global)", label: "Bootstrap Token Enabled", default: false },
unrestrictedModeEnabled: { section: "Feature Flags (Global)", label: "Unrestricted Mode Enabled (GA gate)", default: false },
unrestrictedMode: { section: "Feature Flags (Global)", label: "Unrestricted Mode (tenant toggle)", default: false },
bootstrapTokenEnabled: { section: "Feature Flags (Global)", label: "Bootstrap Token Enabled" },
unrestrictedModeEnabled: { section: "Feature Flags (Global)", label: "Unrestricted Mode Enabled (GA gate)" },
unrestrictedMode: { section: "Feature Flags (Global)", label: "Unrestricted Mode (tenant toggle)" },
};
// ── Runtime parameters (right column) ────────────────────────────────────────
export interface RuntimeContext {
config: TenantConfiguration;
/** GET global/config — null while loading or when the call failed. */
adminConfig: AdminConfiguration | null;
/** GET config/{tenantId}/feature-flags — backend-resolved entitlement surface. */
flags: TenantFeatureFlagsResponse | null;
}
/** Which knob a runtime value is resolved from. */
export type RuntimeSource = "tenant" | "admin" | "flags" | "constant";
export const RUNTIME_SECTIONS = [
"Upload & Batching",
"Agent Behavior",
"Auth Circuit Breaker",
"Enrollment Summary",
"Collectors",
"Collector Internals",
"Diagnostics",
"Analyzers",
"Catalogs & Integrity",
] as const;
export type RuntimeSection = (typeof RUNTIME_SECTIONS)[number];
/** Sentinel for a value that cannot be resolved because its source did not load. */
export const UNRESOLVED = Symbol("unresolved");
/** Sections rendered collapsed by default (agent class defaults, rarely relevant). */
export const RUNTIME_SECTIONS_COLLAPSED: ReadonlySet<RuntimeSection> = new Set<RuntimeSection>(["Collector Internals"]);
export interface RuntimeRow {
section: RuntimeSection;
label: string;
source: RuntimeSource;
/** Backend resolution rule (GetAgentConfigFunction.cs). */
value: (ctx: RuntimeContext) => unknown;
/** The agent-side default (AgentConfigResponse.cs / AdminConfiguration.cs initializer) for the "custom" highlight. */
default?: unknown;
}
/** Shared.Constants.DefaultUploadIntervalSeconds — the one runtime constant not on any config row. */
export const DEFAULT_UPLOAD_INTERVAL_SECONDS = 10;
/** GetAgentConfigFunction.ResolveDiagnosticsUploadEnabled: a SAS URL or the hosted destination, regardless of mode. */
export const resolveDiagnosticsUploadEnabled = (c: TenantConfiguration): boolean =>
!!c.diagnosticsBlobSasUrl || (c.diagnosticsUploadDestination ?? "").toLowerCase() === "hosted";
/** TenantConfiguration.GetLocalAdminAllowedAccounts: JSON string array, anything else → empty. */
export const parseLocalAdminAllowedAccounts = (json: string | null | undefined): string[] => {
if (!json) return [];
try {
const parsed: unknown = JSON.parse(json);
return Array.isArray(parsed) ? parsed.filter((x): x is string => typeof x === "string") : [];
} catch {
return [];
}
};
/** AdminConfiguration.GetModernDeploymentHarmlessEventIds: JSON int array, blank/invalid → defaults. */
export const parseModernDeploymentHarmlessEventIds = (json: string | null | undefined): number[] => {
const defaults = [100, 1005, 1010];
if (!json || !json.trim()) return defaults;
try {
const parsed: unknown = JSON.parse(json);
return Array.isArray(parsed) && parsed.every((x) => typeof x === "number") ? parsed : defaults;
} catch {
return defaults;
}
};
const admin = <T>(pick: (a: AdminConfiguration) => T) =>
(ctx: RuntimeContext): T | typeof UNRESOLVED => (ctx.adminConfig ? pick(ctx.adminConfig) : UNRESOLVED);
const NOT_CONFIG = { excluded: "per-request payload, not a configuration value" } as const;
const PER_DEVICE = { excluded: "per-device kill verdict, only on the agent channel" } as const;
const SUB_OBJECT = { excluded: "sub-object, catalogued separately" } as const;
const CLASS_DEFAULT = { excluded: "agent class default, neither tenant- nor operator-tunable" } as const;
/** Agent class default, neither tenant- nor operator-tunable — shown once, collapsed. */
const internal = (label: string): RuntimeRow => ({ section: "Collector Internals", label, source: "fixed" });
export const RUNTIME_FIELDS: Record<keyof AgentConfigResponse, RuntimeRow | Excluded> = {
configVersion: { excluded: "compiled into the backend" },
uploadIntervalSeconds: { section: "Upload & Batching", label: "Upload Interval (sec)", source: "constant", value: () => DEFAULT_UPLOAD_INTERVAL_SECONDS, default: DEFAULT_UPLOAD_INTERVAL_SECONDS },
maxBatchSize: { section: "Upload & Batching", label: "Max Batch Size", source: "tenant", value: ({ config }) => config.maxBatchSize ?? 100, default: 100 },
configVersion: { section: "Catalogs & Integrity", label: "Config Version", source: "fixed", informational: true },
uploadIntervalSeconds: { section: "Upload & Batching", label: "Upload Interval (sec)", source: "fixed" },
maxBatchSize: { section: "Upload & Batching", label: "Max Batch Size", source: "tenant" },
selfDestructOnComplete: { section: "Agent Behavior", label: "Self-Destruct", source: "tenant", value: ({ config }) => config.selfDestructOnComplete ?? true, default: true },
keepLogFile: { section: "Agent Behavior", label: "Keep Log File", source: "tenant", value: ({ config }) => config.keepLogFile ?? false, default: false },
rebootOnComplete: { section: "Agent Behavior", label: "Reboot On Complete", source: "tenant", value: ({ config }) => config.rebootOnComplete ?? false, default: false },
rebootDelaySeconds: { section: "Agent Behavior", label: "Reboot Delay (sec)", source: "tenant", value: ({ config }) => config.rebootDelaySeconds ?? 10, default: 10 },
enableGeoLocation: { section: "Agent Behavior", label: "Geo-Location", source: "tenant", value: ({ config }) => config.enableGeoLocation ?? true, default: true },
enableTimezoneAutoSet: { section: "Agent Behavior", label: "Timezone Auto-Set", source: "tenant", value: ({ config }) => config.enableTimezoneAutoSet ?? false, default: false },
enableDoGroupIdAutoSet: { section: "Agent Behavior", label: "DO GroupId Auto-Set", source: "tenant", value: ({ config }) => config.enableDoGroupIdAutoSet ?? false, default: false },
ntpServer: { section: "Agent Behavior", label: "NTP Server", source: "tenant", value: ({ config }) => config.ntpServer || NTP_DEFAULT, default: NTP_DEFAULT },
enableImeMatchLog: { section: "Agent Behavior", label: "IME Match Log", source: "tenant", value: ({ config }) => config.enableImeMatchLog ?? false, default: false },
enableGatherRuleDebugLog: { section: "Agent Behavior", label: "Gather Rule Debug Log", source: "tenant", value: ({ config }) => config.enableGatherRuleDebugLog ?? false, default: false },
enableEspContinueAnywayObservation: { section: "Agent Behavior", label: "Continue-Anyway Observation", source: "tenant", value: ({ config }) => config.enableEspContinueAnywayObservation ?? false, default: false },
logLevel: { section: "Agent Behavior", label: "Log Level", source: "tenant", value: ({ config }) => config.logLevel ?? "Info", default: "Info" },
sendTraceEvents: { section: "Agent Behavior", label: "Send Trace Events", source: "tenant", value: ({ config }) => config.sendTraceEvents, default: true },
// TenantEntitlementService.IsUnrestrictedModeActive = edition entitlement && GA gate && tenant toggle;
// feature-flags carries the backend-resolved value, so the entitlement logic is not rebuilt here.
unrestrictedMode: { section: "Agent Behavior", label: "Unrestricted Mode (effective)", source: "flags", value: ({ flags }) => (flags ? flags.unrestrictedMode : UNRESOLVED), default: false },
allowAgentDowngrade: { section: "Agent Behavior", label: "Allow Agent Downgrade", source: "admin", value: admin((a) => a.allowAgentDowngrade), default: false },
selfDestructOnComplete: { section: "Agent Behavior", label: "Self-Destruct", source: "tenant" },
keepLogFile: { section: "Agent Behavior", label: "Keep Log File", source: "tenant" },
rebootOnComplete: { section: "Agent Behavior", label: "Reboot On Complete", source: "tenant" },
rebootDelaySeconds: { section: "Agent Behavior", label: "Reboot Delay (sec)", source: "tenant" },
enableGeoLocation: { section: "Agent Behavior", label: "Geo-Location", source: "tenant" },
enableTimezoneAutoSet: { section: "Agent Behavior", label: "Timezone Auto-Set", source: "tenant" },
enableDoGroupIdAutoSet: { section: "Agent Behavior", label: "DO GroupId Auto-Set", source: "tenant" },
ntpServer: { section: "Agent Behavior", label: "NTP Server", source: "tenant" },
enableImeMatchLog: { section: "Agent Behavior", label: "IME Match Log", source: "tenant" },
enableGatherRuleDebugLog: { section: "Agent Behavior", label: "Gather Rule Debug Log", source: "tenant" },
enableEspContinueAnywayObservation: { section: "Agent Behavior", label: "Continue-Anyway Observation", source: "tenant" },
logLevel: { section: "Agent Behavior", label: "Log Level", source: "tenant" },
sendTraceEvents: { section: "Agent Behavior", label: "Send Trace Events", source: "tenant" },
// TenantEntitlementService.IsUnrestrictedModeActive = edition entitlement && GA gate && tenant toggle.
unrestrictedMode: { section: "Agent Behavior", label: "Unrestricted Mode (effective)", source: "entitlement" },
allowAgentDowngrade: { section: "Agent Behavior", label: "Allow Agent Downgrade", source: "global" },
maxAuthFailures: { section: "Auth Circuit Breaker", label: "Max Auth Failures", source: "tenant", value: ({ config }) => config.maxAuthFailures ?? 5, default: 5 },
authFailureTimeoutMinutes: { section: "Auth Circuit Breaker", label: "Auth Failure Timeout (min)", source: "tenant", value: ({ config }) => config.authFailureTimeoutMinutes ?? 0, default: 0 },
maxAuthFailures: { section: "Auth Circuit Breaker", label: "Max Auth Failures", source: "tenant" },
authFailureTimeoutMinutes: { section: "Auth Circuit Breaker", label: "Auth Failure Timeout (min)", source: "tenant" },
showEnrollmentSummary: { section: "Enrollment Summary", label: "Show Summary", source: "tenant", value: ({ config }) => config.showEnrollmentSummary ?? false, default: false },
enrollmentSummaryTimeoutSeconds: { section: "Enrollment Summary", label: "Timeout (sec)", source: "tenant", value: ({ config }) => config.enrollmentSummaryTimeoutSeconds ?? 60, default: 60 },
enrollmentSummaryBrandingImageUrl: { section: "Enrollment Summary", label: "Branding Image URL", source: "tenant", value: ({ config }) => config.enrollmentSummaryBrandingImageUrl ?? null },
enrollmentSummaryLaunchRetrySeconds: { section: "Enrollment Summary", label: "Launch Retry (sec)", source: "tenant", value: ({ config }) => config.enrollmentSummaryLaunchRetrySeconds ?? 120, default: 120 },
showEnrollmentSummary: { section: "Enrollment Summary", label: "Show Summary", source: "tenant" },
enrollmentSummaryTimeoutSeconds: { section: "Enrollment Summary", label: "Timeout (sec)", source: "tenant" },
enrollmentSummaryBrandingImageUrl: { section: "Enrollment Summary", label: "Branding Image URL", source: "tenant" },
enrollmentSummaryLaunchRetrySeconds: { section: "Enrollment Summary", label: "Launch Retry (sec)", source: "tenant" },
diagnosticsUploadEnabled: { section: "Diagnostics", label: "Upload Enabled", source: "tenant", value: ({ config }) => resolveDiagnosticsUploadEnabled(config), default: false },
diagnosticsUploadMode: { section: "Diagnostics", label: "Upload Mode", source: "tenant", value: ({ config }) => config.diagnosticsUploadMode ?? "Off", default: "Off" },
diagnosticsLogPaths: { excluded: "merged global + tenant path list (see Custom Log Paths and the global diagnostics settings)" },
diagnosticsUploadEnabled: { section: "Diagnostics", label: "Upload Enabled", source: "tenant" },
diagnosticsUploadMode: { section: "Diagnostics", label: "Upload Mode", source: "tenant" },
diagnosticsLogPaths: { section: "Diagnostics", label: "Log Paths (global + tenant)", source: "global", kind: "count" },
collectors: SUB_OBJECT,
analyzers: SUB_OBJECT,
gatherRules: { excluded: "active gather-rule catalog for the tenant" },
imeLogPatterns: { excluded: "active IME log-pattern catalog for the tenant" },
whiteGloveSealingPatternIds: { excluded: "global pattern-id list (AdminConfiguration)" },
latestAgentSha256: { excluded: "per-major agent hash oracle selected from the caller's X-Agent-Version (AdminConfiguration)" },
latestAgentExeSha256: { excluded: "per-major agent hash oracle selected from the caller's X-Agent-Version (AdminConfiguration)" },
deviceBlocked: NOT_CONFIG,
deviceKillSignal: NOT_CONFIG,
unblockAt: NOT_CONFIG,
migrateToApiBaseUrl: { excluded: "endpoint-migration target, global or per-tenant override (AdminConfiguration)" },
gatherRules: { section: "Catalogs & Integrity", label: "Active Gather Rules", source: "tenant", kind: "count", informational: true },
imeLogPatterns: { section: "Catalogs & Integrity", label: "Active IME Log Patterns", source: "tenant", kind: "count", informational: true },
whiteGloveSealingPatternIds: { section: "Catalogs & Integrity", label: "White Glove Sealing Pattern IDs", source: "global", informational: true },
latestAgentSha256: { section: "Catalogs & Integrity", label: "Latest Agent ZIP SHA-256", source: "global", informational: true },
latestAgentExeSha256: { section: "Catalogs & Integrity", label: "Latest Agent EXE SHA-256", source: "global", informational: true },
migrateToApiBaseUrl: { section: "Catalogs & Integrity", label: "Migrate To API Base URL", source: "global" },
deviceBlocked: PER_DEVICE,
deviceKillSignal: PER_DEVICE,
unblockAt: PER_DEVICE,
};
export const COLLECTOR_FIELDS: Record<keyof CollectorConfiguration, RuntimeRow | Excluded> = {
enablePerformanceCollector: { section: "Collectors", label: "Performance Collector", source: "tenant", value: ({ config }) => config.enablePerformanceCollector, default: true },
performanceIntervalSeconds: { section: "Collectors", label: "Perf. Interval (sec)", source: "tenant", value: ({ config }) => config.performanceCollectorIntervalSeconds, default: 30 },
collectorIdleTimeoutMinutes: { section: "Collectors", label: "Idle Timeout (min)", source: "admin", value: admin((a) => a.collectorIdleTimeoutMinutes), default: 15 },
desktopDetectorNoCandidateTimeoutMinutes: { section: "Collectors", label: "Desktop Detector No-Candidate Timeout (min)", source: "admin", value: admin((a) => a.desktopDetectorNoCandidateTimeoutMinutes), default: 10 },
enableAgentSelfMetrics: { section: "Collectors", label: "Agent Self-Metrics", source: "constant", value: () => true, default: true },
agentSelfMetricsIntervalSeconds: { section: "Collectors", label: "Self-Metrics Interval (sec)", source: "constant", value: () => 60, default: 60 },
helloWaitTimeoutSeconds: { section: "Collectors", label: "Hello Wait Timeout (sec)", source: "tenant", value: ({ config }) => config.helloWaitTimeoutSeconds, default: 30 },
agentMaxLifetimeMinutes: { section: "Collectors", label: "Agent Max Lifetime (min)", source: "tenant", value: ({ config }) => config.agentMaxLifetimeMinutes ?? 360, default: 360 },
modernDeploymentHarmlessEventIds: { section: "Collectors", label: "Modern Deployment Harmless Event IDs", source: "admin", value: admin((a) => parseModernDeploymentHarmlessEventIds(a.modernDeploymentHarmlessEventIdsJson)), default: [100, 1005, 1010] },
enableDeliveryOptimizationCollector: CLASS_DEFAULT,
deliveryOptimizationIntervalSeconds: CLASS_DEFAULT,
enableOfficeInstallDetector: CLASS_DEFAULT,
officeInstallSettleSeconds: CLASS_DEFAULT,
enablePowerStateWatcher: CLASS_DEFAULT,
stallProbeEnabled: CLASS_DEFAULT,
stallProbeThresholdsMinutes: CLASS_DEFAULT,
stallProbeTraceIndices: CLASS_DEFAULT,
stallProbeSources: CLASS_DEFAULT,
sessionStalledAfterProbeIndex: CLASS_DEFAULT,
modernDeploymentWatcherEnabled: CLASS_DEFAULT,
modernDeploymentLogLevelMax: CLASS_DEFAULT,
modernDeploymentBackfillEnabled: CLASS_DEFAULT,
modernDeploymentBackfillLookbackMinutes: CLASS_DEFAULT,
windowsUpdateWatcherEnabled: CLASS_DEFAULT,
windowsUpdateTargetedEventIds: CLASS_DEFAULT,
windowsUpdateBackfillLookbackMinutes: CLASS_DEFAULT,
windowsUpdateChannelCensusEnabled: CLASS_DEFAULT,
mdmRebootPolicyWatcherEnabled: CLASS_DEFAULT,
mdmRebootPolicyBackfillLookbackMinutes: CLASS_DEFAULT,
enableSystemTimelineWatcher: CLASS_DEFAULT,
systemEventBackfillLookbackMinutes: CLASS_DEFAULT,
enablePerformanceCollector: { section: "Collectors", label: "Performance Collector", source: "tenant" },
performanceIntervalSeconds: { section: "Collectors", label: "Perf. Interval (sec)", source: "tenant" },
collectorIdleTimeoutMinutes: { section: "Collectors", label: "Idle Timeout (min)", source: "global" },
desktopDetectorNoCandidateTimeoutMinutes: { section: "Collectors", label: "Desktop Detector No-Candidate Timeout (min)", source: "global" },
enableAgentSelfMetrics: { section: "Collectors", label: "Agent Self-Metrics", source: "fixed" },
agentSelfMetricsIntervalSeconds: { section: "Collectors", label: "Self-Metrics Interval (sec)", source: "fixed" },
helloWaitTimeoutSeconds: { section: "Collectors", label: "Hello Wait Timeout (sec)", source: "tenant" },
agentMaxLifetimeMinutes: { section: "Collectors", label: "Agent Max Lifetime (min)", source: "tenant" },
modernDeploymentHarmlessEventIds: { section: "Collectors", label: "Modern Deployment Harmless Event IDs", source: "global" },
enableDeliveryOptimizationCollector: internal("Delivery Optimization Collector"),
deliveryOptimizationIntervalSeconds: internal("DO Interval (sec)"),
enableOfficeInstallDetector: internal("Office Install Detector"),
officeInstallSettleSeconds: internal("Office Install Settle (sec)"),
enablePowerStateWatcher: internal("Power State Watcher"),
stallProbeEnabled: internal("Stall Probe"),
stallProbeThresholdsMinutes: internal("Stall Probe Thresholds (min)"),
stallProbeTraceIndices: internal("Stall Probe Trace Indices"),
stallProbeSources: internal("Stall Probe Sources"),
sessionStalledAfterProbeIndex: internal("Session Stalled After Probe Index"),
modernDeploymentWatcherEnabled: internal("Modern Deployment Watcher"),
modernDeploymentLogLevelMax: internal("Modern Deployment Log Level Max"),
modernDeploymentBackfillEnabled: internal("Modern Deployment Backfill"),
modernDeploymentBackfillLookbackMinutes: internal("Modern Deployment Backfill Lookback (min)"),
windowsUpdateWatcherEnabled: internal("Windows Update Watcher"),
windowsUpdateTargetedEventIds: internal("Windows Update Targeted Event IDs"),
windowsUpdateBackfillLookbackMinutes: internal("Windows Update Backfill Lookback (min)"),
windowsUpdateChannelCensusEnabled: internal("Windows Update Channel Census"),
mdmRebootPolicyWatcherEnabled: internal("MDM Reboot Policy Watcher"),
mdmRebootPolicyBackfillLookbackMinutes: internal("MDM Reboot Policy Backfill Lookback (min)"),
enableSystemTimelineWatcher: internal("System Timeline Watcher"),
systemEventBackfillLookbackMinutes: internal("System Event Backfill Lookback (min)"),
};
export const ANALYZER_FIELDS: Record<keyof AnalyzerConfiguration, RuntimeRow | Excluded> = {
enableLocalAdminAnalyzer: { section: "Analyzers", label: "Local Admin Analyzer", source: "tenant", value: ({ config }) => config.enableLocalAdminAnalyzer ?? true, default: true },
localAdminAllowedAccounts: { section: "Analyzers", label: "Allowed Local Admin Accounts", source: "tenant", value: ({ config }) => parseLocalAdminAllowedAccounts(config.localAdminAllowedAccountsJson), default: [] },
enableSoftwareInventoryAnalyzer: { section: "Analyzers", label: "Software Inventory Analyzer", source: "tenant", value: ({ config }) => config.enableSoftwareInventoryAnalyzer ?? false, default: false },
enableIntegrityBypassAnalyzer: { section: "Analyzers", label: "Integrity Bypass Analyzer", source: "tenant", value: ({ config }) => config.enableIntegrityBypassAnalyzer ?? true, default: true },
enableRealmJoinWatcher: { section: "Analyzers", label: "Realm Join Watcher", source: "tenant", value: ({ config }) => config.enableRealmJoinWatcher ?? false, default: false },
keepAwakeDuringUserEsp: { section: "Analyzers", label: "Keep Awake During User ESP", source: "tenant", value: ({ config }) => config.keepAwakeDuringUserEsp ?? false, default: false },
enableConsoleBypassDetection: { section: "Analyzers", label: "Console Bypass Detection", source: "tenant", value: ({ config }) => config.enableConsoleBypassDetection ?? true, default: true },
enableLocalAdminAnalyzer: { section: "Analyzers", label: "Local Admin Analyzer", source: "tenant" },
localAdminAllowedAccounts: { section: "Analyzers", label: "Allowed Local Admin Accounts", source: "tenant" },
enableSoftwareInventoryAnalyzer: { section: "Analyzers", label: "Software Inventory Analyzer", source: "tenant" },
enableIntegrityBypassAnalyzer: { section: "Analyzers", label: "Integrity Bypass Analyzer", source: "tenant" },
enableRealmJoinWatcher: { section: "Analyzers", label: "Realm Join Watcher", source: "tenant" },
keepAwakeDuringUserEsp: { section: "Analyzers", label: "Keep Awake During User ESP", source: "tenant" },
enableConsoleBypassDetection: { section: "Analyzers", label: "Console Bypass Detection", source: "tenant" },
};
// ── Ordered views for rendering ──────────────────────────────────────────────
export interface CataloguedRow<TRow> {
/** One rendered row: where its value and default come from, resolved against a response. */
export interface CataloguedRuntimeRow {
key: string;
row: TRow;
row: RuntimeRow;
value: (r: AgentConfigResponse) => unknown;
default: unknown;
}
function groupBySection<TSection extends string, TRow extends { section: TSection }>(
sections: readonly TSection[],
records: Array<Record<string, TRow | Excluded>>,
): Array<{ section: TSection; rows: CataloguedRow<TRow>[] }> {
const bySection = new Map<TSection, CataloguedRow<TRow>[]>(sections.map((s) => [s, []]));
for (const record of records) {
for (const [key, spec] of Object.entries(record)) {
if (isExcluded(spec)) continue;
bySection.get(spec.section)!.push({ key, row: spec });
}
export interface CataloguedTenantRow {
key: keyof TenantConfiguration;
row: TenantRow;
default: unknown;
}
function tenantRows(): Array<{ section: TenantSection; rows: CataloguedTenantRow[] }> {
const bySection = new Map<TenantSection, CataloguedTenantRow[]>(TENANT_SECTIONS.map((s) => [s, []]));
for (const [key, spec] of Object.entries(TENANT_FIELDS) as Array<[keyof TenantConfiguration, TenantRow | Excluded]>) {
if (isExcluded(spec)) continue;
bySection.get(spec.section)!.push({ key, row: spec, default: TENANT_DEFAULTS[key] });
}
return sections.map((section) => ({ section, rows: bySection.get(section)! }));
return TENANT_SECTIONS.map((section) => ({ section, rows: bySection.get(section)! }));
}
export const TENANT_REPORT_SECTIONS = groupBySection<TenantSection, TenantRow>(TENANT_SECTIONS, [TENANT_FIELDS]);
function runtimeRows(): Array<{ section: RuntimeSection; rows: CataloguedRuntimeRow[] }> {
const bySection = new Map<RuntimeSection, CataloguedRuntimeRow[]>(RUNTIME_SECTIONS.map((s) => [s, []]));
const add = (key: string, spec: RuntimeRow | Excluded, value: (r: AgentConfigResponse) => unknown, def: unknown) => {
if (isExcluded(spec)) return;
bySection.get(spec.section)!.push({ key, row: spec, value, default: def });
};
for (const [key, spec] of Object.entries(RUNTIME_FIELDS) as Array<[keyof AgentConfigResponse, RuntimeRow | Excluded]>) {
add(key, spec, (r) => r[key], RUNTIME_DEFAULTS[key]);
}
for (const [key, spec] of Object.entries(COLLECTOR_FIELDS) as Array<[keyof CollectorConfiguration, RuntimeRow | Excluded]>) {
add(`collectors.${key}`, spec, (r) => r.collectors?.[key], COLLECTOR_DEFAULTS[key]);
}
for (const [key, spec] of Object.entries(ANALYZER_FIELDS) as Array<[keyof AnalyzerConfiguration, RuntimeRow | Excluded]>) {
add(`analyzers.${key}`, spec, (r) => r.analyzers?.[key], ANALYZER_DEFAULTS[key]);
}
return RUNTIME_SECTIONS.map((section) => ({ section, rows: bySection.get(section)! }));
}
export const RUNTIME_REPORT_SECTIONS = groupBySection<RuntimeSection, RuntimeRow>(RUNTIME_SECTIONS, [
RUNTIME_FIELDS,
COLLECTOR_FIELDS,
ANALYZER_FIELDS,
]);
export const TENANT_REPORT_SECTIONS = tenantRows();
export const RUNTIME_REPORT_SECTIONS = runtimeRows();
/**
* Value equality for the "custom" highlight: null, undefined and the empty string are one
+7
View File
@@ -135,6 +135,13 @@ export const api = {
*/
fields: (tenantId: string) => `${API_BASE_URL}/api/config/${encodeURIComponent(tenantId)}/fields`,
featureFlags: (tenantId: string) => `${API_BASE_URL}/api/config/${encodeURIComponent(tenantId)}/feature-flags`,
/**
* GET — GA-only: the AgentConfigResponse an agent of the tenant receives right now (same
* derivation as agent/config, minus the per-device kill verdict). Optional agentVersion
* selects the hash-oracle line like X-Agent-Version would; absent = the current line.
*/
effectiveAgentConfig: (tenantId: string, agentVersion?: string) =>
`${API_BASE_URL}/api/config/${encodeURIComponent(tenantId)}/effective-agent-config${agentVersion ? `?agentVersion=${encodeURIComponent(agentVersion)}` : ''}`,
/** PATCH — GA-only plan/trial mutation: { planTier?, trialExpiresUtc?: ISO | null }. */
plan: (tenantId: string) => `${API_BASE_URL}/api/config/${encodeURIComponent(tenantId)}/plan`,
/** POST — tenant-admin self-service 30-day Pro trial (once per tenant, 409 after). */
@@ -60,7 +60,62 @@ export const SHARED_MANIFEST = {
"msrcLastSyncUtc",
"nvdCacheLastRefreshUtc",
"epssLastSyncUtc"
]
],
"defaults": {
"partitionKey": "GlobalConfig",
"rowKey": "config",
"lastUpdated": "0001-01-01T00:00:00.0000000",
"updatedBy": null,
"globalRateLimitRequestsPerMinute": 100,
"userRateLimitRequestsPerMinute": 120,
"portalUserRateLimitRequestsPerMinute": 600,
"globalAdminRateLimitRequestsPerMinute": 600,
"planTierDefinitionsJson": null,
"platformStatsBlobSasUrl": "",
"collectorIdleTimeoutMinutes": 15,
"desktopDetectorNoCandidateTimeoutMinutes": 10,
"excessiveEventCountThreshold": 2000,
"excessiveEventAutoActionMode": "Off",
"excessiveEventAutoActionThreshold": 2500,
"excessiveEventAutoActionDurationHours": 24,
"opsEventRetentionDays": 90,
"slaNotificationCooldownHours": 24,
"feedbackEnabled": true,
"feedbackMinTenantAgeDays": 14,
"feedbackCooldownDays": 60,
"diagnosticsGlobalLogPathsJson": null,
"maxDiagnosticsDownloadSizeMB": 500,
"diagnosticsDownloadTimeoutSeconds": 120,
"mcpAccessPolicy": "WhitelistOnly",
"nvdApiKey": null,
"opsAlertRulesJson": null,
"opsAlertTelegramEnabled": false,
"opsAlertTelegramChatId": null,
"opsAlertTeamsEnabled": false,
"opsAlertTeamsWebhookUrl": null,
"opsAlertSlackEnabled": false,
"opsAlertSlackWebhookUrl": null,
"opsNotificationChannelsJson": null,
"latestAgentV2Version": null,
"latestAgentV2Sha256": null,
"latestAgentV2ExeSha256": null,
"latestBootstrapV2ScriptVersion": null,
"allowAgentDowngrade": false,
"agentMigrateApiBaseUrl": "",
"agentMigrateTenantOverridesJson": "",
"modernDeploymentHarmlessEventIdsJson": null,
"whiteGloveSealingPatternIdsJson": null,
"vulnerabilityCorrelationEnabled": true,
"autoApproveNewTenants": false,
"imeMsiArchivingEnabled": true,
"maxImeMsiDownloadSizeMB": 250,
"selfServiceAppHomingEnabled": false,
"sessionDeletionKillSwitch": false,
"vulnerabilityDataLastSyncUtc": null,
"msrcLastSyncUtc": null,
"nvdCacheLastRefreshUtc": null,
"epssLastSyncUtc": null
}
},
"tenantConfiguration": {
"fields": [
@@ -166,7 +221,217 @@ export const SHARED_MANIFEST = {
"slaNotifyOnAppInstallBreach",
"slaNotifyOnConsecutiveFailures",
"slaConsecutiveFailureThreshold"
]
],
"defaults": {
"tenantId": null,
"domainName": null,
"lastUpdated": "0001-01-01T00:00:00.0000000",
"updatedBy": null,
"onboardedBy": null,
"contactEmail": null,
"companyName": null,
"onboardedAt": null,
"homedAppClientId": null,
"lastAuthClientId": null,
"lastAuthClientIdSince": null,
"disabled": false,
"disabledReason": null,
"disabledUntil": null,
"mcpDisabled": false,
"mcpDisabledReason": null,
"customRateLimitRequestsPerMinute": null,
"customUserRateLimitRequestsPerMinute": null,
"planTier": "free",
"trialExpiresUtc": null,
"trialStartedUtc": null,
"trialConsumed": false,
"trialGrantedBy": null,
"proDowngradedUtc": null,
"maxDelegatedTenantsOverride": null,
"mcpUsagePlanOverride": null,
"payingCustomer": false,
"managedByProTenantId": null,
"manufacturerWhitelist": "Dell*,HP*,Lenovo*,Microsoft Corporation",
"modelWhitelist": "*",
"validateAutopilotDevice": false,
"validateCorporateIdentifier": false,
"validateDeviceAssociation": false,
"validateCloudPcDevice": false,
"validateIntuneDeviceBinding": false,
"allowInsecureAgentRequests": false,
"dataRetentionDays": 90,
"sessionTimeoutHours": 5,
"sessionGraceHours": 0,
"maxNdjsonPayloadSizeMB": 5,
"enablePerformanceCollector": true,
"performanceCollectorIntervalSeconds": 30,
"helloWaitTimeoutSeconds": 30,
"maxAuthFailures": null,
"authFailureTimeoutMinutes": null,
"agentMaxLifetimeMinutes": null,
"absoluteMaxSessionHours": null,
"selfDestructOnComplete": true,
"keepLogFile": false,
"rebootOnComplete": null,
"rebootDelaySeconds": null,
"enableGeoLocation": null,
"ntpServer": "time.windows.com",
"enableTimezoneAutoSet": null,
"enableDoGroupIdAutoSet": null,
"enableImeMatchLog": null,
"enableGatherRuleDebugLog": null,
"enableEspContinueAnywayObservation": null,
"logLevel": null,
"maxBatchSize": null,
"showEnrollmentSummary": null,
"enrollmentSummaryTimeoutSeconds": null,
"enrollmentSummaryBrandingImageUrl": null,
"enrollmentSummaryLaunchRetrySeconds": null,
"showScriptOutput": true,
"enableLocalAdminAnalyzer": null,
"enableSoftwareInventoryAnalyzer": null,
"enableIntegrityBypassAnalyzer": null,
"enableRealmJoinWatcher": null,
"keepAwakeDuringUserEsp": null,
"enableConsoleBypassDetection": null,
"localAdminAllowedAccountsJson": null,
"bootstrapTokenEnabled": false,
"unrestrictedModeEnabled": false,
"unrestrictedMode": false,
"entraAppRolesEnabled": false,
"diagnosticsLogPathsJson": null,
"diagnosticsBlobSasUrl": null,
"diagnosticsUploadMode": "Off",
"diagnosticsUploadDestination": "CustomerSas",
"sendTraceEvents": true,
"teamsWebhookUrl": null,
"teamsNotifyOnSuccess": true,
"teamsNotifyOnFailure": true,
"teamsNotifyOnStart": false,
"webhookProviderType": 0,
"webhookUrl": null,
"webhookNotifyOnSuccess": true,
"webhookNotifyOnFailure": true,
"webhookNotifyOnHardwareRejection": false,
"webhookNotifyOnStart": false,
"webhookCustomHeadersJson": null,
"notificationChannelsJson": null,
"slaTargetSuccessRate": null,
"slaTargetMaxDurationMinutes": null,
"slaTargetAppInstallSuccessRate": null,
"slaNotifyOnSuccessRateBreach": false,
"slaSuccessRateNotifyThreshold": null,
"slaNotifyOnDurationBreach": false,
"slaNotifyOnAppInstallBreach": false,
"slaNotifyOnConsecutiveFailures": false,
"slaConsecutiveFailureThreshold": 5
}
},
"agentConfig": {
"responseDefaults": {
"configVersion": 0,
"uploadIntervalSeconds": 10,
"selfDestructOnComplete": true,
"keepLogFile": false,
"enableGeoLocation": true,
"enableImeMatchLog": false,
"enableGatherRuleDebugLog": false,
"enableEspContinueAnywayObservation": false,
"collectors": null,
"gatherRules": [],
"imeLogPatterns": [],
"whiteGloveSealingPatternIds": [],
"maxAuthFailures": 5,
"authFailureTimeoutMinutes": 0,
"logLevel": "Info",
"rebootOnComplete": false,
"rebootDelaySeconds": 10,
"showEnrollmentSummary": false,
"enrollmentSummaryTimeoutSeconds": 60,
"enrollmentSummaryBrandingImageUrl": null,
"enrollmentSummaryLaunchRetrySeconds": 120,
"maxBatchSize": 100,
"diagnosticsUploadEnabled": false,
"diagnosticsUploadMode": "Off",
"diagnosticsLogPaths": [],
"analyzers": null,
"sendTraceEvents": true,
"unrestrictedMode": false,
"latestAgentSha256": null,
"latestAgentExeSha256": null,
"allowAgentDowngrade": false,
"ntpServer": "time.windows.com",
"enableTimezoneAutoSet": false,
"enableDoGroupIdAutoSet": false,
"deviceBlocked": false,
"deviceKillSignal": false,
"unblockAt": null,
"migrateToApiBaseUrl": null
},
"collectorDefaults": {
"enablePerformanceCollector": true,
"performanceIntervalSeconds": 30,
"collectorIdleTimeoutMinutes": 15,
"desktopDetectorNoCandidateTimeoutMinutes": 10,
"enableAgentSelfMetrics": true,
"agentSelfMetricsIntervalSeconds": 60,
"helloWaitTimeoutSeconds": 30,
"agentMaxLifetimeMinutes": 360,
"enableDeliveryOptimizationCollector": true,
"deliveryOptimizationIntervalSeconds": 3,
"enableOfficeInstallDetector": true,
"officeInstallSettleSeconds": 10,
"enablePowerStateWatcher": true,
"stallProbeEnabled": true,
"stallProbeThresholdsMinutes": [
2,
15,
30,
60,
180
],
"stallProbeTraceIndices": [
2
],
"stallProbeSources": [
"provisioning_registry",
"diagnostics_registry",
"eventlog",
"appworkload_log"
],
"sessionStalledAfterProbeIndex": 4,
"modernDeploymentWatcherEnabled": true,
"modernDeploymentLogLevelMax": 3,
"modernDeploymentBackfillEnabled": true,
"modernDeploymentHarmlessEventIds": [
100,
1005,
1010
],
"modernDeploymentBackfillLookbackMinutes": 30,
"windowsUpdateWatcherEnabled": true,
"windowsUpdateTargetedEventIds": [
19,
20,
43,
44
],
"windowsUpdateBackfillLookbackMinutes": 60,
"windowsUpdateChannelCensusEnabled": true,
"mdmRebootPolicyWatcherEnabled": true,
"mdmRebootPolicyBackfillLookbackMinutes": 60,
"enableSystemTimelineWatcher": true,
"systemEventBackfillLookbackMinutes": 1440
},
"analyzerDefaults": {
"enableLocalAdminAnalyzer": true,
"localAdminAllowedAccounts": [],
"enableSoftwareInventoryAnalyzer": false,
"enableIntegrityBypassAnalyzer": true,
"enableRealmJoinWatcher": false,
"keepAwakeDuringUserEsp": false,
"enableConsoleBypassDetection": true
}
},
"sessionStatuses": [
"InProgress",
@@ -56,7 +56,62 @@
"msrcLastSyncUtc",
"nvdCacheLastRefreshUtc",
"epssLastSyncUtc"
]
],
"defaults": {
"partitionKey": "GlobalConfig",
"rowKey": "config",
"lastUpdated": "0001-01-01T00:00:00.0000000",
"updatedBy": null,
"globalRateLimitRequestsPerMinute": 100,
"userRateLimitRequestsPerMinute": 120,
"portalUserRateLimitRequestsPerMinute": 600,
"globalAdminRateLimitRequestsPerMinute": 600,
"planTierDefinitionsJson": null,
"platformStatsBlobSasUrl": "",
"collectorIdleTimeoutMinutes": 15,
"desktopDetectorNoCandidateTimeoutMinutes": 10,
"excessiveEventCountThreshold": 2000,
"excessiveEventAutoActionMode": "Off",
"excessiveEventAutoActionThreshold": 2500,
"excessiveEventAutoActionDurationHours": 24,
"opsEventRetentionDays": 90,
"slaNotificationCooldownHours": 24,
"feedbackEnabled": true,
"feedbackMinTenantAgeDays": 14,
"feedbackCooldownDays": 60,
"diagnosticsGlobalLogPathsJson": null,
"maxDiagnosticsDownloadSizeMB": 500,
"diagnosticsDownloadTimeoutSeconds": 120,
"mcpAccessPolicy": "WhitelistOnly",
"nvdApiKey": null,
"opsAlertRulesJson": null,
"opsAlertTelegramEnabled": false,
"opsAlertTelegramChatId": null,
"opsAlertTeamsEnabled": false,
"opsAlertTeamsWebhookUrl": null,
"opsAlertSlackEnabled": false,
"opsAlertSlackWebhookUrl": null,
"opsNotificationChannelsJson": null,
"latestAgentV2Version": null,
"latestAgentV2Sha256": null,
"latestAgentV2ExeSha256": null,
"latestBootstrapV2ScriptVersion": null,
"allowAgentDowngrade": false,
"agentMigrateApiBaseUrl": "",
"agentMigrateTenantOverridesJson": "",
"modernDeploymentHarmlessEventIdsJson": null,
"whiteGloveSealingPatternIdsJson": null,
"vulnerabilityCorrelationEnabled": true,
"autoApproveNewTenants": false,
"imeMsiArchivingEnabled": true,
"maxImeMsiDownloadSizeMB": 250,
"selfServiceAppHomingEnabled": false,
"sessionDeletionKillSwitch": false,
"vulnerabilityDataLastSyncUtc": null,
"msrcLastSyncUtc": null,
"nvdCacheLastRefreshUtc": null,
"epssLastSyncUtc": null
}
},
"tenantConfiguration": {
"fields": [
@@ -162,7 +217,217 @@
"slaNotifyOnAppInstallBreach",
"slaNotifyOnConsecutiveFailures",
"slaConsecutiveFailureThreshold"
]
],
"defaults": {
"tenantId": null,
"domainName": null,
"lastUpdated": "0001-01-01T00:00:00.0000000",
"updatedBy": null,
"onboardedBy": null,
"contactEmail": null,
"companyName": null,
"onboardedAt": null,
"homedAppClientId": null,
"lastAuthClientId": null,
"lastAuthClientIdSince": null,
"disabled": false,
"disabledReason": null,
"disabledUntil": null,
"mcpDisabled": false,
"mcpDisabledReason": null,
"customRateLimitRequestsPerMinute": null,
"customUserRateLimitRequestsPerMinute": null,
"planTier": "free",
"trialExpiresUtc": null,
"trialStartedUtc": null,
"trialConsumed": false,
"trialGrantedBy": null,
"proDowngradedUtc": null,
"maxDelegatedTenantsOverride": null,
"mcpUsagePlanOverride": null,
"payingCustomer": false,
"managedByProTenantId": null,
"manufacturerWhitelist": "Dell*,HP*,Lenovo*,Microsoft Corporation",
"modelWhitelist": "*",
"validateAutopilotDevice": false,
"validateCorporateIdentifier": false,
"validateDeviceAssociation": false,
"validateCloudPcDevice": false,
"validateIntuneDeviceBinding": false,
"allowInsecureAgentRequests": false,
"dataRetentionDays": 90,
"sessionTimeoutHours": 5,
"sessionGraceHours": 0,
"maxNdjsonPayloadSizeMB": 5,
"enablePerformanceCollector": true,
"performanceCollectorIntervalSeconds": 30,
"helloWaitTimeoutSeconds": 30,
"maxAuthFailures": null,
"authFailureTimeoutMinutes": null,
"agentMaxLifetimeMinutes": null,
"absoluteMaxSessionHours": null,
"selfDestructOnComplete": true,
"keepLogFile": false,
"rebootOnComplete": null,
"rebootDelaySeconds": null,
"enableGeoLocation": null,
"ntpServer": "time.windows.com",
"enableTimezoneAutoSet": null,
"enableDoGroupIdAutoSet": null,
"enableImeMatchLog": null,
"enableGatherRuleDebugLog": null,
"enableEspContinueAnywayObservation": null,
"logLevel": null,
"maxBatchSize": null,
"showEnrollmentSummary": null,
"enrollmentSummaryTimeoutSeconds": null,
"enrollmentSummaryBrandingImageUrl": null,
"enrollmentSummaryLaunchRetrySeconds": null,
"showScriptOutput": true,
"enableLocalAdminAnalyzer": null,
"enableSoftwareInventoryAnalyzer": null,
"enableIntegrityBypassAnalyzer": null,
"enableRealmJoinWatcher": null,
"keepAwakeDuringUserEsp": null,
"enableConsoleBypassDetection": null,
"localAdminAllowedAccountsJson": null,
"bootstrapTokenEnabled": false,
"unrestrictedModeEnabled": false,
"unrestrictedMode": false,
"entraAppRolesEnabled": false,
"diagnosticsLogPathsJson": null,
"diagnosticsBlobSasUrl": null,
"diagnosticsUploadMode": "Off",
"diagnosticsUploadDestination": "CustomerSas",
"sendTraceEvents": true,
"teamsWebhookUrl": null,
"teamsNotifyOnSuccess": true,
"teamsNotifyOnFailure": true,
"teamsNotifyOnStart": false,
"webhookProviderType": 0,
"webhookUrl": null,
"webhookNotifyOnSuccess": true,
"webhookNotifyOnFailure": true,
"webhookNotifyOnHardwareRejection": false,
"webhookNotifyOnStart": false,
"webhookCustomHeadersJson": null,
"notificationChannelsJson": null,
"slaTargetSuccessRate": null,
"slaTargetMaxDurationMinutes": null,
"slaTargetAppInstallSuccessRate": null,
"slaNotifyOnSuccessRateBreach": false,
"slaSuccessRateNotifyThreshold": null,
"slaNotifyOnDurationBreach": false,
"slaNotifyOnAppInstallBreach": false,
"slaNotifyOnConsecutiveFailures": false,
"slaConsecutiveFailureThreshold": 5
}
},
"agentConfig": {
"responseDefaults": {
"configVersion": 0,
"uploadIntervalSeconds": 10,
"selfDestructOnComplete": true,
"keepLogFile": false,
"enableGeoLocation": true,
"enableImeMatchLog": false,
"enableGatherRuleDebugLog": false,
"enableEspContinueAnywayObservation": false,
"collectors": null,
"gatherRules": [],
"imeLogPatterns": [],
"whiteGloveSealingPatternIds": [],
"maxAuthFailures": 5,
"authFailureTimeoutMinutes": 0,
"logLevel": "Info",
"rebootOnComplete": false,
"rebootDelaySeconds": 10,
"showEnrollmentSummary": false,
"enrollmentSummaryTimeoutSeconds": 60,
"enrollmentSummaryBrandingImageUrl": null,
"enrollmentSummaryLaunchRetrySeconds": 120,
"maxBatchSize": 100,
"diagnosticsUploadEnabled": false,
"diagnosticsUploadMode": "Off",
"diagnosticsLogPaths": [],
"analyzers": null,
"sendTraceEvents": true,
"unrestrictedMode": false,
"latestAgentSha256": null,
"latestAgentExeSha256": null,
"allowAgentDowngrade": false,
"ntpServer": "time.windows.com",
"enableTimezoneAutoSet": false,
"enableDoGroupIdAutoSet": false,
"deviceBlocked": false,
"deviceKillSignal": false,
"unblockAt": null,
"migrateToApiBaseUrl": null
},
"collectorDefaults": {
"enablePerformanceCollector": true,
"performanceIntervalSeconds": 30,
"collectorIdleTimeoutMinutes": 15,
"desktopDetectorNoCandidateTimeoutMinutes": 10,
"enableAgentSelfMetrics": true,
"agentSelfMetricsIntervalSeconds": 60,
"helloWaitTimeoutSeconds": 30,
"agentMaxLifetimeMinutes": 360,
"enableDeliveryOptimizationCollector": true,
"deliveryOptimizationIntervalSeconds": 3,
"enableOfficeInstallDetector": true,
"officeInstallSettleSeconds": 10,
"enablePowerStateWatcher": true,
"stallProbeEnabled": true,
"stallProbeThresholdsMinutes": [
2,
15,
30,
60,
180
],
"stallProbeTraceIndices": [
2
],
"stallProbeSources": [
"provisioning_registry",
"diagnostics_registry",
"eventlog",
"appworkload_log"
],
"sessionStalledAfterProbeIndex": 4,
"modernDeploymentWatcherEnabled": true,
"modernDeploymentLogLevelMax": 3,
"modernDeploymentBackfillEnabled": true,
"modernDeploymentHarmlessEventIds": [
100,
1005,
1010
],
"modernDeploymentBackfillLookbackMinutes": 30,
"windowsUpdateWatcherEnabled": true,
"windowsUpdateTargetedEventIds": [
19,
20,
43,
44
],
"windowsUpdateBackfillLookbackMinutes": 60,
"windowsUpdateChannelCensusEnabled": true,
"mdmRebootPolicyWatcherEnabled": true,
"mdmRebootPolicyBackfillLookbackMinutes": 60,
"enableSystemTimelineWatcher": true,
"systemEventBackfillLookbackMinutes": 1440
},
"analyzerDefaults": {
"enableLocalAdminAnalyzer": true,
"localAdminAllowedAccounts": [],
"enableSoftwareInventoryAnalyzer": false,
"enableIntegrityBypassAnalyzer": true,
"enableRealmJoinWatcher": false,
"keepAwakeDuringUserEsp": false,
"enableConsoleBypassDetection": true
}
},
"sessionStatuses": [
"InProgress",