From 1939b52402f544949cd143c0b4deb5ae4bf3230c Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Tue, 10 Feb 2026 12:01:44 +0100 Subject: [PATCH] Updated rules to new events etc. --- .../Collectors/GatherRuleExecutor.cs | 8 +- .../Services/BuiltInAnalyzeRules.cs | 107 +++++++++++++++++- .../Services/RuleEngine.cs | 6 +- .../Models/GatherRule.cs | 33 ++++-- .../app/gather-rules/page.tsx | 8 +- 5 files changed, 138 insertions(+), 24 deletions(-) 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) => ( ))}