diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/GatherRuleExecutor.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/GatherRuleExecutor.cs
index bda5aeb5..86a8195e 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/GatherRuleExecutor.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/GatherRuleExecutor.cs
@@ -16,8 +16,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
{
///
/// Executes gather rules received from the backend API
- /// Supports registry, eventlog, wmi, file, command (allowlisted), and logparser collector types
- ///
+ /// Supports registry, eventlog, wmi, file, command_allowlisted, and logparser collector types
+ /// NOTE: "command" is accepted as a legacy alias for "command_allowlisted" - both enforce the allowlist
+ ///
public class GatherRuleExecutor : IDisposable
{
private readonly AgentLogger _logger;
@@ -167,7 +168,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
result = ExecuteWmiRule(rule);
break;
- case "command":
+ case "command_allowlisted":
+ case "command": // legacy alias - both enforce the allowlist
result = ExecuteCommandRule(rule);
break;
diff --git a/src/Backend/AutopilotMonitor.Functions/Services/BuiltInAnalyzeRules.cs b/src/Backend/AutopilotMonitor.Functions/Services/BuiltInAnalyzeRules.cs
index 58063b34..3b739288 100644
--- a/src/Backend/AutopilotMonitor.Functions/Services/BuiltInAnalyzeRules.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Services/BuiltInAnalyzeRules.cs
@@ -29,9 +29,16 @@ namespace AutopilotMonitor.Functions.Services
CreateDeviceBitlockerEscrowRule(),
CreateDeviceSecureBootRule(),
+ // ===== DEVICE RULES (continued) =====
+ CreateDeviceAadJoinNotDetectedRule(),
+
+ // ===== APP RULES (continued) =====
+ CreateAppTrackingSummaryErrorsRule(),
+
// ===== CORRELATION RULES (cross-event analysis) =====
CreateCorrelationNetworkCausedInstallFailureRule(),
CreateCorrelationDiskSpaceCausedInstallFailureRule(),
+ CreateCorrelationProxyCausedDownloadFailureRule(),
};
}
@@ -151,7 +158,7 @@ namespace AutopilotMonitor.Functions.Services
BaseConfidence = 70,
Conditions = new List
{
- new RuleCondition { Signal = "reboot_loop", Source = "event_count", EventType = "app_install_start", Operator = "count_gte", Value = "3", Required = true }
+ new RuleCondition { Signal = "reboot_loop", Source = "event_count", EventType = "app_install_started", Operator = "count_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.",
Remediation = new List
@@ -173,7 +180,7 @@ namespace AutopilotMonitor.Functions.Services
BaseConfidence = 60,
Conditions = new List
{
- new RuleCondition { Signal = "esp_stalled", Source = "phase_duration", EventType = "phase_changed", DataField = "currentPhase", Operator = "equals", Value = "EspDeviceSetup", Required = true }
+ new RuleCondition { Signal = "esp_stalled", Source = "phase_duration", EventType = "esp_phase_changed", DataField = "espPhase", Operator = "equals", Value = "DeviceSetup", Required = true }
},
ConfidenceFactors = new List
{
@@ -259,7 +266,7 @@ namespace AutopilotMonitor.Functions.Services
BaseConfidence = 60,
Conditions = new List
{
- new RuleCondition { Signal = "secureboot_disabled", Source = "event_type", EventType = "gather_secureboot_status", DataField = "UEFISecureBootEnabled", Operator = "equals", Value = "0", Required = true }
+ new RuleCondition { Signal = "secureboot_disabled", Source = "event_type", EventType = "secureboot_status", DataField = "uefiSecureBootEnabled", Operator = "equals", Value = "False", Required = true }
},
Explanation = "Secure Boot is disabled on this device. Some compliance policies require Secure Boot to be enabled.",
Remediation = new List
@@ -272,6 +279,61 @@ namespace AutopilotMonitor.Functions.Services
// ===== CORRELATION RULES =====
// These combine signals from multiple event types for root-cause analysis
+ private static AnalyzeRule CreateDeviceAadJoinNotDetectedRule() => new AnalyzeRule
+ {
+ RuleId = "ANALYZE-DEV-004",
+ Title = "Azure AD Join Not Detected",
+ Description = "Detects when the device is not Azure AD joined at the time of enrollment.",
+ Severity = "critical",
+ Category = "device",
+ BaseConfidence = 85,
+ Conditions = new List
+ {
+ new RuleCondition { Signal = "aad_not_joined", Source = "event_type", EventType = "aad_join_status", DataField = "joinType", Operator = "equals", Value = "Not Joined", Required = true }
+ },
+ Explanation = "The device was not Azure AD joined at the start of enrollment. Autopilot requires the device to join Azure AD. This will prevent Intune enrollment and policy delivery from succeeding.",
+ Remediation = new List
+ {
+ new RemediationStep { Title = "Verify Azure AD connectivity", Steps = new List {
+ "Ensure the device can reach login.microsoftonline.com",
+ "Check that the Autopilot profile's tenant matches the signing-in user's tenant",
+ "Verify the device is not already joined to an on-premises domain (Hybrid Join may be required)"
+ }}
+ },
+ Tags = new[] { "device", "aad", "join", "critical" }
+ };
+
+ private static AnalyzeRule CreateAppTrackingSummaryErrorsRule() => new AnalyzeRule
+ {
+ RuleId = "ANALYZE-APP-007",
+ Title = "Multiple App Installation Failures",
+ Description = "Detects when the app tracking summary reports multiple failed app installations.",
+ Severity = "high",
+ Category = "apps",
+ BaseConfidence = 75,
+ Conditions = new List
+ {
+ new RuleCondition { Signal = "summary_errors", Source = "event_type", EventType = "app_tracking_summary", DataField = "errorCount", Operator = "gte", Value = "2", Required = true }
+ },
+ ConfidenceFactors = new List
+ {
+ new ConfidenceFactor { Signal = "enrollment_failed", Condition = "exists", Weight = 15 }
+ },
+ Explanation = "The app tracking summary reports 2 or more failed app installations during enrollment. This suggests a systemic issue rather than an isolated app failure.",
+ Remediation = new List
+ {
+ new RemediationStep { Title = "Review failed apps", Steps = new List {
+ "Check the individual app_install_failed events for specific error codes",
+ "Look for a common pattern (all MSI apps, all large apps, all from a specific publisher)",
+ "Consider whether a shared prerequisite or dependency is failing"
+ }}
+ },
+ Tags = new[] { "apps", "summary", "multiple-failures" }
+ };
+
+ // ===== CORRELATION RULES =====
+ // These combine signals from multiple event types for root-cause analysis
+
private static AnalyzeRule CreateCorrelationNetworkCausedInstallFailureRule() => new AnalyzeRule
{
RuleId = "ANALYZE-CORR-003",
@@ -347,5 +409,44 @@ namespace AutopilotMonitor.Functions.Services
},
Tags = new[] { "correlation", "apps", "disk-space", "performance", "root-cause" }
};
+
+ private static AnalyzeRule CreateCorrelationProxyCausedDownloadFailureRule() => new AnalyzeRule
+ {
+ RuleId = "ANALYZE-CORR-005",
+ Title = "Proxy Configuration Causing Download Failure",
+ Description = "Correlates an active proxy configuration with app download failures, suggesting the proxy is blocking or throttling Intune content delivery.",
+ Severity = "high",
+ Category = "apps",
+ Trigger = "correlation",
+ BaseConfidence = 60,
+ Conditions = new List
+ {
+ new RuleCondition { Signal = "proxy_active", Source = "event_type", EventType = "proxy_configuration", DataField = "proxyType", Operator = "regex", Value = "Proxy|PAC", Required = true },
+ new RuleCondition { Signal = "app_failed", Source = "event_type", EventType = "app_install_failed", Operator = "exists", Value = "", Required = true }
+ },
+ ConfidenceFactors = new List
+ {
+ new ConfidenceFactor { Signal = "download_timeout", Condition = "exists", Weight = 20 },
+ new ConfidenceFactor { Signal = "multiple_failures", Condition = "count >= 2", Weight = 15 },
+ new ConfidenceFactor { Signal = "enrollment_failed", Condition = "exists", Weight = 10 }
+ },
+ ConfidenceThreshold = 50,
+ Explanation = "A proxy (explicit or PAC-based) is configured on this device and app installations failed. Proxies can block or throttle downloads from *.delivery.mp.microsoft.com and other Intune CDN endpoints.\n\nThis is a common root cause when apps fail consistently on proxy-connected corporate networks.",
+ Remediation = new List
+ {
+ new RemediationStep { Title = "Verify proxy bypass for Intune endpoints", Steps = new List {
+ "Add *.delivery.mp.microsoft.com to proxy bypass list",
+ "Add *.do.dsp.mp.microsoft.com (Delivery Optimization) to bypass list",
+ "Add login.microsoftonline.com and *.manage.microsoft.com to bypass list",
+ "Verify the PAC file is reachable at enrollment time (before user login)"
+ }},
+ new RemediationStep { Title = "Check proxy authentication", Steps = new List {
+ "Ensure the proxy does not require user authentication during OOBE/device phase",
+ "Consider using machine certificate-based proxy authentication",
+ "Test with proxy temporarily bypassed to confirm it is the root cause"
+ }}
+ },
+ Tags = new[] { "correlation", "apps", "proxy", "network", "download", "root-cause" }
+ };
}
}
diff --git a/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.cs b/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.cs
index 464311bd..75340809 100644
--- a/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.cs
@@ -221,7 +221,7 @@ namespace AutopilotMonitor.Functions.Services
{
// Find phase change events to calculate phase duration
var phaseEvents = events
- .Where(e => e.EventType == "phase_changed")
+ .Where(e => e.EventType == "esp_phase_changed")
.OrderBy(e => e.Timestamp)
.ToList();
@@ -233,8 +233,8 @@ namespace AutopilotMonitor.Functions.Services
for (int i = 0; i < phaseEvents.Count; i++)
{
var phaseData = phaseEvents[i].Data;
- var currentPhase = phaseData?.ContainsKey("currentPhase") == true
- ? phaseData["currentPhase"]?.ToString()
+ var currentPhase = phaseData?.ContainsKey("espPhase") == true
+ ? phaseData["espPhase"]?.ToString()
: null;
if (currentPhase == targetPhase)
diff --git a/src/Shared/AutopilotMonitor.Shared/Models/GatherRule.cs b/src/Shared/AutopilotMonitor.Shared/Models/GatherRule.cs
index c5ab1aaa..a218f6a3 100644
--- a/src/Shared/AutopilotMonitor.Shared/Models/GatherRule.cs
+++ b/src/Shared/AutopilotMonitor.Shared/Models/GatherRule.cs
@@ -59,29 +59,38 @@ namespace AutopilotMonitor.Shared.Models
// ===== WHAT TO COLLECT =====
///
- /// Type of data collection: "registry", "eventlog", "wmi", "file", "command", "logparser"
+ /// Type of data collection:
+ /// - "registry": Read values from the Windows Registry
+ /// - "eventlog": Read entries from a Windows Event Log
+ /// - "wmi": Execute a WMI/CIM query
+ /// - "file": Check file/directory existence and optionally read content
+ /// - "command_allowlisted": Run a pre-approved command (PowerShell or CLI). Only commands on
+ /// the agent's hardcoded allowlist in GatherRuleExecutor.cs are permitted. Unlisted commands
+ /// are blocked and generate a security_warning event. See the allowlist in GatherRuleExecutor.cs
+ /// for the full list of approved commands.
+ /// - "logparser": Parse a CMTrace-format log file using a regex pattern with named capture groups
///
public string CollectorType { get; set; }
///
/// Target for collection:
- /// - registry: Registry path (e.g., "HKLM:\SOFTWARE\Microsoft\...")
- /// - eventlog: Event log name (e.g., "Microsoft-Windows-DeviceManagement...")
- /// - wmi: WMI query (e.g., "SELECT * FROM Win32_TPM")
- /// - file: File path (e.g., "C:\Windows\INF\setupapi.dev.log")
- /// - command: Command name from allowlist (e.g., "Get-TpmStatus")
+ /// - registry: Registry path (e.g., "HKLM\SOFTWARE\Microsoft\...")
+ /// - eventlog: Event log name (e.g., "Microsoft-Windows-DeviceManagement-Enterprise-Diagnostics-Provider/Admin")
+ /// - wmi: WMI query (e.g., "SELECT * FROM Win32_TPM WHERE __NAMESPACE='root\\CIMV2\\Security\\MicrosoftTpm'")
+ /// - file: File or directory path with env vars (e.g., "C:\Windows\INF\setupapi.dev.log")
+ /// - command_allowlisted: Exact command string as it appears in the allowlist (e.g., "Get-Tpm", "dsregcmd /status")
/// - logparser: Log file path with env vars (e.g., "%ProgramData%\Microsoft\IntuneManagementExtension\Logs\AppWorkload.log")
///
public string Target { get; set; }
///
- /// Additional parameters for the collector
- /// - registry: { "values": "ProxyServer,ProxyEnable" }
- /// - eventlog: { "eventIds": "407,12029", "maxEvents": "10" }
+ /// Additional parameters for the collector:
+ /// - registry: { "valueName": "ProxyServer" } — omit to read all values
+ /// - eventlog: { "maxEntries": "10", "source": "Microsoft-Windows-DeviceManagement-Enterprise-Diagnostics" }
/// - wmi: { "namespace": "root\\CIMV2\\Security\\MicrosoftTpm" }
- /// - file: { "maxLines": "100", "pattern": "error|fail" }
- /// - command: { "arguments": "-Detailed" }
- /// - logparser: { "pattern": "regex with (?<named>groups)", "logFormat": "cmtrace", "trackPosition": "true", "maxLines": "1000" }
+ /// - file: { "readContent": "true" } — only reads files <50 KB
+ /// - command_allowlisted: (no additional parameters — command is the full string in Target)
+ /// - logparser: { "pattern": "regex with (?<namedGroups>)", "trackPosition": "true", "maxLines": "1000" }
///
public Dictionary Parameters { get; set; } = new Dictionary();
diff --git a/src/Web/autopilot-monitor-web/app/gather-rules/page.tsx b/src/Web/autopilot-monitor-web/app/gather-rules/page.tsx
index 3d6e94ee..d1560903 100644
--- a/src/Web/autopilot-monitor-web/app/gather-rules/page.tsx
+++ b/src/Web/autopilot-monitor-web/app/gather-rules/page.tsx
@@ -47,7 +47,7 @@ interface NewRuleForm {
}
const CATEGORIES = ["network", "identity", "apps", "device", "esp", "enrollment"] as const;
-const COLLECTOR_TYPES = ["registry", "eventlog", "wmi", "file", "command"] as const;
+const COLLECTOR_TYPES = ["registry", "eventlog", "wmi", "file", "command_allowlisted", "logparser"] as const;
const TRIGGERS = ["startup", "phase_change", "interval", "on_event"] as const;
const SEVERITIES = ["info", "warning", "error", "critical"] as const;
@@ -65,7 +65,9 @@ const COLLECTOR_TYPE_LABELS: Record = {
eventlog: "Event Log",
wmi: "WMI Query",
file: "File",
- command: "Command",
+ command_allowlisted: "Command (Allowlisted)",
+ command: "Command (Allowlisted)", // legacy alias
+ logparser: "Log Parser",
};
const EMPTY_FORM: NewRuleForm = {
@@ -506,7 +508,7 @@ export default function GatherRulesPage() {
>
{COLLECTOR_TYPES.map((ct) => (
))}