diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/DiagnosticsPackageService.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/DiagnosticsPackageService.cs
index d9dd9390..38f3f133 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/DiagnosticsPackageService.cs
+++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/DiagnosticsPackageService.cs
@@ -525,40 +525,69 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime
continue;
}
- if (length > tracker.Budget.MaxSingleFileBytes)
+ // Active event-log channels hold their .evtx exclusively locked — a raw
+ // FileStream open fails with a sharing violation. Export the channel via
+ // wevtutil instead and stream the export (deleted below). Session a11102f4:
+ // the tenant-configured BootstrapperAgent channel never made it into the ZIP.
+ string streamSource = file;
+ string tempExport = null;
+ if (file.EndsWith(".evtx", StringComparison.OrdinalIgnoreCase))
{
- _logger.Warning($"Skipping oversized file ({length} bytes > {tracker.Budget.MaxSingleFileBytes} cap): {file}");
- tracker.RecordSkip(file, "size", length);
- continue;
- }
- if (tracker.WouldExceedCount())
- {
- _logger.Warning($"Skipping file (file-count cap {tracker.Budget.MaxFileCount} reached): {file}");
- tracker.RecordSkip(file, "count", length);
- continue;
- }
- if (tracker.WouldExceedTotal(length))
- {
- _logger.Warning($"Skipping file (total-bytes cap {tracker.Budget.MaxTotalUncompressedBytes} reached): {file}");
- tracker.RecordSkip(file, "total", length);
- continue;
+ tempExport = TryExportEventLogChannel(file);
+ if (tempExport == null)
+ {
+ tracker.RecordSkip(file, "evtx-export", length);
+ continue;
+ }
+ streamSource = tempExport;
+ try { length = new FileInfo(tempExport).Length; }
+ catch { /* keep original length estimate */ }
}
- // Preserve subfolder structure in the ZIP when includeSubfolders is enabled
- var relativePath = file.Substring(sourceFolder.Length).TrimStart(Path.DirectorySeparatorChar);
- var entryName = $"{zipFolder}/{relativePath.Replace(Path.DirectorySeparatorChar, '/')}";
-
- var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);
- // Stream directly from disk into the entry — no per-file MemoryStream/byte[]
- // copy. FileShare.ReadWrite avoids locking conflicts with active log writers.
- using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
- using (var entryStream = entry.Open())
+ try
{
- fs.CopyTo(entryStream);
- }
+ if (length > tracker.Budget.MaxSingleFileBytes)
+ {
+ _logger.Warning($"Skipping oversized file ({length} bytes > {tracker.Budget.MaxSingleFileBytes} cap): {file}");
+ tracker.RecordSkip(file, "size", length);
+ continue;
+ }
+ if (tracker.WouldExceedCount())
+ {
+ _logger.Warning($"Skipping file (file-count cap {tracker.Budget.MaxFileCount} reached): {file}");
+ tracker.RecordSkip(file, "count", length);
+ continue;
+ }
+ if (tracker.WouldExceedTotal(length))
+ {
+ _logger.Warning($"Skipping file (total-bytes cap {tracker.Budget.MaxTotalUncompressedBytes} reached): {file}");
+ tracker.RecordSkip(file, "total", length);
+ continue;
+ }
- tracker.RecordIncluded(length);
- _logger.Debug($"Added to diagnostics package: {entryName} ({length / 1024} KB)");
+ // Preserve subfolder structure in the ZIP when includeSubfolders is enabled
+ var relativePath = file.Substring(sourceFolder.Length).TrimStart(Path.DirectorySeparatorChar);
+ var entryName = $"{zipFolder}/{relativePath.Replace(Path.DirectorySeparatorChar, '/')}";
+
+ var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);
+ // Stream directly from disk into the entry — no per-file MemoryStream/byte[]
+ // copy. FileShare.ReadWrite avoids locking conflicts with active log writers.
+ using (var fs = new FileStream(streamSource, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ using (var entryStream = entry.Open())
+ {
+ fs.CopyTo(entryStream);
+ }
+
+ tracker.RecordIncluded(length);
+ _logger.Debug($"Added to diagnostics package: {entryName} ({length / 1024} KB)");
+ }
+ finally
+ {
+ if (tempExport != null)
+ {
+ try { File.Delete(tempExport); } catch { }
+ }
+ }
}
catch (Exception ex)
{
@@ -567,6 +596,59 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime
}
}
+ ///
+ /// Exports the event-log channel behind a winevt .evtx file to a temp copy via
+ /// wevtutil epl. Channel name is derived from the file name (%4 decodes to '/',
+ /// e.g. "Microsoft-Autopilot-BootstrapperAgent%4BootstrapperAgentServiceLogProvider.evtx"
+ /// → "Microsoft-Autopilot-BootstrapperAgent/BootstrapperAgentServiceLogProvider").
+ /// Returns the temp file path (caller deletes) or null when the export failed.
+ ///
+ private string TryExportEventLogChannel(string evtxPath)
+ {
+ string tempPath = null;
+ try
+ {
+ var channel = Path.GetFileNameWithoutExtension(evtxPath).Replace("%4", "/");
+ tempPath = Path.Combine(Path.GetTempPath(), $"am-evtx-{Guid.NewGuid():N}.evtx");
+ var psi = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = Path.Combine(Environment.SystemDirectory, "wevtutil.exe"),
+ Arguments = $"epl \"{channel}\" \"{tempPath}\" /ow:true",
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardError = true,
+ };
+ using (var process = System.Diagnostics.Process.Start(psi))
+ {
+ if (process == null) return null;
+ if (!process.WaitForExit(30000))
+ {
+ try { process.Kill(); } catch { }
+ _logger.Warning($"wevtutil export timed out for channel '{channel}'.");
+ try { File.Delete(tempPath); } catch { }
+ return null;
+ }
+ if (process.ExitCode != 0)
+ {
+ var stderr = process.StandardError.ReadToEnd();
+ _logger.Warning($"wevtutil export failed for channel '{channel}' (exit {process.ExitCode}): {stderr}");
+ try { File.Delete(tempPath); } catch { }
+ return null;
+ }
+ }
+ return File.Exists(tempPath) ? tempPath : null;
+ }
+ catch (Exception ex)
+ {
+ _logger.Warning($"Event log export failed for {evtxPath}: {ex.Message}");
+ if (tempPath != null)
+ {
+ try { File.Delete(tempPath); } catch { }
+ }
+ return null;
+ }
+ }
+
private static void WriteTruncatedMarker(ZipArchive archive, BudgetTracker tracker)
{
var sb = new StringBuilder();
diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.NetworkAndSecurity.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.NetworkAndSecurity.cs
index bf73e678..95d394ff 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.NetworkAndSecurity.cs
+++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.NetworkAndSecurity.cs
@@ -156,7 +156,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo
///
private (string enrollmentType, bool isHybridJoin, int? autopilotMode) CollectAutopilotProfile()
{
- var detectedType = "v1";
+ // Single source of truth for the rail classification — the SAME detector the
+ // session registration uses (incl. the WDP DevicePreparation subkey probe). The
+ // previous inline CloudAssigned*-only re-implementation kept reporting "v1" on
+ // Device Preparation devices while the session row already said "v2" (a11102f4).
+ var (detectedType, detectionRule) = AutopilotMonitor.Agent.V2.Core.Security.EnrollmentRegistryDetector.DetectEnrollmentTypeWithRule();
var isHybridJoin = false;
int? detectedAutopilotMode = null;
@@ -188,20 +192,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo
}
}
- // Extract key values for enrollment type detection
- var deviceReg = data.ContainsKey("CloudAssignedDeviceRegistration")
- ? data["CloudAssignedDeviceRegistration"]?.ToString()
- : null;
- var espEnabled = data.ContainsKey("CloudAssignedEspEnabled")
- ? data["CloudAssignedEspEnabled"]?.ToString()
- : null;
-
- // Determine enrollment type: WDP if DeviceRegistration=2 or ESP explicitly disabled
- if (deviceReg == "2" || espEnabled == "0")
- detectedType = "v2";
- else
- detectedType = "v1";
-
// --- Interpret AutopilotMode (profile capability, not live state) ---
// 0 = User Driven, 1 = Self Deploying, 2 = Pre-Provisioning (White Glove)
var autopilotModeStr = data.ContainsKey("AutopilotMode")
@@ -377,11 +367,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo
{ "enrollmentType", detectedType },
{ "CloudAssignedDeviceRegistration", data.ContainsKey("CloudAssignedDeviceRegistration") ? data["CloudAssignedDeviceRegistration"] : "n/a" },
{ "CloudAssignedEspEnabled", data.ContainsKey("CloudAssignedEspEnabled") ? data["CloudAssignedEspEnabled"] : "n/a" },
- { "detectionRule", detectedType == "v2"
- ? (data.ContainsKey("CloudAssignedDeviceRegistration") && data["CloudAssignedDeviceRegistration"]?.ToString() == "2"
- ? "CloudAssignedDeviceRegistration=2"
- : "CloudAssignedEspEnabled=0")
- : "default (no v2 indicators)" }
+ { "detectionRule", detectionRule }
});
_logger.Info($"EnrollmentTracker: enrollment type detected: {detectedType}");
diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Security/EnrollmentRegistryDetector.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Security/EnrollmentRegistryDetector.cs
index 6de5c353..73b173bc 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Security/EnrollmentRegistryDetector.cs
+++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Security/EnrollmentRegistryDetector.cs
@@ -186,7 +186,15 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
/// Defaults to "v1" on any error so a flaky registry does not misclassify a
/// classic session as WDP.
///
- public static string DetectEnrollmentType()
+ public static string DetectEnrollmentType() => DetectEnrollmentTypeWithRule().enrollmentType;
+
+ ///
+ /// Same as but also returns WHICH rule fired, so the
+ /// enrollment_type_detected event can name its evidence instead of re-implementing the
+ /// detection (the duplicate inline logic in DeviceInfoCollector missed the WDP subkey
+ /// rule and kept reporting "v1" on Device Preparation devices — session a11102f4).
+ ///
+ public static (string enrollmentType, string detectionRule) DetectEnrollmentTypeWithRule()
{
try
{
@@ -194,25 +202,26 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
AutopilotSettingsKey + @"\DevicePreparation\BootstrapperAgent"))
{
var executionContext = wdpKey?.GetValue("ExecutionContext")?.ToString();
- if (!string.IsNullOrEmpty(executionContext)) return "v2";
+ if (!string.IsNullOrEmpty(executionContext))
+ return ("v2", @"DevicePreparation\BootstrapperAgent\ExecutionContext present");
}
using (var key = Registry.LocalMachine.OpenSubKey(AutopilotSettingsKey))
{
- if (key == null) return "v1";
+ if (key == null) return ("v1", "AutopilotSettings key missing");
var deviceReg = key.GetValue("CloudAssignedDeviceRegistration")?.ToString();
- if (deviceReg == "2") return "v2";
+ if (deviceReg == "2") return ("v2", "CloudAssignedDeviceRegistration=2");
var espEnabled = key.GetValue("CloudAssignedEspEnabled")?.ToString();
- if (espEnabled == "0") return "v2";
+ if (espEnabled == "0") return ("v2", "CloudAssignedEspEnabled=0");
}
}
catch
{
// Fall through to v1 default.
}
- return "v1";
+ return ("v1", "default (no v2 indicators)");
}
}
}