From e14675c84738206e9bfaa2b03159b87ab415ee6f Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Mon, 16 Feb 2026 12:29:03 +0100 Subject: [PATCH] fixed constants fix for analyze rule reboot loop added completed timestamp to progress portal longer os age default in bootstrapper removed legacy filed name in events --- .../Bootstrap/Install-AutopilotMonitor.ps1 | 6 +- .../Monitoring/Tracking/AppPackageState.cs | 1 - .../AutopilotMonitor.Agent/buildcounter.txt | 2 +- .../Functions/GetAgentConfigFunction.cs | 2 +- .../Functions/IngestEventsFunction.cs | 3 - .../Services/BuiltInAnalyzeRules.cs | 8 +- .../Services/RuleEngine.cs | 37 +++++++- .../AutopilotMonitor.Shared/Constants.cs | 5 ++ .../Models/AgentConfigResponse.cs | 2 +- .../app/landing/page.tsx | 90 ++++++++++++++----- .../app/progress/page.tsx | 9 ++ 11 files changed, 127 insertions(+), 38 deletions(-) diff --git a/scripts/Bootstrap/Install-AutopilotMonitor.ps1 b/scripts/Bootstrap/Install-AutopilotMonitor.ps1 index e6f121f1..59788c3f 100644 --- a/scripts/Bootstrap/Install-AutopilotMonitor.ps1 +++ b/scripts/Bootstrap/Install-AutopilotMonitor.ps1 @@ -31,17 +31,19 @@ param( [string]$AgentDownloadUrl = "https://autopilotmonitor.blob.core.windows.net/agent/AutopilotMonitor-Agent.zip", [Parameter(Mandatory = $false)] - [int]$MaxOsAgeMinutes = 60 + [int]$MaxOsAgeMinutes = 120 ) # Configuration - Everything in ProgramData for easy cleanup $AgentBasePath = "$env:ProgramData\AutopilotMonitor" $AgentBinPath = "$AgentBasePath\Agent" -$LogFile = "$AgentBasePath\Bootstrap.log" +$AgentLogPath = "$AgentBasePath\Logs" +$LogFile = "$AgentLogPath\Bootstrap.log" # Ensure directories exist New-Item -Path $AgentBasePath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null New-Item -Path $AgentBinPath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null +New-Item -Path $AgentLogPath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null function Write-Log { param([string]$Message) diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/AppPackageState.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/AppPackageState.cs index 37ead45f..34fc5728 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/AppPackageState.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/AppPackageState.cs @@ -347,7 +347,6 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking var data = new Dictionary { { "appId", Id }, - { "name", Name ?? Id }, { "appName", Name ?? Id }, { "state", InstallationState.ToString() }, { "intent", Intent.ToString() }, diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index 64bb6b74..e85087af 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -30 +31 diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/GetAgentConfigFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/GetAgentConfigFunction.cs index 60dc97fb..f9f6be09 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/GetAgentConfigFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/GetAgentConfigFunction.cs @@ -95,7 +95,7 @@ namespace AutopilotMonitor.Functions.Functions CleanupOnExit = false, SelfDestructOnComplete = false, KeepLogFile = true, - ImeMatchLogPath = @"C:\ProgramData\AutopilotMonitor\Logs\ime_pattern_matches.log", + Collectors = collectors, GatherRules = gatherRules, ImeLogPatterns = imeLogPatterns, diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/IngestEventsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/IngestEventsFunction.cs index 8b68e26d..c7851ab7 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/IngestEventsFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/IngestEventsFunction.cs @@ -409,10 +409,7 @@ namespace AutopilotMonitor.Functions.Functions if (!isRelevant) return; - // Agent sends "appName" (and also "name") in the Data dictionary var appName = evt.Data?.ContainsKey("appName") == true ? evt.Data["appName"]?.ToString() : null; - if (string.IsNullOrEmpty(appName)) - appName = evt.Data?.ContainsKey("name") == true ? evt.Data["name"]?.ToString() : null; if (string.IsNullOrEmpty(appName)) return; if (!summaries.TryGetValue(appName, out var summary)) diff --git a/src/Backend/AutopilotMonitor.Functions/Services/BuiltInAnalyzeRules.cs b/src/Backend/AutopilotMonitor.Functions/Services/BuiltInAnalyzeRules.cs index 9dea6871..f94eaf25 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/BuiltInAnalyzeRules.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/BuiltInAnalyzeRules.cs @@ -181,18 +181,18 @@ namespace AutopilotMonitor.Functions.Services { RuleId = "ANALYZE-APP-006", Title = "Reboot Required Loop", - Description = "Detects when an app installation enters a reboot-required loop.", + Description = "Detects when the same app installation is attempted 3 or more times, indicating a reboot-required loop.", Severity = "high", Category = "apps", BaseConfidence = 70, Conditions = new List { - new RuleCondition { Signal = "reboot_loop", Source = "event_count", EventType = "app_install_started", Operator = "count_gte", Value = "3", Required = true } + new RuleCondition { Signal = "reboot_loop", Source = "event_count", EventType = "app_install_started", DataField = "appId", Operator = "count_per_group_gte", Value = "3", Required = true } }, - Explanation = "An app appears to be in a reboot loop - it has been attempted multiple times. This may indicate that the app requires a reboot that is not being honored by the ESP.", + Explanation = "The same app has been installed 3 or more times during this enrollment. This typically indicates the app requires a reboot between install attempts, but the ESP is not honoring the reboot request, causing an install-detect-retry loop.", Remediation = new List { - new RemediationStep { Title = "Fix reboot handling", Steps = new List { "Check if the app's return code mapping includes reboot codes", "Verify ESP reboot behavior settings", "Consider setting the app to 'Hard reboot' return code" } } + new RemediationStep { Title = "Fix reboot handling", Steps = new List { "Check if the app's return code mapping includes reboot codes (3010, 1641)", "Verify ESP reboot behavior settings", "Consider setting the app to 'Hard reboot' return code in Intune" } } }, Tags = new[] { "apps", "reboot", "loop" } }; diff --git a/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.cs b/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.cs index bc31baf2..2db27c50 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.cs @@ -233,8 +233,39 @@ namespace AutopilotMonitor.Functions.Services private (bool matched, object evidence) EvaluateEventCountCondition(RuleCondition condition, List events) { var matchingEvents = events.Where(e => MatchesEventType(e, condition.EventType)).ToList(); - var count = matchingEvents.Count; + // count_per_group_gte: group by DataField value (e.g. appId), fire if any group >= threshold + if (condition.Operator == "count_per_group_gte" && !string.IsNullOrEmpty(condition.DataField) && int.TryParse(condition.Value, out var groupThreshold)) + { + var groups = matchingEvents + .Select(e => new { Event = e, Key = GetDataFieldValue(e, condition.DataField) }) + .Where(x => !string.IsNullOrEmpty(x.Key)) + .GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() >= groupThreshold) + .ToList(); + + if (groups.Any()) + { + var worst = groups.OrderByDescending(g => g.Count()).First(); + var firstEvent = worst.First().Event; + var appName = GetDataFieldValue(firstEvent, "appName") ?? worst.Key; + return (true, new Dictionary + { + ["eventId"] = firstEvent.EventId, + ["sequence"] = firstEvent.Sequence, + ["timestamp"] = firstEvent.Timestamp, + ["groupKey"] = worst.Key, + ["appName"] = appName, + ["count"] = worst.Count(), + ["threshold"] = groupThreshold + }); + } + + return (false, new Dictionary { ["totalEvents"] = matchingEvents.Count, ["distinctApps"] = matchingEvents.Select(e => GetDataFieldValue(e, condition.DataField)).Where(k => k != null).Distinct(StringComparer.OrdinalIgnoreCase).Count() }); + } + + // count_gte: global count across all matching events + var count = matchingEvents.Count; if (condition.Operator == "count_gte" && int.TryParse(condition.Value, out var threshold)) { if (count >= threshold) @@ -437,7 +468,7 @@ namespace AutopilotMonitor.Functions.Services foreach (var completionEvent in completionEvents) { var appId = GetDataFieldValue(completionEvent, "appId"); - var appName = GetDataFieldValue(completionEvent, "appName") ?? GetDataFieldValue(completionEvent, "name"); + var appName = GetDataFieldValue(completionEvent, "appName"); var appKey = !string.IsNullOrWhiteSpace(appId) ? appId : appName; if (string.IsNullOrWhiteSpace(appKey)) @@ -447,7 +478,7 @@ namespace AutopilotMonitor.Functions.Services (string.Equals(e.EventType, "app_install_started", StringComparison.OrdinalIgnoreCase) || string.Equals(e.EventType, "app_install_start", StringComparison.OrdinalIgnoreCase)) && e.Timestamp <= completionEvent.Timestamp && - string.Equals(GetDataFieldValue(e, "appId") ?? GetDataFieldValue(e, "appName") ?? GetDataFieldValue(e, "name"), appKey, StringComparison.OrdinalIgnoreCase)); + string.Equals(GetDataFieldValue(e, "appId") ?? GetDataFieldValue(e, "appName"), appKey, StringComparison.OrdinalIgnoreCase)); if (startEvent == null) continue; diff --git a/src/Shared/AutopilotMonitor.Shared/Constants.cs b/src/Shared/AutopilotMonitor.Shared/Constants.cs index ae64430b..29ceb6ba 100644 --- a/src/Shared/AutopilotMonitor.Shared/Constants.cs +++ b/src/Shared/AutopilotMonitor.Shared/Constants.cs @@ -29,6 +29,11 @@ namespace AutopilotMonitor.Shared /// public const string StateDirectory = @"%ProgramData%\AutopilotMonitor\State"; + /// + /// Default path for the IME pattern match log file (debugging/diagnostics) + /// + public const string ImeMatchLogPath = @"%ProgramData%\AutopilotMonitor\Logs\ime_pattern_matches.log"; + /// /// Scheduled Task name used to run the agent as SYSTEM /// diff --git a/src/Shared/AutopilotMonitor.Shared/Models/AgentConfigResponse.cs b/src/Shared/AutopilotMonitor.Shared/Models/AgentConfigResponse.cs index 4615da04..fb8b72d3 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/AgentConfigResponse.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/AgentConfigResponse.cs @@ -37,7 +37,7 @@ namespace AutopilotMonitor.Shared.Models /// /// Optional path to IME pattern match log file. /// - public string ImeMatchLogPath { get; set; } = @"C:\ProgramData\AutopilotMonitor\Logs\ime_pattern_matches.log"; + public string ImeMatchLogPath { get; set; } = Constants.ImeMatchLogPath; public CollectorConfiguration Collectors { get; set; } diff --git a/src/Web/autopilot-monitor-web/app/landing/page.tsx b/src/Web/autopilot-monitor-web/app/landing/page.tsx index 882f07d0..1dd13e64 100644 --- a/src/Web/autopilot-monitor-web/app/landing/page.tsx +++ b/src/Web/autopilot-monitor-web/app/landing/page.tsx @@ -73,7 +73,7 @@ export default function LandingPage() {

Real-time insights, intelligent troubleshooting, and comprehensive analytics for your Autopilot deployments. - Monitor every phase, track performance, and resolve issues faster than ever before. + Monitor every phase, run customizable analyze rules, and resolve issues faster than ever before.

)}