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; }