From 6deb2f98456597c4ccbe6a5ec247a2c1771aa790 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Tue, 24 Feb 2026 13:19:05 +0100 Subject: [PATCH] fix ANALYZE-APP-010: correlate app regression by appId using new source type The previous rule incorrectly matched any app_install_completed event against any app_install_failed event without correlating by appId. Changes: - RuleEngine: add app_state_regression source that finds apps where app_install_completed is followed by app_install_failed for the same appId, with optional DataField filter on the failed event; returns appId, appName, both event details and errorDetail as evidence - AnalyzeRule: document new app_state_regression source in comment - ANALYZE-APP-010: rewrite to use single app_state_regression condition (IME-ERROR-REPORT filter) + enrollment_complete check; evidence now contains the exact app name and id that regressed Co-Authored-By: Claude Sonnet 4.6 --- .../Services/BuiltInAnalyzeRules.cs | 63 ++++++++++------- .../Services/RuleEngine.cs | 69 +++++++++++++++++++ .../Models/AnalyzeRule.cs | 2 +- 3 files changed, 109 insertions(+), 25 deletions(-) diff --git a/src/Backend/AutopilotMonitor.Functions/Services/BuiltInAnalyzeRules.cs b/src/Backend/AutopilotMonitor.Functions/Services/BuiltInAnalyzeRules.cs index c70ec8a2..154961cb 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/BuiltInAnalyzeRules.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/BuiltInAnalyzeRules.cs @@ -568,51 +568,66 @@ namespace AutopilotMonitor.Functions.Services /// /// Detects when an app was initially reported as successfully installed but then - /// transitioned back to an error state (IME-ERROR-REPORT), while enrollment completed successfully. + /// transitioned back to an error state (IME-ERROR-REPORT), correlated by appId. + /// Uses the app_state_regression source which matches the exact same app across both events. /// This is a silent failure that can leave apps broken post-enrollment. /// private static AnalyzeRule CreateAppPostSuccessFailureRule() => new AnalyzeRule { RuleId = "ANALYZE-APP-010", Title = "App Reverted to Error After Successful Install", - Description = "Detects when an app was marked as successfully installed but subsequently reported an error state, even though enrollment completed successfully. The app may be broken post-enrollment.", + Description = "Detects when a specific app (matched by appId) was first reported as successfully installed but subsequently reported an IME enforcement error — while enrollment completed successfully. The app may be broken post-enrollment.", Severity = "warning", Category = "apps", Trigger = "correlation", - BaseConfidence = 65, + BaseConfidence = 75, Conditions = new List { - new RuleCondition { Signal = "app_initially_succeeded", Source = "event_type", EventType = "app_install_completed", Operator = "exists", Value = "", Required = true }, - new RuleCondition { Signal = "app_post_success_failure", Source = "event_type", EventType = "app_install_failed", DataField = "errorPatternId", Operator = "equals", Value = "IME-ERROR-REPORT", Required = true }, - new RuleCondition { Signal = "enrollment_succeeded", Source = "event_type", EventType = "enrollment_complete", Operator = "exists", Value = "", Required = true } + // Single condition: finds apps where completed -> failed regression exists for the same appId, + // and the failed event has errorPatternId = IME-ERROR-REPORT + new RuleCondition + { + Signal = "app_regression", + Source = "app_state_regression", + DataField = "errorPatternId", + Operator = "equals", + Value = "IME-ERROR-REPORT", + Required = true + }, + new RuleCondition + { + Signal = "enrollment_succeeded", + Source = "event_type", + EventType = "enrollment_complete", + Operator = "exists", + Value = "", + Required = true + } }, - ConfidenceFactors = new List - { - new ConfidenceFactor { Signal = "enforcement_state_changed", Condition = "exists", Weight = 20 }, - new ConfidenceFactor { Signal = "error_code_present", Condition = "exists", Weight = 10 } - }, - ConfidenceThreshold = 60, - Explanation = @"An app was initially reported as **successfully installed** (`app_install_completed`) but then transitioned back to an **Error** state via an `IME-ERROR-REPORT` event — while the overall enrollment completed successfully. + ConfidenceThreshold = 70, + Explanation = @"An app was initially reported as **successfully installed** (`app_install_completed`) but the IME (Intune Management Extension) subsequently reported an **Error** state for the same app via an `IME-ERROR-REPORT` event — while the overall enrollment completed successfully. -This pattern typically occurs when: -- The IME (Intune Management Extension) re-evaluates enforcement state after the initial install report and detects a failure (e.g., `EnforcementState` changed from `Success` → `Error`) -- A post-install detection check fails, causing the enforcement error code to be updated (e.g., error `-2016345060`) -- The app may appear installed in the ESP but is actually in a broken or partially-deployed state on the device +This pattern occurs when the IME re-evaluates enforcement state after the initial install report and detects a failure, e.g. `EnforcementState` transitions from `Success` → `Error`. The affected app and its details are captured in the matched evidence. + +Common causes: +- A post-install detection check fails (detection rule mismatch) +- The enforcement error code `-2016345060` (0x87D13B9C) indicates the app state diverged from expected +- The app may appear installed in the ESP summary but is in an error state on the device **The enrollment succeeded, but the affected app requires attention.**", Remediation = new List { new RemediationStep { - Title = "Investigate the app's post-install state", + Title = "Investigate the app's post-install enforcement state", Steps = new List { - "Check the Intune portal: Device > Apps — look for the app and confirm its current enforcement state", + "Note the appId and appName from the rule finding — these identify the exact affected app", + "Check the Intune portal: Devices > [Device] > Apps — confirm the current enforcement state of the app", "Review IME logs on the device: C:\\ProgramData\\Microsoft\\IntuneManagementExtension\\Logs\\IntuneManagementExtension.log", - "Search the log for the appId or appName and look for 'EnforcementState' transitions near the enrollment time", - "Check the EnforcementErrorCode — error -2016345060 (0x87D13B9C) typically indicates a detection failure after install", - "Verify the app's detection rule in Intune is correct and matches the installed state", - "Trigger a manual sync on the device (Company Portal or Settings > Accounts > Access work or school > Sync) to force re-evaluation" + "Search the log for the appId and look for 'EnforcementState' transitions (Success → Error) near the enrollment time", + "Error -2016345060 (0x87D13B9C) typically indicates a detection failure after install — verify the app's detection rule in Intune", + "Trigger a manual sync (Company Portal or Settings > Accounts > Access work or school > Sync) to force re-evaluation" } }, new RemediationStep @@ -620,7 +635,7 @@ This pattern typically occurs when: Title = "Fix detection or installation issues", Steps = new List { - "If detection fails: correct the detection rule (file path, registry key, or script) in the Intune app configuration", + "If detection fails: correct the detection rule (file path, registry key, or PowerShell script) in the Intune app configuration", "If the app itself is broken: redeploy or reassign the app to trigger a fresh install", "Consider adding a post-install validation script to catch this scenario proactively" } diff --git a/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.cs b/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.cs index 9343cc2c..8e3b1137 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.cs @@ -161,6 +161,9 @@ namespace AutopilotMonitor.Functions.Services case "app_install_duration": return EvaluateAppInstallDurationCondition(condition, events); + case "app_state_regression": + return EvaluateAppStateRegressionCondition(condition, events); + default: return (false, "unknown source"); } @@ -512,5 +515,71 @@ namespace AutopilotMonitor.Functions.Services return (false, "no app install duration matched"); } + + /// + /// Detects apps that regressed: first seen as app_install_completed, then as app_install_failed, + /// correlated by appId. Optionally filters the failed event by a DataField/Operator/Value condition. + /// Returns evidence with appId, appName, and both event details. + /// + private (bool matched, object evidence) EvaluateAppStateRegressionCondition(RuleCondition condition, List events) + { + var sortedEvents = events.OrderBy(e => e.Timestamp).ThenBy(e => e.Sequence).ToList(); + + var completedEvents = sortedEvents + .Where(e => string.Equals(e.EventType, "app_install_completed", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var failedEvents = sortedEvents + .Where(e => string.Equals(e.EventType, "app_install_failed", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (!completedEvents.Any() || !failedEvents.Any()) + return (false, "no completed/failed event pair found"); + + foreach (var failedEvent in failedEvents) + { + // Apply optional DataField filter on the failed event + if (!string.IsNullOrEmpty(condition.DataField) && !string.IsNullOrEmpty(condition.Operator)) + { + var fieldValue = GetDataFieldValue(failedEvent, condition.DataField); + if (fieldValue == null || !MatchesOperator(fieldValue, condition.Operator, condition.Value ?? "")) + continue; + } + + var failedAppId = GetDataFieldValue(failedEvent, "appId"); + var failedAppName = GetDataFieldValue(failedEvent, "appName"); + var appKey = !string.IsNullOrWhiteSpace(failedAppId) ? failedAppId : failedAppName; + + if (string.IsNullOrWhiteSpace(appKey)) + continue; + + // Find a completed event for the same app that occurred before the failed event + var matchingCompleted = completedEvents.LastOrDefault(c => + c.Timestamp <= failedEvent.Timestamp && + string.Equals( + GetDataFieldValue(c, "appId") ?? GetDataFieldValue(c, "appName"), + appKey, + StringComparison.OrdinalIgnoreCase)); + + if (matchingCompleted == null) + continue; + + return (true, new Dictionary + { + ["appId"] = failedAppId ?? string.Empty, + ["appName"] = failedAppName ?? appKey, + ["completedEventId"] = matchingCompleted.EventId ?? string.Empty, + ["completedSequence"] = matchingCompleted.Sequence, + ["completedTimestamp"] = matchingCompleted.Timestamp, + ["failedEventId"] = failedEvent.EventId ?? string.Empty, + ["failedSequence"] = failedEvent.Sequence, + ["failedTimestamp"] = failedEvent.Timestamp, + ["errorPatternId"] = GetDataFieldValue(failedEvent, "errorPatternId") ?? string.Empty, + ["errorDetail"] = GetDataFieldValue(failedEvent, "errorDetail") ?? failedEvent.Message ?? string.Empty + }); + } + + return (false, "no app found with completed-then-failed regression"); + } } } diff --git a/src/Shared/AutopilotMonitor.Shared/Models/AnalyzeRule.cs b/src/Shared/AutopilotMonitor.Shared/Models/AnalyzeRule.cs index a2de6fcd..e18a56ed 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/AnalyzeRule.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/AnalyzeRule.cs @@ -134,7 +134,7 @@ namespace AutopilotMonitor.Shared.Models public string Signal { get; set; } /// - /// Source of the signal: "event_type", "event_data", "phase_duration", "event_count", "app_install_duration" + /// Source of the signal: "event_type", "event_data", "phase_duration", "event_count", "app_install_duration", "app_state_regression" /// public string Source { get; set; }