Updated rules to new events etc.

This commit is contained in:
Oliver Kieselbach
2026-02-10 12:01:44 +01:00
parent 080624cc5b
commit 1939b52402
5 changed files with 138 additions and 24 deletions
@@ -16,8 +16,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
{
/// <summary>
/// Executes gather rules received from the backend API
/// Supports registry, eventlog, wmi, file, command (allowlisted), and logparser collector types
/// </summary>
/// 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
///</summary>
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;
@@ -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<RuleCondition>
{
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<RemediationStep>
@@ -173,7 +180,7 @@ namespace AutopilotMonitor.Functions.Services
BaseConfidence = 60,
Conditions = new List<RuleCondition>
{
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<ConfidenceFactor>
{
@@ -259,7 +266,7 @@ namespace AutopilotMonitor.Functions.Services
BaseConfidence = 60,
Conditions = new List<RuleCondition>
{
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<RemediationStep>
@@ -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<RuleCondition>
{
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<RemediationStep>
{
new RemediationStep { Title = "Verify Azure AD connectivity", Steps = new List<string> {
"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<RuleCondition>
{
new RuleCondition { Signal = "summary_errors", Source = "event_type", EventType = "app_tracking_summary", DataField = "errorCount", Operator = "gte", Value = "2", Required = true }
},
ConfidenceFactors = new List<ConfidenceFactor>
{
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<RemediationStep>
{
new RemediationStep { Title = "Review failed apps", Steps = new List<string> {
"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<RuleCondition>
{
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<ConfidenceFactor>
{
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<RemediationStep>
{
new RemediationStep { Title = "Verify proxy bypass for Intune endpoints", Steps = new List<string> {
"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<string> {
"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" }
};
}
}
@@ -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)
@@ -59,29 +59,38 @@ namespace AutopilotMonitor.Shared.Models
// ===== WHAT TO COLLECT =====
/// <summary>
/// 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
/// </summary>
public string CollectorType { get; set; }
/// <summary>
/// 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")
/// </summary>
public string Target { get; set; }
/// <summary>
/// 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 (?&lt;named&gt;groups)", "logFormat": "cmtrace", "trackPosition": "true", "maxLines": "1000" }
/// - file: { "readContent": "true" } — only reads files &lt;50 KB
/// - command_allowlisted: (no additional parameters — command is the full string in Target)
/// - logparser: { "pattern": "regex with (?&lt;namedGroups&gt;)", "trackPosition": "true", "maxLines": "1000" }
/// </summary>
public Dictionary<string, string> Parameters { get; set; } = new Dictionary<string, string>();
@@ -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<string, string> = {
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) => (
<option key={ct} value={ct}>
{ct.charAt(0).toUpperCase() + ct.slice(1)}
{COLLECTOR_TYPE_LABELS[ct] || (ct.charAt(0).toUpperCase() + ct.slice(1))}
</option>
))}
</select>