From a70785fb42a70a792bc9a4f385af116e80015eb2 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Mon, 6 Jul 2026 12:32:32 +0200 Subject: [PATCH] feat(rules): built-in analyze-rule hygiene pass ahead of public docs catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Retire ANALYZE-ESP-003 (duplicate of ESP-001) and ANALYZE-CORR-002 (duplicate of APP-004) via the established sunset path; ESP-001 gains a >60min factor, APP-004 absorbs the co-occurrence story with working confidence factors - CORR-003: download evidence (errorPatternId) is now a required condition — no more firing on proxy + any unrelated app failure - DEV-002 v2: 'sustained' semantics — new event_count value filter (filterField/filterOperator/filterValue in schema, Shared model, engine, web types) requires >=3 snapshots above 90% memory instead of one transient spike - DEV-003 v2: build-cutoff-only condition (<=22621) now also covers Windows 10; LTSC caveat + maintenance note (raise to 22631 after 2026-11-10) - SEC-001 v2: rewritten for the post-June-2026 reality — certificates HAVE expired, active remediation required (OEM firmware hint added) - SEC-005/006 v2.1: allow-list extended with OEM factory-preload families (Intel PPM, AMD Power, Lenovo ThinkPad power/battery, Surface/carrier), telemetry-curated from 1780 fired sessions; imaging leftovers (USMT.PPKG, InstallOrder.ini) and generic names deliberately still alert - APP-001: suppressByEvent on app_install_completed; APP-002: exact MSI exit codes instead of bare 16xx; APP-006 reframed as 'App Install Retry Loop'; ID-001: title/logic mismatch fixed + GATHER-ID-002 dependency documented - Fix all 13 dead confidence factors (signal-name mismatches / unsupported syntax) by rewiring or removal; version bumps everywhere so seeding propagates the updates - New RuleEngineEventCountFilterTests + SEC-005 OEM allow-list coverage Co-Authored-By: Claude Fable 5 --- rules/analyze/ANALYZE-APP-001.json | 12 +- rules/analyze/ANALYZE-APP-002.json | 3 +- rules/analyze/ANALYZE-APP-004.json | 29 +- rules/analyze/ANALYZE-APP-006.json | 20 +- rules/analyze/ANALYZE-CORR-002.json | 55 --- rules/analyze/ANALYZE-CORR-003.json | 17 +- rules/analyze/ANALYZE-DEV-001.json | 4 +- rules/analyze/ANALYZE-DEV-002.json | 35 +- rules/analyze/ANALYZE-DEV-003.json | 29 +- rules/analyze/ANALYZE-ENRL-001.json | 4 +- rules/analyze/ANALYZE-ESP-001.json | 10 +- rules/analyze/ANALYZE-ESP-003.json | 38 -- rules/analyze/ANALYZE-ESP-004.json | 4 +- rules/analyze/ANALYZE-ID-001.json | 20 +- rules/analyze/ANALYZE-ID-003.json | 4 +- rules/analyze/ANALYZE-OFFICE-001.json | 11 +- rules/analyze/ANALYZE-SEC-001.json | 31 +- rules/analyze/ANALYZE-SEC-005.json | 8 +- rules/analyze/ANALYZE-SEC-006.json | 8 +- rules/dist/analyze-rules.json | 377 ++++++++---------- rules/schema/analyze-rule.schema.json | 13 + .../RuleEngineArrayConditionTests.cs | 52 ++- .../RuleEngineDetectionFailureTests.cs | 2 +- .../RuleEngineEventCountFilterTests.cs | 201 ++++++++++ .../RuleEngine.ConditionEvaluators.cs | 9 + .../Models/Rules/AnalyzeRule.cs | 21 + .../app/analyze-rules/types.ts | 6 + 27 files changed, 622 insertions(+), 401 deletions(-) delete mode 100644 rules/analyze/ANALYZE-CORR-002.json delete mode 100644 rules/analyze/ANALYZE-ESP-003.json create mode 100644 src/Backend/AutopilotMonitor.Functions.Tests/RuleEngineEventCountFilterTests.cs diff --git a/rules/analyze/ANALYZE-APP-001.json b/rules/analyze/ANALYZE-APP-001.json index 9fa23655..534ef403 100644 --- a/rules/analyze/ANALYZE-APP-001.json +++ b/rules/analyze/ANALYZE-APP-001.json @@ -5,6 +5,7 @@ "description": "Detects when a Win32 app detection script fails or returns unexpected results.", "severity": "warning", "category": "apps", + "version": "1.1.0", "enabled": true, "baseConfidence": 60, "conditions": [ @@ -15,13 +16,14 @@ "dataField": "detectionResult", "operator": "equals", "value": "NotDetected", - "required": true + "required": true, + "suppressByEvent": { + "eventType": "app_install_completed", + "joinField": "appId" + } } ], - "confidenceFactors": [ - { "signal": "error_detail_match", "condition": "exists", "weight": 20 } - ], - "explanation": "A Win32 app detection script failed or did not detect the app after installation. This causes the app to appear as 'not installed' despite successful installation.", + "explanation": "A Win32 app detection script failed or did not detect the app after installation. This causes the app to appear as 'not installed' despite successful installation. Detection failures that were later resolved by a successful install of the same app are suppressed and do not raise this finding.", "remediation": [ { "title": "Fix detection rules", diff --git a/rules/analyze/ANALYZE-APP-002.json b/rules/analyze/ANALYZE-APP-002.json index 6bbf43ef..9c09d8b5 100644 --- a/rules/analyze/ANALYZE-APP-002.json +++ b/rules/analyze/ANALYZE-APP-002.json @@ -5,6 +5,7 @@ "description": "Detects app installation failures with specific exit codes or HRESULT error codes and suggests fixes.", "severity": "high", "category": "apps", + "version": "1.1.0", "enabled": true, "baseConfidence": 80, "conditions": [ @@ -14,7 +15,7 @@ "eventType": "app_install_failed", "dataField": "message", "operator": "regex", - "value": "16[0-9]{2}|0x80070[0-9a-fA-F]+|lpExitCode|unmapped.*exit", + "value": "\\b(1601|1602|1603|1618|1619|1620|1622|1624|1625|1633|1638|1653)\\b|0x80070[0-9a-fA-F]+|lpExitCode|unmapped.*exit", "required": true, "suppressByEvent": { "eventType": "app_install_completed", diff --git a/rules/analyze/ANALYZE-APP-004.json b/rules/analyze/ANALYZE-APP-004.json index 534c3709..0a257db6 100644 --- a/rules/analyze/ANALYZE-APP-004.json +++ b/rules/analyze/ANALYZE-APP-004.json @@ -2,9 +2,10 @@ "$schema": "../schema/analyze-rule.schema.json", "ruleId": "ANALYZE-APP-004", "title": "Insufficient Disk Space", - "description": "Detects when disk space is critically low during enrollment, which can cause app installation failures.", + "description": "Detects when disk space is critically low during enrollment, which can cause app installation failures. Confidence increases when app installations actually failed while disk space was low.", "severity": "critical", "category": "apps", + "version": "1.1.0", "enabled": true, "baseConfidence": 75, "conditions": [ @@ -16,13 +17,29 @@ "operator": "lt", "value": "5", "required": true + }, + { + "signal": "app_failed", + "source": "event_type", + "eventType": "app_install_failed", + "operator": "exists", + "value": "", + "required": false + }, + { + "signal": "enrollment_failed", + "source": "event_type", + "eventType": "enrollment_failed", + "operator": "exists", + "value": "", + "required": false } ], "confidenceFactors": [ { "signal": "app_failed", "condition": "exists", "weight": 15 }, { "signal": "enrollment_failed", "condition": "exists", "weight": 10 } ], - "explanation": "Performance monitoring shows disk free space dropped below 5 GB during enrollment. This can cause app installation failures, especially for large Win32 apps.", + "explanation": "Performance monitoring shows disk free space dropped below 5 GB during enrollment. This can cause app installation failures, especially for large Win32 apps.\n\nIf app installations failed in the same session while free space was low, treat storage exhaustion as the likely contributing cause — the confidence score of this finding is raised accordingly. Verify the individual app failures for their concrete error codes before concluding storage was the only cause.", "remediation": [ { "title": "Free disk space", @@ -31,6 +48,14 @@ "Clean up temporary files and IME cache (C:\\Windows\\IMECache)", "Consider using a larger disk image" ] + }, + { + "title": "Reduce disk consumption during enrollment", + "steps": [ + "Review the total download size of all blocking apps", + "Stagger large app installations so temporary install files do not accumulate", + "Use devices with larger drives for app-heavy deployment profiles" + ] } ], "tags": ["apps", "disk-space", "critical"] diff --git a/rules/analyze/ANALYZE-APP-006.json b/rules/analyze/ANALYZE-APP-006.json index fcfc7099..01a97ad2 100644 --- a/rules/analyze/ANALYZE-APP-006.json +++ b/rules/analyze/ANALYZE-APP-006.json @@ -1,15 +1,16 @@ { "$schema": "../schema/analyze-rule.schema.json", "ruleId": "ANALYZE-APP-006", - "title": "Reboot Required Loop", - "description": "Detects when the same app installation is attempted 3 or more times, indicating a reboot-required loop.", + "title": "App Install Retry Loop", + "description": "Detects when the same app installation is attempted 3 or more times during one enrollment — a retry loop caused by unhonored reboot requests, repeated failures, or detection mismatches.", "severity": "high", "category": "apps", + "version": "1.1.0", "enabled": true, "baseConfidence": 70, "conditions": [ { - "signal": "reboot_loop", + "signal": "install_retry_loop", "source": "event_count", "eventType": "app_install_started", "dataField": "appId", @@ -18,16 +19,17 @@ "required": true } ], - "explanation": "The same app has been installed 3 or more times during this enrollment. This typically indicates the app requires a reboot between install attempts, but the ESP is not honoring the reboot request, causing an install-detect-retry loop.", + "explanation": "The same app was started 3 or more times during this enrollment. Repeated install attempts have several possible causes:\n\n- **Reboot-required loop** — the app requests a reboot (exit code 3010/1641) that the ESP does not honor, so the install-detect-retry cycle repeats\n- **Failure retries** — the install keeps failing and the IME retries it\n- **Detection mismatch** — the install succeeds but the detection rule does not match, so the app is retried as 'not installed'\n\nCheck the app's install results in this session to see which pattern applies; enrollments that legitimately span multiple reboots can also produce additional install starts.", "remediation": [ { - "title": "Fix reboot handling", + "title": "Identify the retry cause", "steps": [ - "Check if the app's return code mapping includes reboot codes (3010, 1641)", - "Verify ESP reboot behavior settings", - "Consider setting the app to 'Hard reboot' return code in Intune" + "Review this app's install started/completed/failed events in the session timeline", + "If reboot-related: check the app's return code mapping includes reboot codes (3010, 1641) and verify ESP reboot behavior settings; consider the 'Hard reboot' return code in Intune", + "If the install keeps failing: look up the failure error code (see the App Installation Error finding if it fired)", + "If the install succeeds but is retried: fix the app's detection rule (see the Win32 App Detection Script Failure finding)" ] } ], - "tags": ["apps", "reboot", "loop"] + "tags": ["apps", "reboot", "retry", "loop"] } diff --git a/rules/analyze/ANALYZE-CORR-002.json b/rules/analyze/ANALYZE-CORR-002.json deleted file mode 100644 index 20e0bd0f..00000000 --- a/rules/analyze/ANALYZE-CORR-002.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "$schema": "../schema/analyze-rule.schema.json", - "ruleId": "ANALYZE-CORR-002", - "title": "Disk Space Exhaustion Caused Installation Failure", - "description": "Correlates declining disk free space with app installation failure, confirming insufficient storage as root cause.", - "severity": "critical", - "category": "apps", - "enabled": true, - "trigger": "correlation", - "baseConfidence": 65, - "conditions": [ - { - "signal": "low_disk", - "source": "event_type", - "eventType": "performance_snapshot", - "dataField": "disk_free_gb", - "operator": "lt", - "value": "5", - "required": true - }, - { - "signal": "app_failed", - "source": "event_type", - "eventType": "app_install_failed", - "operator": "exists", - "value": "", - "required": true - } - ], - "confidenceFactors": [ - { "signal": "multiple_failures", "condition": "count >= 2", "weight": 15 }, - { "signal": "enrollment_failed", "condition": "exists", "weight": 10 } - ], - "confidenceThreshold": 50, - "explanation": "**Root cause confirmed:** Performance monitoring shows disk free space dropped below 5 GB during enrollment, and apps failed to install. The device likely ran out of storage during app installation.\n\nThis is common when deploying many large Win32 apps to devices with small drives (64 GB or 128 GB).", - "remediation": [ - { - "title": "Reduce disk consumption during enrollment", - "steps": [ - "Review total size of all apps deployed during ESP", - "Move non-critical apps to post-ESP deployment", - "Use MSIX or Store apps instead of Win32 where possible (smaller footprint)", - "Clean up IME cache: C:\\Windows\\IMECache" - ] - }, - { - "title": "Use devices with larger drives", - "steps": [ - "Minimum 128 GB recommended for standard deployments", - "256 GB recommended when deploying large app suites (Office, Visual Studio, etc.)" - ] - } - ], - "tags": ["correlation", "apps", "disk-space", "performance", "root-cause"] -} diff --git a/rules/analyze/ANALYZE-CORR-003.json b/rules/analyze/ANALYZE-CORR-003.json index ceaba636..6da61c34 100644 --- a/rules/analyze/ANALYZE-CORR-003.json +++ b/rules/analyze/ANALYZE-CORR-003.json @@ -2,9 +2,10 @@ "$schema": "../schema/analyze-rule.schema.json", "ruleId": "ANALYZE-CORR-003", "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.", + "description": "Correlates an active proxy configuration with app content download failures (Delivery Optimization timeout or download error), suggesting the proxy is blocking or throttling Intune content delivery.", "severity": "high", "category": "apps", + "version": "1.1.0", "enabled": true, "trigger": "correlation", "baseConfidence": 60, @@ -19,21 +20,21 @@ "required": true }, { - "signal": "app_failed", + "signal": "download_failure", "source": "event_type", "eventType": "app_install_failed", - "operator": "exists", - "value": "", + "dataField": "errorPatternId", + "operator": "regex", + "value": "IME-DO-TIMEOUT|IME-ERROR-DOWNLOAD|IME-ERROR-NO-CONTENT", "required": true } ], "confidenceFactors": [ - { "signal": "download_timeout", "condition": "exists", "weight": 20 }, - { "signal": "multiple_failures", "condition": "count >= 2", "weight": 15 }, - { "signal": "enrollment_failed", "condition": "exists", "weight": 10 } + { "signal": "app_install_failed", "condition": "count >= 2", "weight": 15 }, + { "signal": "enrollment_failed", "condition": "count >= 1", "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.", + "explanation": "A proxy (explicit or PAC-based) is configured on this device and app content downloads failed (Delivery Optimization timeout or download content error). Proxies can block or throttle downloads from *.delivery.mp.microsoft.com and other Intune CDN endpoints.\n\nThis is a common root cause when app downloads fail consistently on proxy-connected corporate networks. Note this rule only fires when the failure is download-related — generic app install errors behind a proxy do not trigger it.", "remediation": [ { "title": "Verify proxy bypass for Intune endpoints", diff --git a/rules/analyze/ANALYZE-DEV-001.json b/rules/analyze/ANALYZE-DEV-001.json index d623b02b..d3007719 100644 --- a/rules/analyze/ANALYZE-DEV-001.json +++ b/rules/analyze/ANALYZE-DEV-001.json @@ -5,6 +5,7 @@ "description": "Detects when Windows Hello for Business provisioning did not start within the expected timeframe after ESP completion.", "severity": "warning", "category": "device", + "version": "1.1.0", "enabled": true, "baseConfidence": 60, "conditions": [ @@ -17,9 +18,6 @@ "required": true } ], - "confidenceFactors": [ - { "signal": "hello_policy_detected", "condition": "exists", "weight": 30 } - ], "explanation": "Windows Hello for Business provisioning did not start within the expected time after ESP exit. This may indicate that the Hello policy is not configured, the device doesn't meet prerequisites, or there's a connectivity issue with the Key Registration Service.", "remediation": [ { diff --git a/rules/analyze/ANALYZE-DEV-002.json b/rules/analyze/ANALYZE-DEV-002.json index 269ef975..19a5578e 100644 --- a/rules/analyze/ANALYZE-DEV-002.json +++ b/rules/analyze/ANALYZE-DEV-002.json @@ -1,28 +1,47 @@ { "$schema": "../schema/analyze-rule.schema.json", "ruleId": "ANALYZE-DEV-002", - "title": "High Memory Usage During Enrollment", - "description": "Detects when memory usage exceeds 90% during enrollment, which can cause app installation failures or system instability.", + "title": "Sustained High Memory Usage During Enrollment", + "description": "Detects when memory usage stays above 90% across at least 3 performance snapshots during enrollment, indicating sustained memory pressure that can cause app installation failures or system instability.", "severity": "warning", "category": "device", + "version": "2.0.0", "enabled": true, "baseConfidence": 55, "conditions": [ { - "signal": "high_memory", - "source": "event_type", + "signal": "sustained_high_memory", + "source": "event_count", "eventType": "performance_snapshot", - "dataField": "memory_used_percent", - "operator": "gt", - "value": "90", + "operator": "count_gte", + "value": "3", + "filterField": "memory_used_percent", + "filterOperator": "gt", + "filterValue": "90", "required": true + }, + { + "signal": "app_failed", + "source": "event_type", + "eventType": "app_install_failed", + "operator": "exists", + "value": "", + "required": false + }, + { + "signal": "enrollment_failed", + "source": "event_type", + "eventType": "enrollment_failed", + "operator": "exists", + "value": "", + "required": false } ], "confidenceFactors": [ { "signal": "app_failed", "condition": "exists", "weight": 20 }, { "signal": "enrollment_failed", "condition": "exists", "weight": 15 } ], - "explanation": "Memory usage exceeded 90% during enrollment. High memory pressure can cause app installation failures, system slowdowns, and ESP timeouts.", + "explanation": "Memory usage exceeded 90% in at least 3 performance snapshots during this enrollment — sustained pressure, not a single transient spike. Persistent high memory usage can cause app installation failures, system slowdowns, and ESP timeouts, especially on devices with 8 GB RAM installing several apps concurrently.", "remediation": [ { "title": "Reduce memory pressure", diff --git a/rules/analyze/ANALYZE-DEV-003.json b/rules/analyze/ANALYZE-DEV-003.json index 22ceb551..2baad76a 100644 --- a/rules/analyze/ANALYZE-DEV-003.json +++ b/rules/analyze/ANALYZE-DEV-003.json @@ -1,22 +1,14 @@ { "$schema": "../schema/analyze-rule.schema.json", "ruleId": "ANALYZE-DEV-003", - "title": "Unsupported Windows 11 Version", - "description": "Detects devices running Windows 11 version 22H2 or older, which is no longer supported by Microsoft.", + "title": "Unsupported Windows Version", + "description": "Detects devices enrolling with an OS build that has reached end of support: Windows 11 22H2 (build 22621) or older, which includes every Windows 10 build. Maintenance note: raise the build cutoff to 22631 after Windows 11 23H2 reaches end of support on 2026-11-10.", "severity": "warning", "category": "device", + "version": "2.0.0", "enabled": true, "baseConfidence": 90, "conditions": [ - { - "signal": "windows_11", - "source": "event_type", - "eventType": "os_info", - "dataField": "osVersion", - "operator": "contains", - "value": "Windows 11", - "required": true - }, { "signal": "old_build", "source": "event_data", @@ -25,11 +17,20 @@ "operator": "lte", "value": "22621", "required": true + }, + { + "signal": "os_version", + "source": "event_type", + "eventType": "os_info", + "dataField": "osVersion", + "operator": "exists", + "value": "", + "required": false } ], "confidenceFactors": [], "confidenceThreshold": 40, - "explanation": "This device is running Windows 11 version 22H2 (Build 22621) or older, which has reached end of support. Microsoft no longer provides security updates or bug fixes for this version. Devices running unsupported OS versions are exposed to known vulnerabilities and may fail compliance checks.", + "explanation": "This device is running an OS build that has reached end of support:\n\n- **OS:** `{{osVersion}}`\n- **Build:** `{{currentBuild}}`\n\nBuilds up to 22621 cover Windows 11 22H2 and older as well as every Windows 10 release — Microsoft no longer provides security updates or bug fixes for these versions (exception: LTSC/IoT LTSC editions follow their own lifecycle; verify the edition if this device is LTSC). Devices running unsupported OS versions are exposed to known vulnerabilities and may fail compliance checks.", "remediation": [ { "title": "Update to a supported Windows 11 version", @@ -47,9 +48,9 @@ "url": "https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information" }, { - "title": "Windows 11 22H2 end of support", + "title": "Windows lifecycle (Home and Pro)", "url": "https://learn.microsoft.com/en-us/lifecycle/products/windows-11-home-and-pro" } ], - "tags": ["device", "os-version", "end-of-support", "windows-11"] + "tags": ["device", "os-version", "end-of-support", "windows-11", "windows-10"] } diff --git a/rules/analyze/ANALYZE-ENRL-001.json b/rules/analyze/ANALYZE-ENRL-001.json index 73ffd526..71b8e254 100644 --- a/rules/analyze/ANALYZE-ENRL-001.json +++ b/rules/analyze/ANALYZE-ENRL-001.json @@ -5,7 +5,7 @@ "description": "Detects when the Autopilot enrollment has explicitly failed.", "severity": "critical", "category": "enrollment", - "version": "1.1.1", + "version": "1.1.2", "enabled": true, "baseConfidence": 95, "conditions": [ @@ -55,7 +55,7 @@ } ], "confidenceFactors": [ - { "signal": "app_failed", "condition": "exists", "weight": 5 } + { "signal": "failed_app", "condition": "exists", "weight": 5 } ], "explanation": "The Autopilot enrollment has explicitly failed.\n\n**Key facts extracted from this session's events:**\n\n- **Failure reason:** `{{reason}}`\n- **First failed app:** `{{appName}}`\n- **ESP failed subcategory:** `{{failedSubcategory}}`\n- **ESP HRESULT:** `{{errorCode}}`\n\nWhen an ESP Apps-subcategory failure is paired with HRESULT `0x87D1041C`, the device-side install most likely completed but the Intune detection rule did not match afterwards — see the **App Detection Failure During ESP** rule (ANALYZE-APP-013) in this same analysis for the actionable root cause and remediation steps. Any field still shown as a literal `{{…}}` placeholder was simply not recorded for this session — for example, a non-ESP failure path has no ESP failed-subcategory or HRESULT.", "remediation": [ diff --git a/rules/analyze/ANALYZE-ESP-001.json b/rules/analyze/ANALYZE-ESP-001.json index d98d5574..27270927 100644 --- a/rules/analyze/ANALYZE-ESP-001.json +++ b/rules/analyze/ANALYZE-ESP-001.json @@ -2,9 +2,10 @@ "$schema": "../schema/analyze-rule.schema.json", "ruleId": "ANALYZE-ESP-001", "title": "ESP Blocking App Timeout", - "description": "Detects when the DeviceSetup phase of the ESP took longer than 30 minutes, indicating a stuck or very slow blocking app.", + "description": "Detects when the DeviceSetup phase of the ESP took longer than 30 minutes, indicating a stuck or very slow blocking app. Confidence increases when the phase exceeds 60 minutes.", "severity": "high", "category": "esp", + "version": "1.1.0", "enabled": true, "baseConfidence": 50, "conditions": [ @@ -19,10 +20,11 @@ } ], "confidenceFactors": [ - { "signal": "long_esp", "condition": "phase_duration > 1800", "weight": 40 } + { "signal": "long_esp", "condition": "phase_duration > 1800", "weight": 40 }, + { "signal": "very_long_esp", "condition": "phase_duration > 3600", "weight": 15 } ], "confidenceThreshold": 70, - "explanation": "The ESP DeviceSetup phase took more than 30 minutes. This indicates a blocking app is stuck installing, downloading very slowly, or has entered a retry loop.", + "explanation": "The ESP DeviceSetup phase took more than 30 minutes. This indicates a blocking app is stuck installing, downloading very slowly, or has entered a retry loop. A duration beyond 60 minutes raises the confidence further — at that point the enrollment is almost certainly held by a single stuck blocking item rather than ordinary slowness.", "remediation": [ { "title": "Investigate blocking app", @@ -33,5 +35,5 @@ ] } ], - "tags": ["esp", "blocking", "timeout"] + "tags": ["esp", "blocking", "timeout", "duration"] } diff --git a/rules/analyze/ANALYZE-ESP-003.json b/rules/analyze/ANALYZE-ESP-003.json deleted file mode 100644 index fb030568..00000000 --- a/rules/analyze/ANALYZE-ESP-003.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "$schema": "../schema/analyze-rule.schema.json", - "ruleId": "ANALYZE-ESP-003", - "title": "Enrollment Duration Exceeded 60 Minutes", - "description": "Detects when the total Autopilot enrollment takes longer than 60 minutes.", - "severity": "high", - "category": "esp", - "enabled": true, - "baseConfidence": 50, - "conditions": [ - { - "signal": "long_enrollment", - "source": "phase_duration", - "eventType": "esp_phase_changed", - "dataField": "espPhase", - "operator": "equals", - "value": "DeviceSetup", - "required": true - } - ], - "confidenceFactors": [ - { "signal": "long_esp", "condition": "phase_duration > 3600", "weight": 40 } - ], - "confidenceThreshold": 70, - "explanation": "The Autopilot enrollment exceeded 60 minutes. This is unusually long and may indicate stuck apps, slow network, or policy conflicts. The default ESP timeout is 60 minutes, after which the enrollment may be marked as failed.", - "remediation": [ - { - "title": "Investigate long enrollment", - "steps": [ - "Check the app installation timeline - identify which app took the longest", - "Review download speeds in the performance data", - "Consider reducing the number of ESP-blocking apps", - "Check if the ESP timeout policy is configured (default: 60 min)" - ] - } - ], - "tags": ["esp", "timeout", "duration"] -} diff --git a/rules/analyze/ANALYZE-ESP-004.json b/rules/analyze/ANALYZE-ESP-004.json index 04a5aac7..3a5cccfc 100644 --- a/rules/analyze/ANALYZE-ESP-004.json +++ b/rules/analyze/ANALYZE-ESP-004.json @@ -5,7 +5,7 @@ "description": "Detects an ESP terminal failure on a profile that allows 'Continue anyway'. The enrollment is reported as failed, but the user most likely dismissed the error screen and reached the desktop — a soft failure rather than an unusable device.", "severity": "warning", "category": "esp", - "version": "1.0.0", + "version": "1.0.1", "enabled": true, "baseConfidence": 80, "conditions": [ @@ -55,7 +55,7 @@ } ], "confidenceFactors": [ - { "signal": "app_failed", "condition": "exists", "weight": 10 } + { "signal": "appName", "condition": "exists", "weight": 10 } ], "explanation": "This enrollment was reported as **failed** because the Enrollment Status Page reached its configured terminal timeout (`{{espSyncFailureTimeoutMinutes}} min`) before all blocking items finished. The ESP profile, however, allows **'Continue anyway'** — so the user most likely dismissed the error screen and reached the desktop with a working, though not fully provisioned, device.\n\nThe agent only observes the ESP terminal failure on the device side; it cannot see what the user did after the failure screen. Treat this as a **soft failure**: the device may well be in productive use, while at least one blocking item (`{{failedSubcategory}}` subcategory / app `{{appName}}`) did not complete inside the ESP window.\n\nA cluster of these across a tenant almost always points at one slow blocking app holding the whole fleet at the ESP timeout wall, not at broken devices. Any field still shown as a literal `{{…}}` placeholder was simply not recorded for this session (e.g. a certificate-only failure has no failed app).", "remediation": [ diff --git a/rules/analyze/ANALYZE-ID-001.json b/rules/analyze/ANALYZE-ID-001.json index d4a36fc0..a0bed42a 100644 --- a/rules/analyze/ANALYZE-ID-001.json +++ b/rules/analyze/ANALYZE-ID-001.json @@ -1,13 +1,13 @@ { "$schema": "../schema/analyze-rule.schema.json", "ruleId": "ANALYZE-ID-001", - "title": "No Machine Certificates Found", - "description": "Detects when no certificates are present in the local machine personal store (LocalMachine\\My). Missing certificates may indicate that MDM enrollment certificate provisioning or SCEP certificate deployment failed.", + "title": "Expected Machine Certificate Not Found", + "description": "Detects when a certificate with the configured subject is missing from the local machine personal store (LocalMachine\\My). Requires the gather rule GATHER-ID-002 (machine cert store inventory) to be enabled — the check runs against its gather_machine_cert_store output. A missing certificate may indicate that MDM enrollment certificate provisioning or SCEP certificate deployment failed.", "severity": "warning", "category": "identity", "isCommunity": true, "enabled": false, - "version": "1.1.0", + "version": "1.2.0", "baseConfidence": 75, "conditions": [ { @@ -28,20 +28,8 @@ "required": true } ], - "confidenceFactors": [ - { - "signal": "no_output_at_all", - "condition": "gather_machine_cert_store.output not_exists", - "weight": 20 - }, - { - "signal": "command_error", - "condition": "gather_machine_cert_store.error_output exists", - "weight": 10 - } - ], "confidenceThreshold": 70, - "explanation": "The expected certificate with the configured subject was not found in the local machine certificate store (LocalMachine\\My). Customize the 'value' field in the 'cert_subject_missing' condition to match your expected certificate subject (e.g. 'CN=Contoso Root CA'). The check is case-insensitive (substring match). If the certificate should have been provisioned via SCEP or MDM enrollment, this may indicate a deployment failure.", + "explanation": "The expected certificate with the configured subject was not found in the local machine certificate store (LocalMachine\\My). Customize the 'value' field in the 'cert_subject_missing' condition to match your expected certificate subject (e.g. 'CN=Contoso Root CA'). The check is case-insensitive (substring match). This rule depends on the gather rule GATHER-ID-002 delivering the machine cert store inventory (gather_machine_cert_store event) — without it the rule never evaluates. If the certificate should have been provisioned via SCEP or MDM enrollment, this may indicate a deployment failure.", "remediation": [ { "title": "Verify the expected certificate", diff --git a/rules/analyze/ANALYZE-ID-003.json b/rules/analyze/ANALYZE-ID-003.json index 84362f17..3cd20395 100644 --- a/rules/analyze/ANALYZE-ID-003.json +++ b/rules/analyze/ANALYZE-ID-003.json @@ -5,7 +5,7 @@ "description": "Fires when the vulnerability correlation finds critical-severity CVEs or CISA KEV entries in installed software. This indicates the device has software with known, actively exploited vulnerabilities.", "severity": "critical", "category": "security", - "version": "1.0.0", + "version": "1.0.1", "author": "Autopilot Monitor", "enabled": true, "trigger": "single", @@ -31,7 +31,7 @@ } ], "confidenceFactors": [ - { "signal": "kev_confirmed", "condition": "exists", "weight": 10 } + { "signal": "kev_matches", "condition": "exists", "weight": 10 } ], "confidenceThreshold": 80, "explanation": "The device has installed software with critical vulnerabilities. Software with CVEs rated CRITICAL (CVSS >= 9.0) or listed in the CISA Known Exploited Vulnerabilities catalog was detected. These vulnerabilities are actively targeted by attackers and should be remediated immediately.", diff --git a/rules/analyze/ANALYZE-OFFICE-001.json b/rules/analyze/ANALYZE-OFFICE-001.json index ad48e06c..61eff36f 100644 --- a/rules/analyze/ANALYZE-OFFICE-001.json +++ b/rules/analyze/ANALYZE-OFFICE-001.json @@ -5,7 +5,7 @@ "description": "Detects when the Microsoft 365 Apps (Office Click-to-Run) background install did not reach completion. The V2 OfficeInstallDetector watches the C2R client directly (registry + process) and emits office_install_failed when the install scenario was abandoned (worker gone and streaming never finished) or a C2R failure code was observed. This catches Office failures that the Intune integrated app type hides, because IME reports the app done within 1-2 minutes while C2R keeps installing in the background.", "severity": "warning", "category": "apps", - "version": "1.0.0", + "version": "1.0.1", "enabled": true, "baseConfidence": 75, "conditions": [ @@ -16,6 +16,15 @@ "operator": "exists", "value": "", "required": true + }, + { + "signal": "error_code_present", + "source": "event_type", + "eventType": "office_install_failed", + "dataField": "errorCode", + "operator": "exists", + "value": "", + "required": false } ], "confidenceFactors": [ diff --git a/rules/analyze/ANALYZE-SEC-001.json b/rules/analyze/ANALYZE-SEC-001.json index c390cc78..2b757ccc 100644 --- a/rules/analyze/ANALYZE-SEC-001.json +++ b/rules/analyze/ANALYZE-SEC-001.json @@ -2,10 +2,10 @@ "$schema": "../schema/analyze-rule.schema.json", "ruleId": "ANALYZE-SEC-001", "title": "Secure Boot UEFI CA 2023 certificate not deployed", - "description": "Detects devices where the Windows UEFI CA 2023 certificate has not been deployed to the Secure Boot DB. These devices will lose Secure Boot security update support when the 2011 certificates expire in June 2026.", + "description": "Detects devices where the Windows UEFI CA 2023 certificate has not been deployed to the Secure Boot DB. The 2011 certificates expired in June 2026 — affected devices no longer receive Secure Boot security updates until the 2023 certificate is deployed.", "severity": "warning", "category": "security", - "version": "1.1.0", + "version": "2.0.0", "enabled": true, "baseConfidence": 70, "preconditions": [ @@ -36,19 +36,38 @@ "operator": "equals", "value": "true", "required": true + }, + { + "signal": "ca2023_notfound", + "source": "event_data", + "eventType": "secureboot_status", + "dataField": "uefiCA2023Status", + "operator": "equals", + "value": "notfound", + "required": false + }, + { + "signal": "ca2023_error", + "source": "event_data", + "eventType": "secureboot_status", + "dataField": "uefiCA2023Error", + "operator": "exists", + "value": "", + "required": false } ], "confidenceFactors": [ - { "signal": "ca2023_notfound", "condition": "equals:notfound", "weight": 20 }, + { "signal": "ca2023_notfound", "condition": "exists", "weight": 20 }, { "signal": "ca2023_error", "condition": "exists", "weight": 10 } ], - "explanation": "This device has Secure Boot enabled but the **Windows UEFI CA 2023** certificate has not been deployed to the Secure Boot DB yet.\n\nThe original Windows UEFI CA 2011 certificates expire on **June 27, 2026**. After expiration, devices without the 2023 certificate will no longer receive Secure Boot security updates and may fail to boot with updated boot managers.\n\nCurrent UEFI CA 2023 status: `{{uefiCA2023Status}}`", + "explanation": "This device has Secure Boot enabled but the **Windows UEFI CA 2023** certificate has not been deployed to the Secure Boot DB.\n\nThe original Windows UEFI CA 2011 certificates **expired on June 27, 2026 — this deadline has passed**. The automatic pre-expiry rollout window is over: this device no longer receives Secure Boot security updates (DB/DBX) and cannot trust 2023-signed boot managers until the 2023 certificate is deployed. Deploying it now requires active remediation (Windows servicing opt-in and, on some models, an OEM firmware update) rather than waiting for an automatic update.\n\nCurrent UEFI CA 2023 status: `{{uefiCA2023Status}}`", "remediation": [ { - "title": "Deploy UEFI CA 2023 certificate", + "title": "Deploy UEFI CA 2023 certificate now", "steps": [ "Deploy KB5036210 via Windows Update or WSUS", "Use Intune policy to enable Secure Boot DB update (Windows UEFI CA 2023)", + "Check with the device OEM whether a firmware update is required for the DB update to apply on this model", "Alternatively, use the WinCS CLI tool from Microsoft for manual deployment", "Verify deployment: check registry HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecureBoot\\Servicing\\UEFICA2023Status = 'Updated'", "After DB update: ensure the boot manager is updated to a 2023-signed version via monthly cumulative update" @@ -57,7 +76,7 @@ ], "relatedDocs": [ { - "title": "Microsoft: Secure Boot certificates expire June 2026", + "title": "Microsoft: Secure Boot certificates expired June 2026 — act now", "url": "https://techcommunity.microsoft.com/blog/windows-itpro-blog/act-now-secure-boot-certificates-expire-in-june-2026/4426856" }, { diff --git a/rules/analyze/ANALYZE-SEC-005.json b/rules/analyze/ANALYZE-SEC-005.json index 5598d20d..f267d9c4 100644 --- a/rules/analyze/ANALYZE-SEC-005.json +++ b/rules/analyze/ANALYZE-SEC-005.json @@ -2,10 +2,10 @@ "$schema": "../schema/analyze-rule.schema.json", "ruleId": "ANALYZE-SEC-005", "title": "Unexpected Provisioning Package", - "description": "Detects provisioning packages (PPKG) that are not on the built-in allow-list. The agent emits one or more provisioning_package_scan events whose artifacts[] arrays list every PPKG (registry package, .ppkg file, or Recovery\\Customizations residue; a large set is split across several events). This rule iterates that array across all of them and fires for any artifact whose identity does not match the allow-list regex. The default allow-list covers the Windows OS-inbox packages that ship on every device (Cosa, Power.EnergyEstimationEngine, Power.Settings, SecureStart) so a clean device stays quiet. To allow your own vendor/recovery packages, enable the template rule ANALYZE-SEC-006, extend its allow-list, then disable this rule.", + "description": "Detects provisioning packages (PPKG) that are not on the built-in allow-list. The agent emits one or more provisioning_package_scan events whose artifacts[] arrays list every PPKG (registry package, .ppkg file, or Recovery\\Customizations residue; a large set is split across several events). This rule iterates that array across all of them and fires for any artifact whose identity does not match the allow-list regex. The default allow-list covers the Windows OS-inbox packages (Cosa, Power.EnergyEstimationEngine, Power.Settings, SecureStart) plus common OEM factory-preload families (Intel PPM power profiles, AMD Power profiles, Lenovo ThinkPad power/battery packages, Microsoft Surface defaults and carrier packages) so clean factory devices stay quiet. OEM imaging leftovers (e.g. USMT.PPKG, InstallOrder.ini) and generically named packages deliberately still alert — on a pure-Autopilot device they are worth a look. To allow your own vendor/recovery packages, enable the template rule ANALYZE-SEC-006, extend its allow-list, then disable this rule.", "severity": "warning", "category": "security", - "version": "2.0.0", + "version": "2.1.0", "enabled": true, "baseConfidence": 80, "conditions": [ @@ -16,12 +16,12 @@ "dataField": "artifacts", "itemField": "identity", "operator": "not_regex", - "value": "^(?:Microsoft\\.Windows\\.Cosa|Power\\.EnergyEstimationEngine|Power\\.Settings|SecureStart\\.Settings)\\b", + "value": "^(?:Microsoft\\.Windows\\.Cosa\\b|Power\\.EnergyEstimationEngine\\b|Power\\.Settings\\b|SecureStart\\.Settings\\b|PPM[-_]|PPMSettings-|AMD\\.Power\\.|TpPower|DisableBatteryDischargeEstimator\\b|PerfEppForBatterySaver\\b|AHSControl\\b|IRFeatureOptimization\\b|Surface[._]|vzw\\.ppkg\\b)", "required": true } ], "confidenceThreshold": 40, - "explanation": "A provisioning package (PPKG) was detected that is NOT on the allow-list.\n\n- **Unexpected package:** `{{identity}}`\n\nThe agent reports every PPKG it finds in `provisioning_package_scan` event(s) (see their `artifacts` array(s) for the full list, plus file metadata and best-effort content indicators; large sets span several events). This rule ignores the Windows OS-inbox packages that ship on every device (Cosa, Power.EnergyEstimationEngine, Power.Settings, SecureStart) and only surfaces the rest.\n\nProvisioning packages can apply local accounts, certificates, Wi-Fi/VPN profiles, applications (MSI/EXE) and scripts. They are legitimate in bulk-enrollment and OEM-recovery scenarios, but a PPKG appearing on a device that is expected to be \"pure Autopilot\" can indicate tampering.\n\n**To stop alerting on a known-good package:** enable the template rule **ANALYZE-SEC-006**, add the package to its allow-list regex, then disable this rule. ANALYZE-SEC-006 then only fires for packages outside your extended allow-list.", + "explanation": "A provisioning package (PPKG) was detected that is NOT on the allow-list.\n\n- **Unexpected package:** `{{identity}}`\n\nThe agent reports every PPKG it finds in `provisioning_package_scan` event(s) (see their `artifacts` array(s) for the full list, plus file metadata and best-effort content indicators; large sets span several events). This rule ignores the Windows OS-inbox packages (Cosa, Power.EnergyEstimationEngine, Power.Settings, SecureStart) and the common OEM factory-preload families (Intel PPM power profiles, AMD Power profiles, Lenovo ThinkPad power/battery packages, Surface defaults and carrier packages) and only surfaces the rest. OEM imaging leftovers such as `USMT.PPKG` or `InstallOrder.ini` are deliberately NOT allow-listed — on a device expected to be freshly provisioned via Autopilot they indicate the image was touched by another deployment workflow.\n\nProvisioning packages can apply local accounts, certificates, Wi-Fi/VPN profiles, applications (MSI/EXE) and scripts. They are legitimate in bulk-enrollment and OEM-recovery scenarios, but a PPKG appearing on a device that is expected to be \"pure Autopilot\" can indicate tampering.\n\n**To stop alerting on a known-good package:** enable the template rule **ANALYZE-SEC-006**, add the package to its allow-list regex, then disable this rule. ANALYZE-SEC-006 then only fires for packages outside your extended allow-list.", "remediation": [ { "title": "Verify the provisioning package is expected", diff --git a/rules/analyze/ANALYZE-SEC-006.json b/rules/analyze/ANALYZE-SEC-006.json index 21391963..f0fc0768 100644 --- a/rules/analyze/ANALYZE-SEC-006.json +++ b/rules/analyze/ANALYZE-SEC-006.json @@ -5,7 +5,7 @@ "description": "Template version of ANALYZE-SEC-005 with a customizable allow-list. Iterates the provisioning_package_scan artifacts[] array and fires for any PPKG whose identity is not on YOUR allow-list regex. The placeholder ships with the same OS-inbox defaults as ANALYZE-SEC-005 — extend it with your own vendor/recovery packages. This rule is OFF by default: enable it, set your allow-list, then disable ANALYZE-SEC-005 so only packages outside your list alert.", "severity": "warning", "category": "security", - "version": "2.0.0", + "version": "2.1.0", "enabled": false, "baseConfidence": 80, "conditions": [ @@ -16,12 +16,12 @@ "dataField": "artifacts", "itemField": "identity", "operator": "not_regex", - "value": "^(?:Microsoft\\.Windows\\.Cosa|Power\\.EnergyEstimationEngine|Power\\.Settings|SecureStart\\.Settings)\\b", + "value": "^(?:Microsoft\\.Windows\\.Cosa\\b|Power\\.EnergyEstimationEngine\\b|Power\\.Settings\\b|SecureStart\\.Settings\\b|PPM[-_]|PPMSettings-|AMD\\.Power\\.|TpPower|DisableBatteryDischargeEstimator\\b|PerfEppForBatterySaver\\b|AHSControl\\b|IRFeatureOptimization\\b|Surface[._]|vzw\\.ppkg\\b)", "required": true } ], "confidenceThreshold": 40, - "explanation": "A provisioning package (PPKG) was detected whose identity is NOT on your configured allow-list.\n\n- **Unexpected package:** `{{identity}}`\n\nThe `identity` value combines a package's name, .ppkg file name, ownerType and packageId, so your allow-list regex can match an approved package by any of these. The default allow-list already covers the Windows OS-inbox packages (Cosa, Power.EnergyEstimationEngine, Power.Settings, SecureStart) — extend it with your own vendor/recovery packages.\n\nIf this package should be allowed, add it to the allow-list regex (the 'value' of the 'ppkg_not_allowlisted' condition). If it is genuinely unexpected on a pure-Autopilot device, treat it as a potential manipulation and investigate how it was applied.", + "explanation": "A provisioning package (PPKG) was detected whose identity is NOT on your configured allow-list.\n\n- **Unexpected package:** `{{identity}}`\n\nThe `identity` value combines a package's name, .ppkg file name, ownerType and packageId, so your allow-list regex can match an approved package by any of these. The default allow-list already covers the Windows OS-inbox packages (Cosa, Power.EnergyEstimationEngine, Power.Settings, SecureStart) and the common OEM factory-preload families (Intel PPM, AMD Power, Lenovo ThinkPad power/battery, Surface defaults and carrier packages) — extend it with your own vendor/recovery packages.\n\nIf this package should be allowed, add it to the allow-list regex (the 'value' of the 'ppkg_not_allowlisted' condition). If it is genuinely unexpected on a pure-Autopilot device, treat it as a potential manipulation and investigate how it was applied.", "remediation": [ { "title": "Triage the unexpected package", @@ -49,7 +49,7 @@ "description": "Case-insensitive regex matched against each package's identity (name | fileName | ownerType | packageId). Packages matching this pattern are treated as approved and do NOT alert. The default is anchored (^...) so it matches only at the START of the identity — extend it by adding an alternative inside the group, e.g. add '|Acme\\.Recovery' before the closing ')'.", "conditionIndex": 0, "field": "value", - "placeholder": "^(?:Microsoft\\.Windows\\.Cosa|Power\\.EnergyEstimationEngine|Power\\.Settings|SecureStart\\.Settings)\\b" + "placeholder": "^(?:Microsoft\\.Windows\\.Cosa\\b|Power\\.EnergyEstimationEngine\\b|Power\\.Settings\\b|SecureStart\\.Settings\\b|PPM[-_]|PPMSettings-|AMD\\.Power\\.|TpPower|DisableBatteryDischargeEstimator\\b|PerfEppForBatterySaver\\b|AHSControl\\b|IRFeatureOptimization\\b|Surface[._]|vzw\\.ppkg\\b)" } ] } diff --git a/rules/dist/analyze-rules.json b/rules/dist/analyze-rules.json index 307ea669..6e208068 100644 --- a/rules/dist/analyze-rules.json +++ b/rules/dist/analyze-rules.json @@ -7,6 +7,7 @@ "description": "Detects when a Win32 app detection script fails or returns unexpected results.", "severity": "warning", "category": "apps", + "version": "1.1.0", "enabled": true, "baseConfidence": 60, "conditions": [ @@ -17,17 +18,14 @@ "dataField": "detectionResult", "operator": "equals", "value": "NotDetected", - "required": true + "required": true, + "suppressByEvent": { + "eventType": "app_install_completed", + "joinField": "appId" + } } ], - "confidenceFactors": [ - { - "signal": "error_detail_match", - "condition": "exists", - "weight": 20 - } - ], - "explanation": "A Win32 app detection script failed or did not detect the app after installation. This causes the app to appear as 'not installed' despite successful installation.", + "explanation": "A Win32 app detection script failed or did not detect the app after installation. This causes the app to appear as 'not installed' despite successful installation. Detection failures that were later resolved by a successful install of the same app are suppressed and do not raise this finding.", "remediation": [ { "title": "Fix detection rules", @@ -50,6 +48,7 @@ "description": "Detects app installation failures with specific exit codes or HRESULT error codes and suggests fixes.", "severity": "high", "category": "apps", + "version": "1.1.0", "enabled": true, "baseConfidence": 80, "conditions": [ @@ -59,7 +58,7 @@ "eventType": "app_install_failed", "dataField": "message", "operator": "regex", - "value": "16[0-9]{2}|0x80070[0-9a-fA-F]+|lpExitCode|unmapped.*exit", + "value": "\\b(1601|1602|1603|1618|1619|1620|1622|1624|1625|1633|1638|1653)\\b|0x80070[0-9a-fA-F]+|lpExitCode|unmapped.*exit", "required": true, "suppressByEvent": { "eventType": "app_install_completed", @@ -124,9 +123,10 @@ { "ruleId": "ANALYZE-APP-004", "title": "Insufficient Disk Space", - "description": "Detects when disk space is critically low during enrollment, which can cause app installation failures.", + "description": "Detects when disk space is critically low during enrollment, which can cause app installation failures. Confidence increases when app installations actually failed while disk space was low.", "severity": "critical", "category": "apps", + "version": "1.1.0", "enabled": true, "baseConfidence": 75, "conditions": [ @@ -138,6 +138,22 @@ "operator": "lt", "value": "5", "required": true + }, + { + "signal": "app_failed", + "source": "event_type", + "eventType": "app_install_failed", + "operator": "exists", + "value": "", + "required": false + }, + { + "signal": "enrollment_failed", + "source": "event_type", + "eventType": "enrollment_failed", + "operator": "exists", + "value": "", + "required": false } ], "confidenceFactors": [ @@ -152,7 +168,7 @@ "weight": 10 } ], - "explanation": "Performance monitoring shows disk free space dropped below 5 GB during enrollment. This can cause app installation failures, especially for large Win32 apps.", + "explanation": "Performance monitoring shows disk free space dropped below 5 GB during enrollment. This can cause app installation failures, especially for large Win32 apps.\n\nIf app installations failed in the same session while free space was low, treat storage exhaustion as the likely contributing cause — the confidence score of this finding is raised accordingly. Verify the individual app failures for their concrete error codes before concluding storage was the only cause.", "remediation": [ { "title": "Free disk space", @@ -161,6 +177,14 @@ "Clean up temporary files and IME cache (C:\\Windows\\IMECache)", "Consider using a larger disk image" ] + }, + { + "title": "Reduce disk consumption during enrollment", + "steps": [ + "Review the total download size of all blocking apps", + "Stagger large app installations so temporary install files do not accumulate", + "Use devices with larger drives for app-heavy deployment profiles" + ] } ], "tags": [ @@ -207,15 +231,16 @@ }, { "ruleId": "ANALYZE-APP-006", - "title": "Reboot Required Loop", - "description": "Detects when the same app installation is attempted 3 or more times, indicating a reboot-required loop.", + "title": "App Install Retry Loop", + "description": "Detects when the same app installation is attempted 3 or more times during one enrollment — a retry loop caused by unhonored reboot requests, repeated failures, or detection mismatches.", "severity": "high", "category": "apps", + "version": "1.1.0", "enabled": true, "baseConfidence": 70, "conditions": [ { - "signal": "reboot_loop", + "signal": "install_retry_loop", "source": "event_count", "eventType": "app_install_started", "dataField": "appId", @@ -224,20 +249,22 @@ "required": true } ], - "explanation": "The same app has been installed 3 or more times during this enrollment. This typically indicates the app requires a reboot between install attempts, but the ESP is not honoring the reboot request, causing an install-detect-retry loop.", + "explanation": "The same app was started 3 or more times during this enrollment. Repeated install attempts have several possible causes:\n\n- **Reboot-required loop** — the app requests a reboot (exit code 3010/1641) that the ESP does not honor, so the install-detect-retry cycle repeats\n- **Failure retries** — the install keeps failing and the IME retries it\n- **Detection mismatch** — the install succeeds but the detection rule does not match, so the app is retried as 'not installed'\n\nCheck the app's install results in this session to see which pattern applies; enrollments that legitimately span multiple reboots can also produce additional install starts.", "remediation": [ { - "title": "Fix reboot handling", + "title": "Identify the retry cause", "steps": [ - "Check if the app's return code mapping includes reboot codes (3010, 1641)", - "Verify ESP reboot behavior settings", - "Consider setting the app to 'Hard reboot' return code in Intune" + "Review this app's install started/completed/failed events in the session timeline", + "If reboot-related: check the app's return code mapping includes reboot codes (3010, 1641) and verify ESP reboot behavior settings; consider the 'Hard reboot' return code in Intune", + "If the install keeps failing: look up the failure error code (see the App Installation Error finding if it fired)", + "If the install succeeds but is retried: fix the app's detection rule (see the Win32 App Detection Script Failure finding)" ] } ], "tags": [ "apps", "reboot", + "retry", "loop" ] }, @@ -526,80 +553,13 @@ "intune" ] }, - { - "ruleId": "ANALYZE-CORR-002", - "title": "Disk Space Exhaustion Caused Installation Failure", - "description": "Correlates declining disk free space with app installation failure, confirming insufficient storage as root cause.", - "severity": "critical", - "category": "apps", - "enabled": true, - "trigger": "correlation", - "baseConfidence": 65, - "conditions": [ - { - "signal": "low_disk", - "source": "event_type", - "eventType": "performance_snapshot", - "dataField": "disk_free_gb", - "operator": "lt", - "value": "5", - "required": true - }, - { - "signal": "app_failed", - "source": "event_type", - "eventType": "app_install_failed", - "operator": "exists", - "value": "", - "required": true - } - ], - "confidenceFactors": [ - { - "signal": "multiple_failures", - "condition": "count >= 2", - "weight": 15 - }, - { - "signal": "enrollment_failed", - "condition": "exists", - "weight": 10 - } - ], - "confidenceThreshold": 50, - "explanation": "**Root cause confirmed:** Performance monitoring shows disk free space dropped below 5 GB during enrollment, and apps failed to install. The device likely ran out of storage during app installation.\n\nThis is common when deploying many large Win32 apps to devices with small drives (64 GB or 128 GB).", - "remediation": [ - { - "title": "Reduce disk consumption during enrollment", - "steps": [ - "Review total size of all apps deployed during ESP", - "Move non-critical apps to post-ESP deployment", - "Use MSIX or Store apps instead of Win32 where possible (smaller footprint)", - "Clean up IME cache: C:\\Windows\\IMECache" - ] - }, - { - "title": "Use devices with larger drives", - "steps": [ - "Minimum 128 GB recommended for standard deployments", - "256 GB recommended when deploying large app suites (Office, Visual Studio, etc.)" - ] - } - ], - "tags": [ - "correlation", - "apps", - "disk-space", - "performance", - "root-cause" - ] - }, { "ruleId": "ANALYZE-CORR-003", "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.", + "description": "Correlates an active proxy configuration with app content download failures (Delivery Optimization timeout or download error), suggesting the proxy is blocking or throttling Intune content delivery.", "severity": "high", "category": "apps", + "version": "1.1.0", "enabled": true, "trigger": "correlation", "baseConfidence": 60, @@ -614,33 +574,29 @@ "required": true }, { - "signal": "app_failed", + "signal": "download_failure", "source": "event_type", "eventType": "app_install_failed", - "operator": "exists", - "value": "", + "dataField": "errorPatternId", + "operator": "regex", + "value": "IME-DO-TIMEOUT|IME-ERROR-DOWNLOAD|IME-ERROR-NO-CONTENT", "required": true } ], "confidenceFactors": [ { - "signal": "download_timeout", - "condition": "exists", - "weight": 20 - }, - { - "signal": "multiple_failures", + "signal": "app_install_failed", "condition": "count >= 2", "weight": 15 }, { "signal": "enrollment_failed", - "condition": "exists", + "condition": "count >= 1", "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.", + "explanation": "A proxy (explicit or PAC-based) is configured on this device and app content downloads failed (Delivery Optimization timeout or download content error). Proxies can block or throttle downloads from *.delivery.mp.microsoft.com and other Intune CDN endpoints.\n\nThis is a common root cause when app downloads fail consistently on proxy-connected corporate networks. Note this rule only fires when the failure is download-related — generic app install errors behind a proxy do not trigger it.", "remediation": [ { "title": "Verify proxy bypass for Intune endpoints", @@ -675,6 +631,7 @@ "description": "Detects when Windows Hello for Business provisioning did not start within the expected timeframe after ESP completion.", "severity": "warning", "category": "device", + "version": "1.1.0", "enabled": true, "baseConfidence": 60, "conditions": [ @@ -687,13 +644,6 @@ "required": true } ], - "confidenceFactors": [ - { - "signal": "hello_policy_detected", - "condition": "exists", - "weight": 30 - } - ], "explanation": "Windows Hello for Business provisioning did not start within the expected time after ESP exit. This may indicate that the Hello policy is not configured, the device doesn't meet prerequisites, or there's a connectivity issue with the Key Registration Service.", "remediation": [ { @@ -714,21 +664,40 @@ }, { "ruleId": "ANALYZE-DEV-002", - "title": "High Memory Usage During Enrollment", - "description": "Detects when memory usage exceeds 90% during enrollment, which can cause app installation failures or system instability.", + "title": "Sustained High Memory Usage During Enrollment", + "description": "Detects when memory usage stays above 90% across at least 3 performance snapshots during enrollment, indicating sustained memory pressure that can cause app installation failures or system instability.", "severity": "warning", "category": "device", + "version": "2.0.0", "enabled": true, "baseConfidence": 55, "conditions": [ { - "signal": "high_memory", - "source": "event_type", + "signal": "sustained_high_memory", + "source": "event_count", "eventType": "performance_snapshot", - "dataField": "memory_used_percent", - "operator": "gt", - "value": "90", + "operator": "count_gte", + "value": "3", + "filterField": "memory_used_percent", + "filterOperator": "gt", + "filterValue": "90", "required": true + }, + { + "signal": "app_failed", + "source": "event_type", + "eventType": "app_install_failed", + "operator": "exists", + "value": "", + "required": false + }, + { + "signal": "enrollment_failed", + "source": "event_type", + "eventType": "enrollment_failed", + "operator": "exists", + "value": "", + "required": false } ], "confidenceFactors": [ @@ -743,7 +712,7 @@ "weight": 15 } ], - "explanation": "Memory usage exceeded 90% during enrollment. High memory pressure can cause app installation failures, system slowdowns, and ESP timeouts.", + "explanation": "Memory usage exceeded 90% in at least 3 performance snapshots during this enrollment — sustained pressure, not a single transient spike. Persistent high memory usage can cause app installation failures, system slowdowns, and ESP timeouts, especially on devices with 8 GB RAM installing several apps concurrently.", "remediation": [ { "title": "Reduce memory pressure", @@ -763,22 +732,14 @@ }, { "ruleId": "ANALYZE-DEV-003", - "title": "Unsupported Windows 11 Version", - "description": "Detects devices running Windows 11 version 22H2 or older, which is no longer supported by Microsoft.", + "title": "Unsupported Windows Version", + "description": "Detects devices enrolling with an OS build that has reached end of support: Windows 11 22H2 (build 22621) or older, which includes every Windows 10 build. Maintenance note: raise the build cutoff to 22631 after Windows 11 23H2 reaches end of support on 2026-11-10.", "severity": "warning", "category": "device", + "version": "2.0.0", "enabled": true, "baseConfidence": 90, "conditions": [ - { - "signal": "windows_11", - "source": "event_type", - "eventType": "os_info", - "dataField": "osVersion", - "operator": "contains", - "value": "Windows 11", - "required": true - }, { "signal": "old_build", "source": "event_data", @@ -787,11 +748,20 @@ "operator": "lte", "value": "22621", "required": true + }, + { + "signal": "os_version", + "source": "event_type", + "eventType": "os_info", + "dataField": "osVersion", + "operator": "exists", + "value": "", + "required": false } ], "confidenceFactors": [], "confidenceThreshold": 40, - "explanation": "This device is running Windows 11 version 22H2 (Build 22621) or older, which has reached end of support. Microsoft no longer provides security updates or bug fixes for this version. Devices running unsupported OS versions are exposed to known vulnerabilities and may fail compliance checks.", + "explanation": "This device is running an OS build that has reached end of support:\n\n- **OS:** `{{osVersion}}`\n- **Build:** `{{currentBuild}}`\n\nBuilds up to 22621 cover Windows 11 22H2 and older as well as every Windows 10 release — Microsoft no longer provides security updates or bug fixes for these versions (exception: LTSC/IoT LTSC editions follow their own lifecycle; verify the edition if this device is LTSC). Devices running unsupported OS versions are exposed to known vulnerabilities and may fail compliance checks.", "remediation": [ { "title": "Update to a supported Windows 11 version", @@ -809,7 +779,7 @@ "url": "https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information" }, { - "title": "Windows 11 22H2 end of support", + "title": "Windows lifecycle (Home and Pro)", "url": "https://learn.microsoft.com/en-us/lifecycle/products/windows-11-home-and-pro" } ], @@ -817,7 +787,8 @@ "device", "os-version", "end-of-support", - "windows-11" + "windows-11", + "windows-10" ] }, { @@ -826,7 +797,7 @@ "description": "Detects when the Autopilot enrollment has explicitly failed.", "severity": "critical", "category": "enrollment", - "version": "1.1.1", + "version": "1.1.2", "enabled": true, "baseConfidence": 95, "conditions": [ @@ -877,7 +848,7 @@ ], "confidenceFactors": [ { - "signal": "app_failed", + "signal": "failed_app", "condition": "exists", "weight": 5 } @@ -957,9 +928,10 @@ { "ruleId": "ANALYZE-ESP-001", "title": "ESP Blocking App Timeout", - "description": "Detects when the DeviceSetup phase of the ESP took longer than 30 minutes, indicating a stuck or very slow blocking app.", + "description": "Detects when the DeviceSetup phase of the ESP took longer than 30 minutes, indicating a stuck or very slow blocking app. Confidence increases when the phase exceeds 60 minutes.", "severity": "high", "category": "esp", + "version": "1.1.0", "enabled": true, "baseConfidence": 50, "conditions": [ @@ -978,10 +950,15 @@ "signal": "long_esp", "condition": "phase_duration > 1800", "weight": 40 + }, + { + "signal": "very_long_esp", + "condition": "phase_duration > 3600", + "weight": 15 } ], "confidenceThreshold": 70, - "explanation": "The ESP DeviceSetup phase took more than 30 minutes. This indicates a blocking app is stuck installing, downloading very slowly, or has entered a retry loop.", + "explanation": "The ESP DeviceSetup phase took more than 30 minutes. This indicates a blocking app is stuck installing, downloading very slowly, or has entered a retry loop. A duration beyond 60 minutes raises the confidence further — at that point the enrollment is almost certainly held by a single stuck blocking item rather than ordinary slowness.", "remediation": [ { "title": "Investigate blocking app", @@ -995,7 +972,8 @@ "tags": [ "esp", "blocking", - "timeout" + "timeout", + "duration" ] }, { @@ -1060,58 +1038,13 @@ "silent-failure" ] }, - { - "ruleId": "ANALYZE-ESP-003", - "title": "Enrollment Duration Exceeded 60 Minutes", - "description": "Detects when the total Autopilot enrollment takes longer than 60 minutes.", - "severity": "high", - "category": "esp", - "enabled": true, - "baseConfidence": 50, - "conditions": [ - { - "signal": "long_enrollment", - "source": "phase_duration", - "eventType": "esp_phase_changed", - "dataField": "espPhase", - "operator": "equals", - "value": "DeviceSetup", - "required": true - } - ], - "confidenceFactors": [ - { - "signal": "long_esp", - "condition": "phase_duration > 3600", - "weight": 40 - } - ], - "confidenceThreshold": 70, - "explanation": "The Autopilot enrollment exceeded 60 minutes. This is unusually long and may indicate stuck apps, slow network, or policy conflicts. The default ESP timeout is 60 minutes, after which the enrollment may be marked as failed.", - "remediation": [ - { - "title": "Investigate long enrollment", - "steps": [ - "Check the app installation timeline - identify which app took the longest", - "Review download speeds in the performance data", - "Consider reducing the number of ESP-blocking apps", - "Check if the ESP timeout policy is configured (default: 60 min)" - ] - } - ], - "tags": [ - "esp", - "timeout", - "duration" - ] - }, { "ruleId": "ANALYZE-ESP-004", "title": "ESP Timeout with 'Continue Anyway' (Soft Failure)", "description": "Detects an ESP terminal failure on a profile that allows 'Continue anyway'. The enrollment is reported as failed, but the user most likely dismissed the error screen and reached the desktop — a soft failure rather than an unusable device.", "severity": "warning", "category": "esp", - "version": "1.0.0", + "version": "1.0.1", "enabled": true, "baseConfidence": 80, "conditions": [ @@ -1162,7 +1095,7 @@ ], "confidenceFactors": [ { - "signal": "app_failed", + "signal": "appName", "condition": "exists", "weight": 10 } @@ -1189,13 +1122,13 @@ }, { "ruleId": "ANALYZE-ID-001", - "title": "No Machine Certificates Found", - "description": "Detects when no certificates are present in the local machine personal store (LocalMachine\\My). Missing certificates may indicate that MDM enrollment certificate provisioning or SCEP certificate deployment failed.", + "title": "Expected Machine Certificate Not Found", + "description": "Detects when a certificate with the configured subject is missing from the local machine personal store (LocalMachine\\My). Requires the gather rule GATHER-ID-002 (machine cert store inventory) to be enabled — the check runs against its gather_machine_cert_store output. A missing certificate may indicate that MDM enrollment certificate provisioning or SCEP certificate deployment failed.", "severity": "warning", "category": "identity", "isCommunity": true, "enabled": false, - "version": "1.1.0", + "version": "1.2.0", "baseConfidence": 75, "conditions": [ { @@ -1216,20 +1149,8 @@ "required": true } ], - "confidenceFactors": [ - { - "signal": "no_output_at_all", - "condition": "gather_machine_cert_store.output not_exists", - "weight": 20 - }, - { - "signal": "command_error", - "condition": "gather_machine_cert_store.error_output exists", - "weight": 10 - } - ], "confidenceThreshold": 70, - "explanation": "The expected certificate with the configured subject was not found in the local machine certificate store (LocalMachine\\My). Customize the 'value' field in the 'cert_subject_missing' condition to match your expected certificate subject (e.g. 'CN=Contoso Root CA'). The check is case-insensitive (substring match). If the certificate should have been provisioned via SCEP or MDM enrollment, this may indicate a deployment failure.", + "explanation": "The expected certificate with the configured subject was not found in the local machine certificate store (LocalMachine\\My). Customize the 'value' field in the 'cert_subject_missing' condition to match your expected certificate subject (e.g. 'CN=Contoso Root CA'). The check is case-insensitive (substring match). This rule depends on the gather rule GATHER-ID-002 delivering the machine cert store inventory (gather_machine_cert_store event) — without it the rule never evaluates. If the certificate should have been provisioned via SCEP or MDM enrollment, this may indicate a deployment failure.", "remediation": [ { "title": "Verify the expected certificate", @@ -1362,7 +1283,7 @@ "description": "Fires when the vulnerability correlation finds critical-severity CVEs or CISA KEV entries in installed software. This indicates the device has software with known, actively exploited vulnerabilities.", "severity": "critical", "category": "security", - "version": "1.0.0", + "version": "1.0.1", "author": "Autopilot Monitor", "enabled": true, "trigger": "single", @@ -1389,7 +1310,7 @@ ], "confidenceFactors": [ { - "signal": "kev_confirmed", + "signal": "kev_matches", "condition": "exists", "weight": 10 } @@ -1439,7 +1360,7 @@ "description": "Detects when the Microsoft 365 Apps (Office Click-to-Run) background install did not reach completion. The V2 OfficeInstallDetector watches the C2R client directly (registry + process) and emits office_install_failed when the install scenario was abandoned (worker gone and streaming never finished) or a C2R failure code was observed. This catches Office failures that the Intune integrated app type hides, because IME reports the app done within 1-2 minutes while C2R keeps installing in the background.", "severity": "warning", "category": "apps", - "version": "1.0.0", + "version": "1.0.1", "enabled": true, "baseConfidence": 75, "conditions": [ @@ -1450,6 +1371,15 @@ "operator": "exists", "value": "", "required": true + }, + { + "signal": "error_code_present", + "source": "event_type", + "eventType": "office_install_failed", + "dataField": "errorCode", + "operator": "exists", + "value": "", + "required": false } ], "confidenceFactors": [ @@ -1484,10 +1414,10 @@ { "ruleId": "ANALYZE-SEC-001", "title": "Secure Boot UEFI CA 2023 certificate not deployed", - "description": "Detects devices where the Windows UEFI CA 2023 certificate has not been deployed to the Secure Boot DB. These devices will lose Secure Boot security update support when the 2011 certificates expire in June 2026.", + "description": "Detects devices where the Windows UEFI CA 2023 certificate has not been deployed to the Secure Boot DB. The 2011 certificates expired in June 2026 — affected devices no longer receive Secure Boot security updates until the 2023 certificate is deployed.", "severity": "warning", "category": "security", - "version": "1.1.0", + "version": "2.0.0", "enabled": true, "baseConfidence": 70, "preconditions": [ @@ -1518,12 +1448,30 @@ "operator": "equals", "value": "true", "required": true + }, + { + "signal": "ca2023_notfound", + "source": "event_data", + "eventType": "secureboot_status", + "dataField": "uefiCA2023Status", + "operator": "equals", + "value": "notfound", + "required": false + }, + { + "signal": "ca2023_error", + "source": "event_data", + "eventType": "secureboot_status", + "dataField": "uefiCA2023Error", + "operator": "exists", + "value": "", + "required": false } ], "confidenceFactors": [ { "signal": "ca2023_notfound", - "condition": "equals:notfound", + "condition": "exists", "weight": 20 }, { @@ -1532,13 +1480,14 @@ "weight": 10 } ], - "explanation": "This device has Secure Boot enabled but the **Windows UEFI CA 2023** certificate has not been deployed to the Secure Boot DB yet.\n\nThe original Windows UEFI CA 2011 certificates expire on **June 27, 2026**. After expiration, devices without the 2023 certificate will no longer receive Secure Boot security updates and may fail to boot with updated boot managers.\n\nCurrent UEFI CA 2023 status: `{{uefiCA2023Status}}`", + "explanation": "This device has Secure Boot enabled but the **Windows UEFI CA 2023** certificate has not been deployed to the Secure Boot DB.\n\nThe original Windows UEFI CA 2011 certificates **expired on June 27, 2026 — this deadline has passed**. The automatic pre-expiry rollout window is over: this device no longer receives Secure Boot security updates (DB/DBX) and cannot trust 2023-signed boot managers until the 2023 certificate is deployed. Deploying it now requires active remediation (Windows servicing opt-in and, on some models, an OEM firmware update) rather than waiting for an automatic update.\n\nCurrent UEFI CA 2023 status: `{{uefiCA2023Status}}`", "remediation": [ { - "title": "Deploy UEFI CA 2023 certificate", + "title": "Deploy UEFI CA 2023 certificate now", "steps": [ "Deploy KB5036210 via Windows Update or WSUS", "Use Intune policy to enable Secure Boot DB update (Windows UEFI CA 2023)", + "Check with the device OEM whether a firmware update is required for the DB update to apply on this model", "Alternatively, use the WinCS CLI tool from Microsoft for manual deployment", "Verify deployment: check registry HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecureBoot\\Servicing\\UEFICA2023Status = 'Updated'", "After DB update: ensure the boot manager is updated to a 2023-signed version via monthly cumulative update" @@ -1547,7 +1496,7 @@ ], "relatedDocs": [ { - "title": "Microsoft: Secure Boot certificates expire June 2026", + "title": "Microsoft: Secure Boot certificates expired June 2026 — act now", "url": "https://techcommunity.microsoft.com/blog/windows-itpro-blog/act-now-secure-boot-certificates-expire-in-june-2026/4426856" }, { @@ -1694,10 +1643,10 @@ { "ruleId": "ANALYZE-SEC-005", "title": "Unexpected Provisioning Package", - "description": "Detects provisioning packages (PPKG) that are not on the built-in allow-list. The agent emits one or more provisioning_package_scan events whose artifacts[] arrays list every PPKG (registry package, .ppkg file, or Recovery\\Customizations residue; a large set is split across several events). This rule iterates that array across all of them and fires for any artifact whose identity does not match the allow-list regex. The default allow-list covers the Windows OS-inbox packages that ship on every device (Cosa, Power.EnergyEstimationEngine, Power.Settings, SecureStart) so a clean device stays quiet. To allow your own vendor/recovery packages, enable the template rule ANALYZE-SEC-006, extend its allow-list, then disable this rule.", + "description": "Detects provisioning packages (PPKG) that are not on the built-in allow-list. The agent emits one or more provisioning_package_scan events whose artifacts[] arrays list every PPKG (registry package, .ppkg file, or Recovery\\Customizations residue; a large set is split across several events). This rule iterates that array across all of them and fires for any artifact whose identity does not match the allow-list regex. The default allow-list covers the Windows OS-inbox packages (Cosa, Power.EnergyEstimationEngine, Power.Settings, SecureStart) plus common OEM factory-preload families (Intel PPM power profiles, AMD Power profiles, Lenovo ThinkPad power/battery packages, Microsoft Surface defaults and carrier packages) so clean factory devices stay quiet. OEM imaging leftovers (e.g. USMT.PPKG, InstallOrder.ini) and generically named packages deliberately still alert — on a pure-Autopilot device they are worth a look. To allow your own vendor/recovery packages, enable the template rule ANALYZE-SEC-006, extend its allow-list, then disable this rule.", "severity": "warning", "category": "security", - "version": "2.0.0", + "version": "2.1.0", "enabled": true, "baseConfidence": 80, "conditions": [ @@ -1708,12 +1657,12 @@ "dataField": "artifacts", "itemField": "identity", "operator": "not_regex", - "value": "^(?:Microsoft\\.Windows\\.Cosa|Power\\.EnergyEstimationEngine|Power\\.Settings|SecureStart\\.Settings)\\b", + "value": "^(?:Microsoft\\.Windows\\.Cosa\\b|Power\\.EnergyEstimationEngine\\b|Power\\.Settings\\b|SecureStart\\.Settings\\b|PPM[-_]|PPMSettings-|AMD\\.Power\\.|TpPower|DisableBatteryDischargeEstimator\\b|PerfEppForBatterySaver\\b|AHSControl\\b|IRFeatureOptimization\\b|Surface[._]|vzw\\.ppkg\\b)", "required": true } ], "confidenceThreshold": 40, - "explanation": "A provisioning package (PPKG) was detected that is NOT on the allow-list.\n\n- **Unexpected package:** `{{identity}}`\n\nThe agent reports every PPKG it finds in `provisioning_package_scan` event(s) (see their `artifacts` array(s) for the full list, plus file metadata and best-effort content indicators; large sets span several events). This rule ignores the Windows OS-inbox packages that ship on every device (Cosa, Power.EnergyEstimationEngine, Power.Settings, SecureStart) and only surfaces the rest.\n\nProvisioning packages can apply local accounts, certificates, Wi-Fi/VPN profiles, applications (MSI/EXE) and scripts. They are legitimate in bulk-enrollment and OEM-recovery scenarios, but a PPKG appearing on a device that is expected to be \"pure Autopilot\" can indicate tampering.\n\n**To stop alerting on a known-good package:** enable the template rule **ANALYZE-SEC-006**, add the package to its allow-list regex, then disable this rule. ANALYZE-SEC-006 then only fires for packages outside your extended allow-list.", + "explanation": "A provisioning package (PPKG) was detected that is NOT on the allow-list.\n\n- **Unexpected package:** `{{identity}}`\n\nThe agent reports every PPKG it finds in `provisioning_package_scan` event(s) (see their `artifacts` array(s) for the full list, plus file metadata and best-effort content indicators; large sets span several events). This rule ignores the Windows OS-inbox packages (Cosa, Power.EnergyEstimationEngine, Power.Settings, SecureStart) and the common OEM factory-preload families (Intel PPM power profiles, AMD Power profiles, Lenovo ThinkPad power/battery packages, Surface defaults and carrier packages) and only surfaces the rest. OEM imaging leftovers such as `USMT.PPKG` or `InstallOrder.ini` are deliberately NOT allow-listed — on a device expected to be freshly provisioned via Autopilot they indicate the image was touched by another deployment workflow.\n\nProvisioning packages can apply local accounts, certificates, Wi-Fi/VPN profiles, applications (MSI/EXE) and scripts. They are legitimate in bulk-enrollment and OEM-recovery scenarios, but a PPKG appearing on a device that is expected to be \"pure Autopilot\" can indicate tampering.\n\n**To stop alerting on a known-good package:** enable the template rule **ANALYZE-SEC-006**, add the package to its allow-list regex, then disable this rule. ANALYZE-SEC-006 then only fires for packages outside your extended allow-list.", "remediation": [ { "title": "Verify the provisioning package is expected", @@ -1740,7 +1689,7 @@ "description": "Template version of ANALYZE-SEC-005 with a customizable allow-list. Iterates the provisioning_package_scan artifacts[] array and fires for any PPKG whose identity is not on YOUR allow-list regex. The placeholder ships with the same OS-inbox defaults as ANALYZE-SEC-005 — extend it with your own vendor/recovery packages. This rule is OFF by default: enable it, set your allow-list, then disable ANALYZE-SEC-005 so only packages outside your list alert.", "severity": "warning", "category": "security", - "version": "2.0.0", + "version": "2.1.0", "enabled": false, "baseConfidence": 80, "conditions": [ @@ -1751,12 +1700,12 @@ "dataField": "artifacts", "itemField": "identity", "operator": "not_regex", - "value": "^(?:Microsoft\\.Windows\\.Cosa|Power\\.EnergyEstimationEngine|Power\\.Settings|SecureStart\\.Settings)\\b", + "value": "^(?:Microsoft\\.Windows\\.Cosa\\b|Power\\.EnergyEstimationEngine\\b|Power\\.Settings\\b|SecureStart\\.Settings\\b|PPM[-_]|PPMSettings-|AMD\\.Power\\.|TpPower|DisableBatteryDischargeEstimator\\b|PerfEppForBatterySaver\\b|AHSControl\\b|IRFeatureOptimization\\b|Surface[._]|vzw\\.ppkg\\b)", "required": true } ], "confidenceThreshold": 40, - "explanation": "A provisioning package (PPKG) was detected whose identity is NOT on your configured allow-list.\n\n- **Unexpected package:** `{{identity}}`\n\nThe `identity` value combines a package's name, .ppkg file name, ownerType and packageId, so your allow-list regex can match an approved package by any of these. The default allow-list already covers the Windows OS-inbox packages (Cosa, Power.EnergyEstimationEngine, Power.Settings, SecureStart) — extend it with your own vendor/recovery packages.\n\nIf this package should be allowed, add it to the allow-list regex (the 'value' of the 'ppkg_not_allowlisted' condition). If it is genuinely unexpected on a pure-Autopilot device, treat it as a potential manipulation and investigate how it was applied.", + "explanation": "A provisioning package (PPKG) was detected whose identity is NOT on your configured allow-list.\n\n- **Unexpected package:** `{{identity}}`\n\nThe `identity` value combines a package's name, .ppkg file name, ownerType and packageId, so your allow-list regex can match an approved package by any of these. The default allow-list already covers the Windows OS-inbox packages (Cosa, Power.EnergyEstimationEngine, Power.Settings, SecureStart) and the common OEM factory-preload families (Intel PPM, AMD Power, Lenovo ThinkPad power/battery, Surface defaults and carrier packages) — extend it with your own vendor/recovery packages.\n\nIf this package should be allowed, add it to the allow-list regex (the 'value' of the 'ppkg_not_allowlisted' condition). If it is genuinely unexpected on a pure-Autopilot device, treat it as a potential manipulation and investigate how it was applied.", "remediation": [ { "title": "Triage the unexpected package", @@ -1790,7 +1739,7 @@ "description": "Case-insensitive regex matched against each package's identity (name | fileName | ownerType | packageId). Packages matching this pattern are treated as approved and do NOT alert. The default is anchored (^...) so it matches only at the START of the identity — extend it by adding an alternative inside the group, e.g. add '|Acme\\.Recovery' before the closing ')'.", "conditionIndex": 0, "field": "value", - "placeholder": "^(?:Microsoft\\.Windows\\.Cosa|Power\\.EnergyEstimationEngine|Power\\.Settings|SecureStart\\.Settings)\\b" + "placeholder": "^(?:Microsoft\\.Windows\\.Cosa\\b|Power\\.EnergyEstimationEngine\\b|Power\\.Settings\\b|SecureStart\\.Settings\\b|PPM[-_]|PPMSettings-|AMD\\.Power\\.|TpPower|DisableBatteryDischargeEstimator\\b|PerfEppForBatterySaver\\b|AHSControl\\b|IRFeatureOptimization\\b|Surface[._]|vzw\\.ppkg\\b)" } ] }, diff --git a/rules/schema/analyze-rule.schema.json b/rules/schema/analyze-rule.schema.json index 65199558..82796536 100644 --- a/rules/schema/analyze-rule.schema.json +++ b/rules/schema/analyze-rule.schema.json @@ -187,6 +187,19 @@ "description": "Whether this condition must match for the rule to fire. If false, it only contributes to confidence scoring.", "default": false }, + "filterField": { + "type": "string", + "description": "For event_count only: optional value filter — only events whose filterField satisfies filterOperator/filterValue are counted (e.g. count only performance_snapshot events with memory_used_percent > 90). Applies before counting for both count_gte and count_per_group_gte." + }, + "filterOperator": { + "type": "string", + "description": "Operator for the event_count value filter.", + "enum": ["equals", "not_equals", "contains", "not_contains", "regex", "not_regex", "gt", "lt", "gte", "lte", "exists", "not_exists", "in", "not_in"] + }, + "filterValue": { + "type": "string", + "description": "Value for the event_count value filter." + }, "correlateEventType": { "type": "string", "description": "The second event type to correlate with (Event B). Only used when source = \"event_correlation\"." diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/RuleEngineArrayConditionTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/RuleEngineArrayConditionTests.cs index 47f207cc..7c44cd2c 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/RuleEngineArrayConditionTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/RuleEngineArrayConditionTests.cs @@ -17,8 +17,9 @@ public class RuleEngineArrayConditionTests private const string TenantId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"; private const string SessionId = "b2c3d4e5-f6a7-8901-bcde-f12345678901"; - // Mirrors the ANALYZE-SEC-005 default allow-list (OS-inbox families) — ANCHORED at start. - private const string AllowList = @"^(?:Microsoft\.Windows\.Cosa|Power\.EnergyEstimationEngine|Power\.Settings|SecureStart\.Settings)\b"; + // Mirrors the ANALYZE-SEC-005 default allow-list (OS-inbox + OEM factory-preload families) — + // ANCHORED at start; word boundaries per alternative because the OEM entries are prefixes. + private const string AllowList = @"^(?:Microsoft\.Windows\.Cosa\b|Power\.EnergyEstimationEngine\b|Power\.Settings\b|SecureStart\.Settings\b|PPM[-_]|PPMSettings-|AMD\.Power\.|TpPower|DisableBatteryDischargeEstimator\b|PerfEppForBatterySaver\b|AHSControl\b|IRFeatureOptimization\b|Surface[._]|vzw\.ppkg\b)"; [Fact] public async Task AllArtifactsAllowListed_RuleDoesNotFire() @@ -74,6 +75,53 @@ public class RuleEngineArrayConditionTests Assert.Equal(2, Convert.ToInt32(evidence["matchCount"])); } + [Fact] + public async Task OemFactoryPreloadFamilies_AreAllowListed() + { + // Telemetry-driven curation (2026-07): Intel PPM, AMD Power, Lenovo ThinkPad + // power/battery, and Surface/carrier packages ship in-box on factory devices and + // must not alert — including Windows' " (1)" duplicate-name variants. + var events = new List + { + ScanEvent( + "PPM-Win11-24H2-ARL-v2025.0603113409.2be47c2.ppkg", + "PPM_Better_Battery_Bestpowerefficiency.ppkg", + "PPMSettings-8380Profiles.wd.ppkg", + "AMD.Power.NVMe (1).ppkg", + "TpPowerSlider_Mobile.ppkg", + "DisableBatteryDischargeEstimator.ppkg", + "PerfEppForBatterySaver.ppkg", + "AHSControl.ppkg", + "IRFeatureOptimization.ppkg", + "Surface.Power.Policy.Defaults.ppkg", + "surface_esim.ppkg", + "vzw.ppkg") + }; + + var outcome = await RunAsync(MakeAllowListRule(), events); + + Assert.Empty(outcome.Results); + } + + [Fact] + public async Task OemImagingLeftovers_StillFire() + { + // Deliberate decision: USMT.PPKG / InstallOrder.ini (imaging-workflow residue) and + // generically named packages stay OUT of the allow-list — on a pure-Autopilot + // device they indicate the image was touched by another deployment workflow. + var events = new List + { + ScanEvent("AMD.Power.NVMe.ppkg", "USMT.PPKG", "InstallOrder.ini", "Apps.ppkg") + }; + + var outcome = await RunAsync(MakeAllowListRule(), events); + + var result = Assert.Single(outcome.Results); + var evidence = (IDictionary)result.MatchedConditions["ppkg_not_allowlisted"]; + Assert.Equal(3, Convert.ToInt32(evidence["matchCount"])); + Assert.Equal("USMT.PPKG", evidence["value"]); + } + [Fact] public async Task EmptyArtifacts_RuleDoesNotFire() { diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/RuleEngineDetectionFailureTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/RuleEngineDetectionFailureTests.cs index a16ec53f..8a5e3691 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/RuleEngineDetectionFailureTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/RuleEngineDetectionFailureTests.cs @@ -115,7 +115,7 @@ public class RuleEngineDetectionFailureTests // pull appName from app_install_failed (optional condition with dataField=appName) // and resolve {{appName}} via the byField path. var rule = BuiltInAnalyzeRules.GetAll().First(r => r.RuleId == "ANALYZE-ENRL-001"); - Assert.Equal("1.1.1", rule.Version); + Assert.Equal("1.1.2", rule.Version); Assert.True(rule.Enabled); var events = new List diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/RuleEngineEventCountFilterTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/RuleEngineEventCountFilterTests.cs new file mode 100644 index 00000000..df21cb99 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions.Tests/RuleEngineEventCountFilterTests.cs @@ -0,0 +1,201 @@ +using AutopilotMonitor.Functions.Services; +using AutopilotMonitor.Shared.DataAccess; +using AutopilotMonitor.Shared.Models; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; + +namespace AutopilotMonitor.Functions.Tests; + +/// +/// Pins the optional value filter on the event_count condition source +/// (FilterField/FilterOperator/FilterValue): only events whose filter field satisfies the +/// operator are counted. Backs "sustained" rules like ANALYZE-DEV-002, which requires +/// memory_used_percent > 90 across at least 3 performance snapshots instead of firing +/// on a single transient spike. +/// +public class RuleEngineEventCountFilterTests +{ + private const string TenantId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"; + private const string SessionId = "b2c3d4e5-f6a7-8901-bcde-f12345678901"; + + [Fact] + public async Task SustainedHighMemory_ThreeSnapshotsAboveThreshold_Fires() + { + var events = Snapshots(95, 93, 40, 96, 50); + + var outcome = await RunAsync(MakeSustainedMemoryRule(), events); + + var result = Assert.Single(outcome.Results); + Assert.Equal("ANALYZE-DEV-002", result.RuleId); + var evidence = Assert.IsAssignableFrom>(result.MatchedConditions["sustained_high_memory"]); + Assert.Equal(3, Convert.ToInt32(evidence["count"])); + Assert.Equal(3, Convert.ToInt32(evidence["threshold"])); + } + + [Fact] + public async Task TransientSpike_OnlyTwoSnapshotsAboveThreshold_DoesNotFire() + { + // The pre-filter must drop the below-threshold snapshots — an unfiltered count + // (5 events >= 3) would fire here. + var events = Snapshots(95, 40, 50, 60, 93); + + var outcome = await RunAsync(MakeSustainedMemoryRule(), events); + + Assert.Empty(outcome.Results); + } + + [Fact] + public async Task NoFilterConfigured_CountsAllEventsOfType() + { + // Backward compatibility: without FilterField/FilterOperator the classic + // count_gte semantics (count every event of the type) must be unchanged. + var rule = MakeSustainedMemoryRule(); + rule.Conditions[0].FilterField = null!; + rule.Conditions[0].FilterOperator = null!; + rule.Conditions[0].FilterValue = null!; + + var events = Snapshots(10, 20, 30); + + var outcome = await RunAsync(rule, events); + + Assert.Single(outcome.Results); + } + + [Fact] + public async Task FilterAppliesToPerGroupCount() + { + // The filter runs before grouping: only failed installs of the same app count. + var rule = new AnalyzeRule + { + RuleId = "ANALYZE-TST-001", + Title = "Grouped count with filter", + Severity = "warning", + Category = "apps", + Enabled = true, + // Synthetic rule ID not in the embedded built-in catalog: without this flag the + // AnalyzeRuleService live-catalog filter would hide it as sunset-pending + // (AnalyzeRule.IsBuiltIn defaults to true). + IsBuiltIn = false, + BaseConfidence = 80, + ConfidenceThreshold = 40, + Conditions = new List + { + new() + { + Signal = "repeated_not_detected", + Source = "event_count", + EventType = "app_install_failed", + DataField = "appId", + Operator = "count_per_group_gte", + Value = "2", + FilterField = "detectionResult", + FilterOperator = "equals", + FilterValue = "NotDetected", + Required = true + } + }, + Explanation = "test" + }; + + // app-1: two NotDetected → fires. app-2: two events but only one NotDetected. + var events = new List + { + AppFailedEvent("app-1", "NotDetected", 1), + AppFailedEvent("app-1", "NotDetected", 2), + AppFailedEvent("app-2", "NotDetected", 3), + AppFailedEvent("app-2", "Error", 4) + }; + + var outcome = await RunAsync(rule, events); + + var result = Assert.Single(outcome.Results); + var evidence = Assert.IsAssignableFrom>(result.MatchedConditions["repeated_not_detected"]); + Assert.Equal("app-1", evidence["groupKey"]); + Assert.Equal(2, Convert.ToInt32(evidence["count"])); + } + + // ===== Helpers ===== + + /// Mirrors the shipped ANALYZE-DEV-002 v2 condition shape. + private static AnalyzeRule MakeSustainedMemoryRule() => new() + { + RuleId = "ANALYZE-DEV-002", + Title = "Sustained High Memory Usage During Enrollment", + Severity = "warning", + Category = "device", + Enabled = true, + BaseConfidence = 55, + ConfidenceThreshold = 40, + Conditions = new List + { + new() + { + Signal = "sustained_high_memory", + Source = "event_count", + EventType = "performance_snapshot", + Operator = "count_gte", + Value = "3", + FilterField = "memory_used_percent", + FilterOperator = "gt", + FilterValue = "90", + Required = true + } + }, + Explanation = "test" + }; + + private static List Snapshots(params int[] memoryUsedPercents) + { + var events = new List(); + for (int i = 0; i < memoryUsedPercents.Length; i++) + { + events.Add(new EnrollmentEvent + { + EventId = Guid.NewGuid().ToString(), + TenantId = TenantId, + SessionId = SessionId, + EventType = "performance_snapshot", + Timestamp = DateTime.UtcNow.AddMinutes(i), + Sequence = i + 1, + Data = new Dictionary + { + ["memory_used_percent"] = memoryUsedPercents[i], + ["disk_free_gb"] = 100 + } + }); + } + return events; + } + + private static EnrollmentEvent AppFailedEvent(string appId, string detectionResult, int sequence) => new() + { + EventId = Guid.NewGuid().ToString(), + TenantId = TenantId, + SessionId = SessionId, + EventType = "app_install_failed", + Timestamp = DateTime.UtcNow.AddMinutes(sequence), + Sequence = sequence, + Data = new Dictionary + { + ["appId"] = appId, + ["detectionResult"] = detectionResult + } + }; + + private static async Task RunAsync(AnalyzeRule rule, List events) + { + var ruleRepo = new Mock(); + ruleRepo.Setup(r => r.GetAnalyzeRulesAsync("global")).ReturnsAsync(new List { rule }); + ruleRepo.Setup(r => r.GetAnalyzeRulesAsync(TenantId)).ReturnsAsync(new List()); + ruleRepo.Setup(r => r.GetRuleStatesAsync(It.IsAny())).ReturnsAsync(new Dictionary()); + ruleRepo.Setup(r => r.GetRuleResultsAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List()); + + var sessionRepo = new Mock(); + sessionRepo.Setup(s => s.GetSessionEventsStrictAsync(TenantId, SessionId, It.IsAny())).ReturnsAsync(events); + + var ruleService = new AnalyzeRuleService(ruleRepo.Object, NullLogger.Instance); + var engine = new RuleEngine(ruleService, ruleRepo.Object, sessionRepo.Object, NullLogger.Instance); + + return await engine.AnalyzeSessionAsync(TenantId, SessionId); + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.ConditionEvaluators.cs b/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.ConditionEvaluators.cs index 8c05d8ce..71da89a0 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.ConditionEvaluators.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/RuleEngine.ConditionEvaluators.cs @@ -346,6 +346,15 @@ namespace AutopilotMonitor.Functions.Services { var matchingEvents = events.Where(e => MatchesEventType(e, condition.EventType)).ToList(); + // Optional value filter: count only events whose FilterField satisfies FilterOperator/FilterValue + // (e.g. only performance_snapshot events with memory_used_percent > 90). + if (!string.IsNullOrEmpty(condition.FilterField) && !string.IsNullOrEmpty(condition.FilterOperator)) + { + matchingEvents = matchingEvents + .Where(e => MatchesOperator(GetDataFieldValue(e, condition.FilterField) ?? string.Empty, condition.FilterOperator, condition.FilterValue)) + .ToList(); + } + // count_per_group_gte: group by DataField value (e.g. appId), fire if any group >= threshold if (condition.Operator == "count_per_group_gte" && !string.IsNullOrEmpty(condition.DataField) && int.TryParse(condition.Value, out var groupThreshold)) { diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Rules/AnalyzeRule.cs b/src/Shared/AutopilotMonitor.Shared/Models/Rules/AnalyzeRule.cs index aa8cad58..9d55a015 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Rules/AnalyzeRule.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Rules/AnalyzeRule.cs @@ -226,6 +226,27 @@ namespace AutopilotMonitor.Shared.Models /// public bool Required { get; set; } = false; + // ===== Event Count Filter Properties ===== + // Used only when Source = "event_count" + + /// + /// Optional value filter for "event_count": only events whose FilterField + /// satisfies FilterOperator/FilterValue are counted (e.g. count only + /// performance_snapshot events with memory_used_percent > 90). + /// Applies before counting for both count_gte and count_per_group_gte. + /// + public string FilterField { get; set; } = default!; + + /// + /// Operator for the count filter. Uses same operators as the main Operator field. + /// + public string FilterOperator { get; set; } = default!; + + /// + /// Value for the count filter. + /// + public string FilterValue { get; set; } = default!; + // ===== Event Correlation Properties ===== // Used only when Source = "event_correlation" diff --git a/src/Web/autopilot-monitor-web/app/analyze-rules/types.ts b/src/Web/autopilot-monitor-web/app/analyze-rules/types.ts index 7bd84607..4b19a6d4 100644 --- a/src/Web/autopilot-monitor-web/app/analyze-rules/types.ts +++ b/src/Web/autopilot-monitor-web/app/analyze-rules/types.ts @@ -24,6 +24,12 @@ export interface RuleCondition { required: boolean; // event_data_array: sub-field tested on each array element (e.g. "identity") itemField?: string; + // event_count: optional value filter applied before counting + filterField?: string; + filterOperator?: string; + filterValue?: string; + // optional suppression by a resolving event (same joinField value) + suppressByEvent?: { eventType: string; joinField: string } | null; // event_correlation fields correlateEventType?: string; joinField?: string;