diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/DefaultComponentFactoryOrderingTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/DefaultComponentFactoryOrderingTests.cs index a7d5b68e..dc383bf1 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/DefaultComponentFactoryOrderingTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/DefaultComponentFactoryOrderingTests.cs @@ -40,7 +40,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration /// enabled by default) and system-boundary fakes only: temp state dir, fake ingress, /// virtual clock, no network metrics, no telemetry spool. /// - private CollectorSurfaces CreateSurfaces() + private CollectorSurfaces CreateSurfaces(AnalyzerConfiguration? analyzers = null) { var factory = new DefaultComponentFactory( agentConfig: new AgentConfiguration @@ -52,6 +52,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration remoteConfig: new AgentConfigResponse { Collectors = CollectorConfiguration.CreateDefault(), + Analyzers = analyzers ?? new AnalyzerConfiguration(), }, networkMetrics: null, agentVersion: "test", @@ -142,6 +143,26 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration } } + [Theory] + [InlineData(false, 0)] + [InlineData(true, 1)] + public void ConfigMgr_client_host_exists_only_when_the_tenant_enables_the_watcher(bool enabled, int expected) + { + // Opt-in (D-382): with the watcher off the check must not run at all, not merely stay quiet. + var hosts = CreateSurfaces(new AnalyzerConfiguration { EnableConfigMgrWatcher = enabled }).Hosts; + try + { + Assert.Equal(expected, hosts.Count(h => h is ConfigMgrClientHost)); + } + finally + { + foreach (var host in hosts) + { + try { host.Dispose(); } catch { /* construction-only test — best-effort cleanup */ } + } + } + } + [Fact] public void Ime_app_state_callback_is_chained_into_the_user_apps_settled_recheck() { diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs index e1afe325..546b6119 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs @@ -517,14 +517,23 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration startupGate: _startupEventGate)); } - // ConfigMgr client presence (the V1 check) — always-on, no config gate, on every flow. - hosts.Add(new ConfigMgrClientHost( - sessionId: sessionId, - tenantId: tenantId, - logger: logger, - ingress: ingress, - clock: clock, - startupGate: _startupEventGate)); + // ConfigMgr client presence (the V1 check) — opt-in per tenant (portal toggle → + // AnalyzerConfiguration.EnableConfigMgrWatcher, default off). When off, the host is + // never created, so devices of other tenants run no check at all. + if (analyzers.EnableConfigMgrWatcher) + { + hosts.Add(new ConfigMgrClientHost( + sessionId: sessionId, + tenantId: tenantId, + logger: logger, + ingress: ingress, + clock: clock, + startupGate: _startupEventGate)); + } + else + { + logger.Info("DefaultComponentFactory: ConfigMgrClientHost not created (EnableConfigMgrWatcher=false)"); + } // Registry second pillar (audit 2026-08-17): IME's authoritative per-app state from // HKLM\...\IntuneManagementExtension (Win32Apps EnforcementStateMessage, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/Hosts/ConfigMgrClientHost.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/Hosts/ConfigMgrClientHost.cs index 9a979c80..f32ae29a 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/Hosts/ConfigMgrClientHost.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/Hosts/ConfigMgrClientHost.cs @@ -12,7 +12,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration /// /// Drives the : a first check shortly after start (the /// client may already be installed) and one per minute after that (it may arrive later, e.g. - /// as a Win32 app). Always-on, no config gate — like the V1 check it replaces. + /// as a Win32 app). Created only when the tenant enables the ConfigMgr watcher + /// (AnalyzerConfiguration.EnableConfigMgrWatcher). /// internal sealed class ConfigMgrClientHost : ICollectorHost { diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/AgentConfigResolverTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/AgentConfigResolverTests.cs index e87bd99b..4e9456b4 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/AgentConfigResolverTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/AgentConfigResolverTests.cs @@ -72,10 +72,19 @@ public class AgentConfigResolverTests Assert.False(r.Analyzers.EnableSoftwareInventoryAnalyzer); Assert.True(r.Analyzers.EnableIntegrityBypassAnalyzer); Assert.False(r.Analyzers.EnableRealmJoinWatcher); + Assert.False(r.Analyzers.EnableConfigMgrWatcher); Assert.False(r.Analyzers.KeepAwakeDuringUserEsp); Assert.True(r.Analyzers.EnableConsoleBypassDetection); } + [Fact] + public void The_tenant_switches_the_ConfigMgr_watcher_on() + { + var r = Build(Tenant(t => t.EnableConfigMgrWatcher = true)); + + Assert.True(r.Analyzers.EnableConfigMgrWatcher); + } + [Theory] [InlineData(null, "CustomerSas", "Off", false)] [InlineData(null, "Hosted", "Off", true)] // hosted destination needs no SAS diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableConfigRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableConfigRepository.cs index 5e2cd342..6a1a7032 100644 --- a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableConfigRepository.cs +++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableConfigRepository.cs @@ -859,6 +859,7 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage { "EnableSoftwareInventoryAnalyzer", config.EnableSoftwareInventoryAnalyzer }, { "EnableIntegrityBypassAnalyzer", config.EnableIntegrityBypassAnalyzer }, { "EnableRealmJoinWatcher", config.EnableRealmJoinWatcher }, + { "EnableConfigMgrWatcher", config.EnableConfigMgrWatcher }, { "KeepAwakeDuringUserEsp", config.KeepAwakeDuringUserEsp }, { "EnableConsoleBypassDetection", config.EnableConsoleBypassDetection }, { "LocalAdminAllowedAccountsJson", config.LocalAdminAllowedAccountsJson }, @@ -980,6 +981,7 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage EnableSoftwareInventoryAnalyzer = entity.GetBoolean("EnableSoftwareInventoryAnalyzer"), EnableIntegrityBypassAnalyzer = entity.GetBoolean("EnableIntegrityBypassAnalyzer"), EnableRealmJoinWatcher = entity.GetBoolean("EnableRealmJoinWatcher"), + EnableConfigMgrWatcher = entity.GetBoolean("EnableConfigMgrWatcher"), KeepAwakeDuringUserEsp = entity.GetBoolean("KeepAwakeDuringUserEsp"), EnableConsoleBypassDetection = entity.GetBoolean("EnableConsoleBypassDetection"), LocalAdminAllowedAccountsJson = entity.GetString("LocalAdminAllowedAccountsJson"), diff --git a/src/Backend/AutopilotMonitor.Functions/Services/AgentConfigResolver.cs b/src/Backend/AutopilotMonitor.Functions/Services/AgentConfigResolver.cs index 6844c017..841d2b0f 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/AgentConfigResolver.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/AgentConfigResolver.cs @@ -36,7 +36,7 @@ namespace AutopilotMonitor.Functions.Services public class AgentConfigResolver { /// Bumped when the wire shape gains a field the agent must understand. - public const int ConfigVersion = 41; // InterceptedRegistrationRetryBoots (boots after an intercepted registration) + public const int ConfigVersion = 42; // Analyzers.EnableConfigMgrWatcher (ConfigMgr client check, opt-in) /// /// Served as . A code @@ -154,6 +154,7 @@ namespace AutopilotMonitor.Functions.Services EnableSoftwareInventoryAnalyzer = tenantConfig.EnableSoftwareInventoryAnalyzer ?? false, EnableIntegrityBypassAnalyzer = tenantConfig.EnableIntegrityBypassAnalyzer ?? true, EnableRealmJoinWatcher = tenantConfig.EnableRealmJoinWatcher ?? false, + EnableConfigMgrWatcher = tenantConfig.EnableConfigMgrWatcher ?? false, KeepAwakeDuringUserEsp = tenantConfig.KeepAwakeDuringUserEsp ?? false, EnableConsoleBypassDetection = tenantConfig.EnableConsoleBypassDetection ?? true }, diff --git a/src/McpServer/autopilot-monitor-mcp/src/generated/wire-types.generated.ts b/src/McpServer/autopilot-monitor-mcp/src/generated/wire-types.generated.ts index 37dafde2..5b17321e 100644 --- a/src/McpServer/autopilot-monitor-mcp/src/generated/wire-types.generated.ts +++ b/src/McpServer/autopilot-monitor-mcp/src/generated/wire-types.generated.ts @@ -421,6 +421,8 @@ export interface AnalyzerConfiguration { enableIntegrityBypassAnalyzer: boolean; /** Whether to enable the RealmJoin watcher, which tracks RealmJoin deployment state and enrollment packages during provisioning. This is not an IAgentAnalyzer; the flag gates creation of the RealmJoin collector host in the agent runtime. Default: false (opt-in; most tenants do not deploy via RealmJoin). */ enableRealmJoinWatcher: boolean; + /** Whether to enable the ConfigMgr watcher, which reports whether the Configuration Manager client is installed (service state, version, site code). This is not an IAgentAnalyzer; the flag gates creation of the ConfigMgr client host in the agent runtime. Default: false (opt-in; most tenants do not co-manage with ConfigMgr). */ + enableConfigMgrWatcher: boolean; /** Whether to keep the device awake (system + display) during the User-ESP (AccountSetup) phase so idle standby/sleep cannot stall app installs or account provisioning. This is not an IAgentAnalyzer; the flag gates creation of the UserEspKeepAwake host in the agent runtime. Reboots are unaffected (the hold only resets idle timers). Default: false (opt-in per tenant). */ keepAwakeDuringUserEsp: boolean; /** Whether to detect an interactive console opened during enrollment — the classic Shift+F10 OOBE bypass. Gates BOTH the live ConsoleBypass host (an interactive-session cmd.exe with a bare, non-scripted command line; stopped on real-user desktop arrival) and the startup ConsolePrefetchScanner (CMD.EXE-*.pf ran after boot). Detection is best-effort, not gapless — a press before the agent installs is only coarsely covered by the prefetch artifact. Security-sensitive; not an IAgentAnalyzer toggle in the usual sense (it also gates a collector host). Default: true (opt-out per tenant — a Shift+F10 console during enrollment is a finding worth surfacing by default; tenants that knowingly use Shift+F10 for support can disable it). */ @@ -5359,6 +5361,8 @@ export interface TenantConfiguration { enableIntegrityBypassAnalyzer?: boolean | null; /** Whether the RealmJoin watcher is enabled for this tenant's devices. RealmJoin enrollment-package tracking is off by default; enable only for tenants that deploy via RealmJoin. null = use agent default (false). */ enableRealmJoinWatcher?: boolean | null; + /** Whether the ConfigMgr watcher is enabled for this tenant's devices: the agent reports whether the Configuration Manager client is installed (service state, version, site code). Off by default; enable for tenants that co-manage with ConfigMgr. null = use agent default (false). */ + enableConfigMgrWatcher?: boolean | null; /** Whether to keep the device awake during the User-ESP (AccountSetup) phase for this tenant's devices. Prevents idle standby/sleep from stalling app installs / account provisioning; reboots are unaffected. Off by default. null = use agent default (false). */ keepAwakeDuringUserEsp?: boolean | null; /** Whether to detect a SYSTEM console opened during enrollment (Shift+F10 OOBE bypass) for this tenant's devices. Gates the live ConsoleBypass watcher + the startup prefetch scanner. On by default (opt-out); tenants that knowingly use Shift+F10 for support can disable it. null = use agent default (true). */ diff --git a/src/Shared/AutopilotMonitor.Shared/Constants.cs b/src/Shared/AutopilotMonitor.Shared/Constants.cs index a9c3fff2..fb70aef4 100644 --- a/src/Shared/AutopilotMonitor.Shared/Constants.cs +++ b/src/Shared/AutopilotMonitor.Shared/Constants.cs @@ -742,7 +742,7 @@ namespace AutopilotMonitor.Shared public const string SessionParkedWithoutDeadline = "session_parked_without_deadline"; // One-shot Warning tripwire (liveness plan PR1): a non-terminal session entered the post-AccountSetup dead-end zone (ESP final exit or advisory failure) with NO resolution-capable deadline armed. After the 2026-06-12 dead-end fixes this must never fire in production — every occurrence is a bug report for an unknown parking variant. public const string DiskSpaceLow = "disk_space_low"; // One-shot Warning (state-change-only, 1 GB hysteresis — NOT periodic) when the system drive drops below 2 GB free. Review MON-B10: disk-full was previously only visible inside Debug performance_snapshot payloads (filtered out of timelines), so a disk-starved enrollment carried no actionable threshold signal. public const string EspPolicyProviderStalled = "esp_policy_provider_stalled"; // One-shot-per-provider Warning: a registered EnrollmentStatusTracking CSP policy provider stayed incomplete >= 15 min (Setup/Apps TrackingPoliciesCreated != 1, or DevicePreparation InstallationState not in {2,3} while device preparation is still open). The ESP waits on Setup/Apps providers WITHOUT any OS timeout — e.g. pre-provisioning + co-management leaves only "ConfigMgr" registered (no "Sidecar" = IME) and the user ESP sits at "Apps (Identifying)" for days. Observational only; payload carries the full provider list. See EspPolicyProviderProbe. - public const string ConfigMgrClientDetected = "configmgr_client_detected"; // Info when the ConfigMgr client is first seen and again when its reported state changes (CCM key, CCMSetup version, assigned site code, %windir%\CCM, CcmExec state; V1 confidence score), capped per run. The V1 check, restored in V2 (D-382). + public const string ConfigMgrClientDetected = "configmgr_client_detected"; // Info when the ConfigMgr client is first seen and again when its reported state changes (CCM key, CCMSetup version, assigned site code, %windir%\CCM, CcmExec state; V1 confidence score), capped per run. The V1 check, restored in V2 and opt-in per tenant (AnalyzerConfiguration.EnableConfigMgrWatcher, D-382). public const string KeepAwakeEngaged = "keep_awake_engaged"; // Info (state-change, NOT periodic) emitted by the opt-in UserEspKeepAwake host when it holds the device awake (system + display) on entering the User-ESP/AccountSetup phase, so idle standby cannot stall app installs / account provisioning. Reboots are unaffected. Per-tenant toggle (AnalyzerConfiguration.KeepAwakeDuringUserEsp, default off). public const string KeepAwakeReleased = "keep_awake_released"; // Info (Warning when reason=safety_cap) emitted when the keep-awake hold is released — reason = user_esp_exited (Shell-Core 62407 after AccountSetup progress, i.e. the user-ESP page closed; agents before 2026-09-04 wrote account_setup_complete for the synthesized completion, which fired minutes early) | host_stop | safety_cap. Brackets KeepAwakeEngaged; the OS also auto-clears the hold on process exit/reboot. diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Config/AgentConfigResponse.cs b/src/Shared/AutopilotMonitor.Shared/Models/Config/AgentConfigResponse.cs index 0b02f197..5bccc5a0 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Config/AgentConfigResponse.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Config/AgentConfigResponse.cs @@ -659,6 +659,14 @@ namespace AutopilotMonitor.Shared.Models /// public bool EnableRealmJoinWatcher { get; set; } = false; + /// + /// Whether to enable the ConfigMgr watcher, which reports whether the Configuration + /// Manager client is installed (service state, version, site code). This is not an + /// IAgentAnalyzer; the flag gates creation of the ConfigMgr client host in the agent runtime. + /// Default: false (opt-in; most tenants do not co-manage with ConfigMgr). + /// + public bool EnableConfigMgrWatcher { get; set; } = false; + /// /// Whether to keep the device awake (system + display) during the User-ESP (AccountSetup) /// phase so idle standby/sleep cannot stall app installs or account provisioning. This is diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs b/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs index 3c874103..d80a9c2b 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs @@ -603,6 +603,14 @@ namespace AutopilotMonitor.Shared.Models /// public bool? EnableRealmJoinWatcher { get; set; } = null; + /// + /// Whether the ConfigMgr watcher is enabled for this tenant's devices: the agent reports + /// whether the Configuration Manager client is installed (service state, version, site + /// code). Off by default; enable for tenants that co-manage with ConfigMgr. + /// null = use agent default (false). + /// + public bool? EnableConfigMgrWatcher { get; set; } = null; + /// /// Whether to keep the device awake during the User-ESP (AccountSetup) phase for this /// tenant's devices. Prevents idle standby/sleep from stalling app installs / account diff --git a/src/Web/autopilot-monitor-web/app/admin/tenants/sections/__tests__/tenantConfigReportCatalog.test.ts b/src/Web/autopilot-monitor-web/app/admin/tenants/sections/__tests__/tenantConfigReportCatalog.test.ts index 58b4cbcb..f2f7410a 100644 --- a/src/Web/autopilot-monitor-web/app/admin/tenants/sections/__tests__/tenantConfigReportCatalog.test.ts +++ b/src/Web/autopilot-monitor-web/app/admin/tenants/sections/__tests__/tenantConfigReportCatalog.test.ts @@ -145,6 +145,7 @@ describe("runtime catalogs parity with the agent-config defaults manifest", () = expect(agentDefaults.collectorDefaults.collectorIdleTimeoutMinutes).toBe(15); expect(agentDefaults.analyzerDefaults.enableIntegrityBypassAnalyzer).toBe(true); expect(agentDefaults.analyzerDefaults.enableRealmJoinWatcher).toBe(false); + expect(agentDefaults.analyzerDefaults.enableConfigMgrWatcher).toBe(false); }); }); diff --git a/src/Web/autopilot-monitor-web/app/admin/tenants/sections/tenantConfigReportCatalog.ts b/src/Web/autopilot-monitor-web/app/admin/tenants/sections/tenantConfigReportCatalog.ts index b4be9d46..49f1bbd4 100644 --- a/src/Web/autopilot-monitor-web/app/admin/tenants/sections/tenantConfigReportCatalog.ts +++ b/src/Web/autopilot-monitor-web/app/admin/tenants/sections/tenantConfigReportCatalog.ts @@ -190,6 +190,7 @@ export const TENANT_FIELDS: Record
  • Device identity — serial number, device name, manufacturer, model, and the Entra ID tenant the device enrolls into
  • Enrollment progress — phases, ESP stages, application and script results, policy activity, Windows updates installed during the enrollment, reboots, timings, and failure codes
  • -
  • Device context — OS build, hardware characteristics, disk and network state, whether the Configuration Manager client is installed (service state, version, site code), plus whatever your own gather rules request
  • +
  • Device context — OS build, hardware characteristics, disk and network state, plus whatever your own gather rules request; with the ConfigMgr Watcher enabled, also whether the Configuration Manager client is installed (service state, version, site code)
  • Approximate location — country, region, city, and approximate coordinates, if geolocation is enabled for your tenant
  • diff --git a/src/Web/autopilot-monitor-web/app/settings/TenantConfigContext.tsx b/src/Web/autopilot-monitor-web/app/settings/TenantConfigContext.tsx index 14508740..83f6b3c0 100644 --- a/src/Web/autopilot-monitor-web/app/settings/TenantConfigContext.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/TenantConfigContext.tsx @@ -222,6 +222,8 @@ interface TenantConfigContextValue { setEnrollmentSummaryLaunchRetrySeconds: (v: number) => void; enableRealmJoinWatcher: boolean; setEnableRealmJoinWatcher: (v: boolean) => void; + enableConfigMgrWatcher: boolean; + setEnableConfigMgrWatcher: (v: boolean) => void; handleSaveAgentSettings: () => void; handleResetAgentSettings: () => void; @@ -432,6 +434,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode } const [enrollmentSummaryBrandingImageUrl, setEnrollmentSummaryBrandingImageUrl] = useState(""); const [enrollmentSummaryLaunchRetrySeconds, setEnrollmentSummaryLaunchRetrySeconds] = useState(120); const [enableRealmJoinWatcher, setEnableRealmJoinWatcher] = useState(false); + const [enableConfigMgrWatcher, setEnableConfigMgrWatcher] = useState(false); const [keepAwakeDuringUserEsp, setKeepAwakeDuringUserEsp] = useState(false); // Notification channels @@ -592,6 +595,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode } setEnrollmentSummaryBrandingImageUrl(data.enrollmentSummaryBrandingImageUrl ?? ""); setEnrollmentSummaryLaunchRetrySeconds(data.enrollmentSummaryLaunchRetrySeconds ?? 120); setEnableRealmJoinWatcher(data.enableRealmJoinWatcher ?? false); + setEnableConfigMgrWatcher(data.enableConfigMgrWatcher ?? false); setKeepAwakeDuringUserEsp(data.keepAwakeDuringUserEsp ?? false); // Notification channels (auto-migrates from the legacy single-webhook fields) setNotificationChannels(channelsFromConfig(data)); @@ -785,6 +789,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode } enrollmentSummaryBrandingImageUrl: enrollmentSummaryBrandingImageUrl || undefined, enrollmentSummaryLaunchRetrySeconds, enableRealmJoinWatcher, + enableConfigMgrWatcher, keepAwakeDuringUserEsp, // Notification channels are the authoritative config; the legacy single-webhook // fields are cleared on save so deleting the last channel can't resurrect a zombie @@ -853,7 +858,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode } helloWaitTimeoutSeconds, selfDestructOnComplete, keepLogFile, rebootOnComplete, rebootDelaySeconds, contactEmail, companyName, enableGeoLocation, enableTimezoneAutoSet, enableDoGroupIdAutoSet, enableImeMatchLog, enableGatherRuleDebugLog, logLevel, showScriptOutput, showEnrollmentSummary, enrollmentSummaryTimeoutSeconds, enrollmentSummaryBrandingImageUrl, enrollmentSummaryLaunchRetrySeconds, - enableRealmJoinWatcher, keepAwakeDuringUserEsp, + enableRealmJoinWatcher, enableConfigMgrWatcher, keepAwakeDuringUserEsp, notificationChannels, slaTargetSuccessRate, slaTargetMaxDurationMinutes, slaTargetAppInstallSuccessRate, slaNotifyOnSuccessRateBreach, slaSuccessRateNotifyThreshold, slaNotifyOnDurationBreach, @@ -1226,6 +1231,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode } setEnrollmentSummaryBrandingImageUrl(config.enrollmentSummaryBrandingImageUrl ?? ""); setEnrollmentSummaryLaunchRetrySeconds(config.enrollmentSummaryLaunchRetrySeconds ?? 120); setEnableRealmJoinWatcher(config.enableRealmJoinWatcher ?? false); + setEnableConfigMgrWatcher(config.enableConfigMgrWatcher ?? false); setKeepAwakeDuringUserEsp(config.keepAwakeDuringUserEsp ?? false); }, [config]); @@ -1607,6 +1613,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode } enrollmentSummaryBrandingImageUrl, setEnrollmentSummaryBrandingImageUrl, enrollmentSummaryLaunchRetrySeconds, setEnrollmentSummaryLaunchRetrySeconds, enableRealmJoinWatcher, setEnableRealmJoinWatcher, + enableConfigMgrWatcher, setEnableConfigMgrWatcher, keepAwakeDuringUserEsp, setKeepAwakeDuringUserEsp, handleSaveAgentSettings, handleResetAgentSettings, @@ -1690,7 +1697,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode } enableGeoLocation, enableTimezoneAutoSet, enableDoGroupIdAutoSet, enableImeMatchLog, enableGatherRuleDebugLog, logLevel, showScriptOutput, showEnrollmentSummary, enrollmentSummaryTimeoutSeconds, enrollmentSummaryBrandingImageUrl, enrollmentSummaryLaunchRetrySeconds, - enableRealmJoinWatcher, keepAwakeDuringUserEsp, + enableRealmJoinWatcher, enableConfigMgrWatcher, keepAwakeDuringUserEsp, handleSaveAgentSettings, handleResetAgentSettings, enableLocalAdminAnalyzer, localAdminAllowedAccounts, newAllowedAccount, enableSoftwareInventoryAnalyzer, enableIntegrityBypassAnalyzer, enableConsoleBypassDetection, diff --git a/src/Web/autopilot-monitor-web/app/settings/__tests__/sectionFieldMap.test.ts b/src/Web/autopilot-monitor-web/app/settings/__tests__/sectionFieldMap.test.ts index b8b4ce7f..e36ad05f 100644 --- a/src/Web/autopilot-monitor-web/app/settings/__tests__/sectionFieldMap.test.ts +++ b/src/Web/autopilot-monitor-web/app/settings/__tests__/sectionFieldMap.test.ts @@ -34,7 +34,7 @@ const EXPECTED_OWNED_FIELDS = [ "contactEmail", "companyName", // agentSettings "enablePerformanceCollector", "performanceCollectorIntervalSeconds", "helloWaitTimeoutSeconds", - "enableRealmJoinWatcher", + "enableRealmJoinWatcher", "enableConfigMgrWatcher", "selfDestructOnComplete", "keepLogFile", "rebootOnComplete", "rebootDelaySeconds", "enableGeoLocation", "enableTimezoneAutoSet", "enableDoGroupIdAutoSet", "keepAwakeDuringUserEsp", "enableImeMatchLog", "enableGatherRuleDebugLog", diff --git a/src/Web/autopilot-monitor-web/app/settings/agent/sections/SectionAgentSettings.tsx b/src/Web/autopilot-monitor-web/app/settings/agent/sections/SectionAgentSettings.tsx index bf4d77ee..35e7532c 100644 --- a/src/Web/autopilot-monitor-web/app/settings/agent/sections/SectionAgentSettings.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/agent/sections/SectionAgentSettings.tsx @@ -11,6 +11,7 @@ export function SectionAgentSettings() { performanceCollectorInterval, setPerformanceCollectorInterval, helloWaitTimeoutSeconds, setHelloWaitTimeoutSeconds, enableRealmJoinWatcher, setEnableRealmJoinWatcher, + enableConfigMgrWatcher, setEnableConfigMgrWatcher, selfDestructOnComplete, setSelfDestructOnComplete, keepLogFile, setKeepLogFile, rebootOnComplete, setRebootOnComplete, @@ -43,6 +44,8 @@ export function SectionAgentSettings() { setHelloWaitTimeoutSeconds={setHelloWaitTimeoutSeconds} enableRealmJoinWatcher={enableRealmJoinWatcher} setEnableRealmJoinWatcher={setEnableRealmJoinWatcher} + enableConfigMgrWatcher={enableConfigMgrWatcher} + setEnableConfigMgrWatcher={setEnableConfigMgrWatcher} selfDestructOnComplete={selfDestructOnComplete} setSelfDestructOnComplete={setSelfDestructOnComplete} keepLogFile={keepLogFile} diff --git a/src/Web/autopilot-monitor-web/app/settings/components/AgentSettingsSection.tsx b/src/Web/autopilot-monitor-web/app/settings/components/AgentSettingsSection.tsx index b12c1e23..74566211 100644 --- a/src/Web/autopilot-monitor-web/app/settings/components/AgentSettingsSection.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/components/AgentSettingsSection.tsx @@ -20,6 +20,8 @@ interface AgentSettingsSectionProps { setHelloWaitTimeoutSeconds: (value: number) => void; enableRealmJoinWatcher: boolean; setEnableRealmJoinWatcher: (value: boolean) => void; + enableConfigMgrWatcher: boolean; + setEnableConfigMgrWatcher: (value: boolean) => void; selfDestructOnComplete: boolean; setSelfDestructOnComplete: (value: boolean) => void; keepLogFile: boolean; @@ -68,6 +70,8 @@ export default function AgentSettingsSection({ setHelloWaitTimeoutSeconds, enableRealmJoinWatcher, setEnableRealmJoinWatcher, + enableConfigMgrWatcher, + setEnableConfigMgrWatcher, selfDestructOnComplete, setSelfDestructOnComplete, keepLogFile, @@ -423,6 +427,20 @@ export default function AgentSettingsSection({ + {/* ConfigMgr Watcher */} +
    +
    +

    ConfigMgr Watcher

    +

    Shows in Session Info whether the Configuration Manager client is installed, with its service state, version and site code. Off by default; enable for tenants that co-manage devices with Configuration Manager.

    +
    + +
    + {/* Hello Wait Timeout */}
    diff --git a/src/Web/autopilot-monitor-web/app/settings/sectionFieldMap.ts b/src/Web/autopilot-monitor-web/app/settings/sectionFieldMap.ts index 53d908bf..e728d4ec 100644 --- a/src/Web/autopilot-monitor-web/app/settings/sectionFieldMap.ts +++ b/src/Web/autopilot-monitor-web/app/settings/sectionFieldMap.ts @@ -67,6 +67,7 @@ export const SECTION_FIELD_MAP = { "performanceCollectorIntervalSeconds", "helloWaitTimeoutSeconds", "enableRealmJoinWatcher", + "enableConfigMgrWatcher", "selfDestructOnComplete", "keepLogFile", "rebootOnComplete", diff --git a/src/Web/autopilot-monitor-web/app/settings/types.ts b/src/Web/autopilot-monitor-web/app/settings/types.ts index 376cd977..50798754 100644 --- a/src/Web/autopilot-monitor-web/app/settings/types.ts +++ b/src/Web/autopilot-monitor-web/app/settings/types.ts @@ -130,6 +130,7 @@ export interface TenantConfiguration { enableSoftwareInventoryAnalyzer?: boolean; enableIntegrityBypassAnalyzer?: boolean; enableRealmJoinWatcher?: boolean; + enableConfigMgrWatcher?: boolean; keepAwakeDuringUserEsp?: boolean; enableConsoleBypassDetection?: boolean; // Bootstrap token diff --git a/src/Web/autopilot-monitor-web/lib/generated/shared-manifests.generated.ts b/src/Web/autopilot-monitor-web/lib/generated/shared-manifests.generated.ts index 5c1df7ce..fcf37130 100644 --- a/src/Web/autopilot-monitor-web/lib/generated/shared-manifests.generated.ts +++ b/src/Web/autopilot-monitor-web/lib/generated/shared-manifests.generated.ts @@ -199,6 +199,7 @@ export const SHARED_MANIFEST = { "enableSoftwareInventoryAnalyzer", "enableIntegrityBypassAnalyzer", "enableRealmJoinWatcher", + "enableConfigMgrWatcher", "keepAwakeDuringUserEsp", "enableConsoleBypassDetection", "localAdminAllowedAccountsJson", @@ -304,6 +305,7 @@ export const SHARED_MANIFEST = { "enableSoftwareInventoryAnalyzer": null, "enableIntegrityBypassAnalyzer": null, "enableRealmJoinWatcher": null, + "enableConfigMgrWatcher": null, "keepAwakeDuringUserEsp": null, "enableConsoleBypassDetection": null, "localAdminAllowedAccountsJson": null, @@ -446,6 +448,7 @@ export const SHARED_MANIFEST = { "enableSoftwareInventoryAnalyzer": false, "enableIntegrityBypassAnalyzer": true, "enableRealmJoinWatcher": false, + "enableConfigMgrWatcher": false, "keepAwakeDuringUserEsp": false, "enableConsoleBypassDetection": true } diff --git a/src/Web/autopilot-monitor-web/lib/generated/shared-manifests.json b/src/Web/autopilot-monitor-web/lib/generated/shared-manifests.json index 866bf64f..745a45de 100644 --- a/src/Web/autopilot-monitor-web/lib/generated/shared-manifests.json +++ b/src/Web/autopilot-monitor-web/lib/generated/shared-manifests.json @@ -195,6 +195,7 @@ "enableSoftwareInventoryAnalyzer", "enableIntegrityBypassAnalyzer", "enableRealmJoinWatcher", + "enableConfigMgrWatcher", "keepAwakeDuringUserEsp", "enableConsoleBypassDetection", "localAdminAllowedAccountsJson", @@ -300,6 +301,7 @@ "enableSoftwareInventoryAnalyzer": null, "enableIntegrityBypassAnalyzer": null, "enableRealmJoinWatcher": null, + "enableConfigMgrWatcher": null, "keepAwakeDuringUserEsp": null, "enableConsoleBypassDetection": null, "localAdminAllowedAccountsJson": null, @@ -442,6 +444,7 @@ "enableSoftwareInventoryAnalyzer": false, "enableIntegrityBypassAnalyzer": true, "enableRealmJoinWatcher": false, + "enableConfigMgrWatcher": false, "keepAwakeDuringUserEsp": false, "enableConsoleBypassDetection": true } @@ -2945,6 +2948,14 @@ }, "doc": "Whether to enable the RealmJoin watcher, which tracks RealmJoin deployment state and enrollment packages during provisioning. This is not an IAgentAnalyzer; the flag gates creation of the RealmJoin collector host in the agent runtime. Default: false (opt-in; most tenants do not deploy via RealmJoin)." }, + { + "name": "enableConfigMgrWatcher", + "type": { + "kind": "primitive", + "name": "boolean" + }, + "doc": "Whether to enable the ConfigMgr watcher, which reports whether the Configuration Manager client is installed (service state, version, site code). This is not an IAgentAnalyzer; the flag gates creation of the ConfigMgr client host in the agent runtime. Default: false (opt-in; most tenants do not co-manage with ConfigMgr)." + }, { "name": "keepAwakeDuringUserEsp", "type": { @@ -24648,6 +24659,16 @@ "optional": true, "doc": "Whether the RealmJoin watcher is enabled for this tenant's devices. RealmJoin enrollment-package tracking is off by default; enable only for tenants that deploy via RealmJoin. null = use agent default (false)." }, + { + "name": "enableConfigMgrWatcher", + "type": { + "kind": "primitive", + "name": "boolean", + "nullable": true + }, + "optional": true, + "doc": "Whether the ConfigMgr watcher is enabled for this tenant's devices: the agent reports whether the Configuration Manager client is installed (service state, version, site code). Off by default; enable for tenants that co-manage with ConfigMgr. null = use agent default (false)." + }, { "name": "keepAwakeDuringUserEsp", "type": { diff --git a/src/Web/autopilot-monitor-web/lib/generated/wire-types.generated.ts b/src/Web/autopilot-monitor-web/lib/generated/wire-types.generated.ts index 94ff09ec..60b85a5a 100644 --- a/src/Web/autopilot-monitor-web/lib/generated/wire-types.generated.ts +++ b/src/Web/autopilot-monitor-web/lib/generated/wire-types.generated.ts @@ -421,6 +421,8 @@ export interface AnalyzerConfiguration { enableIntegrityBypassAnalyzer: boolean; /** Whether to enable the RealmJoin watcher, which tracks RealmJoin deployment state and enrollment packages during provisioning. This is not an IAgentAnalyzer; the flag gates creation of the RealmJoin collector host in the agent runtime. Default: false (opt-in; most tenants do not deploy via RealmJoin). */ enableRealmJoinWatcher: boolean; + /** Whether to enable the ConfigMgr watcher, which reports whether the Configuration Manager client is installed (service state, version, site code). This is not an IAgentAnalyzer; the flag gates creation of the ConfigMgr client host in the agent runtime. Default: false (opt-in; most tenants do not co-manage with ConfigMgr). */ + enableConfigMgrWatcher: boolean; /** Whether to keep the device awake (system + display) during the User-ESP (AccountSetup) phase so idle standby/sleep cannot stall app installs or account provisioning. This is not an IAgentAnalyzer; the flag gates creation of the UserEspKeepAwake host in the agent runtime. Reboots are unaffected (the hold only resets idle timers). Default: false (opt-in per tenant). */ keepAwakeDuringUserEsp: boolean; /** Whether to detect an interactive console opened during enrollment — the classic Shift+F10 OOBE bypass. Gates BOTH the live ConsoleBypass host (an interactive-session cmd.exe with a bare, non-scripted command line; stopped on real-user desktop arrival) and the startup ConsolePrefetchScanner (CMD.EXE-*.pf ran after boot). Detection is best-effort, not gapless — a press before the agent installs is only coarsely covered by the prefetch artifact. Security-sensitive; not an IAgentAnalyzer toggle in the usual sense (it also gates a collector host). Default: true (opt-out per tenant — a Shift+F10 console during enrollment is a finding worth surfacing by default; tenants that knowingly use Shift+F10 for support can disable it). */ @@ -5359,6 +5361,8 @@ export interface TenantConfiguration { enableIntegrityBypassAnalyzer?: boolean | null; /** Whether the RealmJoin watcher is enabled for this tenant's devices. RealmJoin enrollment-package tracking is off by default; enable only for tenants that deploy via RealmJoin. null = use agent default (false). */ enableRealmJoinWatcher?: boolean | null; + /** Whether the ConfigMgr watcher is enabled for this tenant's devices: the agent reports whether the Configuration Manager client is installed (service state, version, site code). Off by default; enable for tenants that co-manage with ConfigMgr. null = use agent default (false). */ + enableConfigMgrWatcher?: boolean | null; /** Whether to keep the device awake during the User-ESP (AccountSetup) phase for this tenant's devices. Prevents idle standby/sleep from stalling app installs / account provisioning; reboots are unaffected. Off by default. null = use agent default (false). */ keepAwakeDuringUserEsp?: boolean | null; /** Whether to detect a SYSTEM console opened during enrollment (Shift+F10 OOBE bypass) for this tenant's devices. Gates the live ConsoleBypass watcher + the startup prefetch scanner. On by default (opt-out); tenants that knowingly use Shift+F10 for support can disable it. null = use agent default (true). */