mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
feat(agent): added IntegrityBypassAnalyzer
This commit is contained in:
+116
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AutopilotMonitor.Agent.Core.Monitoring.Telemetry.Analyzers;
|
||||
using AutopilotMonitor.Agent.Core.Tests.Helpers;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.Agent.Core.Tests.Monitoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Sanity tests for IntegrityBypassAnalyzer. The analyzer reads HKLM/HKU, Win32_Tpm
|
||||
/// and the real filesystem — we don't mock those here. Instead we verify the event
|
||||
/// contract (type / phase / severity / required payload shape) against whatever the
|
||||
/// test host reports. On any healthy dev machine this should emit a single
|
||||
/// "integrity_bypass_analysis" event with Phase=Unknown and the documented payload.
|
||||
/// </summary>
|
||||
public class IntegrityBypassAnalyzerTests
|
||||
{
|
||||
private IntegrityBypassAnalyzer CreateAnalyzer(List<EnrollmentEvent> sink)
|
||||
{
|
||||
return new IntegrityBypassAnalyzer(
|
||||
sessionId: "test-session",
|
||||
tenantId: "test-tenant",
|
||||
emitEvent: evt => sink.Add(evt),
|
||||
logger: TestLogger.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeAtStartup_EmitsSingleIntegrityBypassEvent_WithUnknownPhase()
|
||||
{
|
||||
var events = new List<EnrollmentEvent>();
|
||||
var analyzer = CreateAnalyzer(events);
|
||||
|
||||
analyzer.AnalyzeAtStartup();
|
||||
|
||||
var evt = Assert.Single(events);
|
||||
Assert.Equal("integrity_bypass_analysis", evt.EventType);
|
||||
Assert.Equal(EnrollmentPhase.Unknown, evt.Phase);
|
||||
Assert.Equal("IntegrityBypassAnalyzer", evt.Source);
|
||||
Assert.Equal("test-session", evt.SessionId);
|
||||
Assert.Equal("test-tenant", evt.TenantId);
|
||||
Assert.NotNull(evt.Data);
|
||||
Assert.True(evt.Data.ContainsKey("severity"));
|
||||
Assert.True(evt.Data.ContainsKey("finding"));
|
||||
Assert.True(evt.Data.ContainsKey("findings"));
|
||||
Assert.True(evt.Data.ContainsKey("triggered_at"));
|
||||
Assert.Equal("startup", evt.Data["triggered_at"]);
|
||||
Assert.True(evt.Data.ContainsKey("checks"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeAtShutdown_EmitsSingleIntegrityBypassEvent_WithShutdownTrigger()
|
||||
{
|
||||
var events = new List<EnrollmentEvent>();
|
||||
var analyzer = CreateAnalyzer(events);
|
||||
|
||||
analyzer.AnalyzeAtShutdown();
|
||||
|
||||
var evt = Assert.Single(events);
|
||||
Assert.Equal("integrity_bypass_analysis", evt.EventType);
|
||||
Assert.Equal(EnrollmentPhase.Unknown, evt.Phase);
|
||||
Assert.Equal("shutdown", evt.Data["triggered_at"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Payload_ContainsDocumentedCheckSections()
|
||||
{
|
||||
var events = new List<EnrollmentEvent>();
|
||||
var analyzer = CreateAnalyzer(events);
|
||||
|
||||
analyzer.AnalyzeAtStartup();
|
||||
|
||||
var evt = Assert.Single(events);
|
||||
var checks = Assert.IsAssignableFrom<Dictionary<string, object>>(evt.Data["checks"]);
|
||||
Assert.True(checks.ContainsKey("lab_config"));
|
||||
Assert.True(checks.ContainsKey("mo_setup"));
|
||||
Assert.True(checks.ContainsKey("pchc_upgrade_eligibility"));
|
||||
Assert.True(checks.ContainsKey("setup_scripts"));
|
||||
Assert.True(checks.ContainsKey("correlation"));
|
||||
|
||||
// Setup scripts entry is always a list of 2 (SetupComplete + ErrorHandler)
|
||||
var scripts = Assert.IsAssignableFrom<System.Collections.IEnumerable>(checks["setup_scripts"]);
|
||||
var count = scripts.Cast<object>().Count();
|
||||
Assert.Equal(2, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Severity_IsLowerCaseString()
|
||||
{
|
||||
var events = new List<EnrollmentEvent>();
|
||||
var analyzer = CreateAnalyzer(events);
|
||||
|
||||
analyzer.AnalyzeAtStartup();
|
||||
|
||||
var evt = Assert.Single(events);
|
||||
var severity = Assert.IsType<string>(evt.Data["severity"]);
|
||||
Assert.Equal(severity, severity.ToLowerInvariant());
|
||||
// Must be one of the documented bucket labels
|
||||
Assert.Contains(severity, new[] { "trace", "debug", "info", "warning", "error", "critical" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullConstructorArgs_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new IntegrityBypassAnalyzer(null, "t", evt => { }, TestLogger.Instance));
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new IntegrityBypassAnalyzer("s", null, evt => { }, TestLogger.Instance));
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new IntegrityBypassAnalyzer("s", "t", null, TestLogger.Instance));
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new IntegrityBypassAnalyzer("s", "t", evt => { }, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,21 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Runtime
|
||||
_logger.Info("SoftwareInventoryAnalyzer disabled by remote config");
|
||||
}
|
||||
|
||||
if (analyzerConfig.EnableIntegrityBypassAnalyzer)
|
||||
{
|
||||
_analyzers.Add(new IntegrityBypassAnalyzer(
|
||||
_configuration.SessionId,
|
||||
_configuration.TenantId,
|
||||
_emitEvent,
|
||||
_logger
|
||||
));
|
||||
_logger.Info("IntegrityBypassAnalyzer registered");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Info("IntegrityBypassAnalyzer disabled by remote config");
|
||||
}
|
||||
|
||||
_logger.Info($"Analyzers initialized: {_analyzers.Count} active");
|
||||
}
|
||||
|
||||
|
||||
+492
@@ -0,0 +1,492 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Management;
|
||||
using AutopilotMonitor.Agent.Core.Logging;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace AutopilotMonitor.Agent.Core.Monitoring.Telemetry.Analyzers
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects Windows 11 "unsupported hardware" installations where the PC Health Check /
|
||||
/// Setup-time hardware gates were bypassed via registry flags, and flags suspicious
|
||||
/// post-OOBE setup scripts. Such devices may complete enrollment successfully yet lack
|
||||
/// TPM-backed features (WHfB, BitLocker TPM protector, Device Health Attestation,
|
||||
/// Credential Guard) — a silent quality risk not otherwise surfaced by enrollment_complete.
|
||||
///
|
||||
/// Checks performed:
|
||||
/// 1. HKLM\SYSTEM\Setup\LabConfig\Bypass{TPM,SecureBoot,RAM,Storage,Disk,CPU}Check (install-time)
|
||||
/// 2. HKLM\SYSTEM\Setup\MoSetup\AllowUpgradesWithUnsupportedTPMOrCPU (in-place upgrade)
|
||||
/// 3. HKU\<SID>\SOFTWARE\Microsoft\PCHC\UpgradeEligibility (WU upgrade bypass, per-user)
|
||||
/// 4. %WINDIR%\Setup\Scripts\SetupComplete.cmd + ErrorHandler.cmd (suspicious post-OOBE hooks)
|
||||
///
|
||||
/// Correlation:
|
||||
/// Bypass flags are cross-checked against current TPM and SecureBoot state; a bypass
|
||||
/// combined with a missing security feature is escalated to "critical".
|
||||
///
|
||||
/// Emits a single "integrity_bypass_analysis" event at startup and at shutdown,
|
||||
/// enabling delta detection (e.g. scripts that appear between enrollment start and end).
|
||||
/// </summary>
|
||||
public class IntegrityBypassAnalyzer : IAgentAnalyzer
|
||||
{
|
||||
private readonly string _sessionId;
|
||||
private readonly string _tenantId;
|
||||
private readonly Action<EnrollmentEvent> _emitEvent;
|
||||
private readonly AgentLogger _logger;
|
||||
|
||||
public string Name => "IntegrityBypassAnalyzer";
|
||||
|
||||
// LabConfig bypass value names. Both BypassStorageCheck and BypassDiskCheck are observed
|
||||
// in the wild — scripts differ, both should be captured.
|
||||
private static readonly string[] LabConfigBypassValues = new[]
|
||||
{
|
||||
"BypassTPMCheck",
|
||||
"BypassSecureBootCheck",
|
||||
"BypassRAMCheck",
|
||||
"BypassStorageCheck",
|
||||
"BypassDiskCheck",
|
||||
"BypassCPUCheck"
|
||||
};
|
||||
|
||||
// Registry base SIDs that are NOT real user profiles
|
||||
private static readonly HashSet<string> SystemSids = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".DEFAULT",
|
||||
"S-1-5-18", // LocalSystem
|
||||
"S-1-5-19", // LocalService
|
||||
"S-1-5-20" // NetworkService
|
||||
};
|
||||
|
||||
public IntegrityBypassAnalyzer(
|
||||
string sessionId,
|
||||
string tenantId,
|
||||
Action<EnrollmentEvent> emitEvent,
|
||||
AgentLogger logger)
|
||||
{
|
||||
_sessionId = sessionId ?? throw new ArgumentNullException(nameof(sessionId));
|
||||
_tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId));
|
||||
_emitEvent = emitEvent ?? throw new ArgumentNullException(nameof(emitEvent));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public void AnalyzeAtStartup()
|
||||
{
|
||||
_logger.Info($"{Name}: Running startup analysis");
|
||||
RunAnalysis("startup");
|
||||
}
|
||||
|
||||
public void AnalyzeAtShutdown()
|
||||
{
|
||||
_logger.Info($"{Name}: Running shutdown analysis");
|
||||
RunAnalysis("shutdown");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Core analysis
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private void RunAnalysis(string trigger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var labConfig = CheckLabConfig();
|
||||
var moSetup = CheckMoSetupAllowUnsupported();
|
||||
var pchc = CheckPchcUpgradeEligibility();
|
||||
var setupScripts = CheckSetupScripts();
|
||||
|
||||
var tpmPresent = CheckTpmPresentAndEnabled();
|
||||
var secureBootEnabled = CheckSecureBootEnabled();
|
||||
|
||||
// --------- Severity aggregation ---------
|
||||
var severity = EventSeverity.Info;
|
||||
var findings = new List<string>();
|
||||
|
||||
bool anyLabConfigBypass = labConfig.ActiveBypasses.Count > 0;
|
||||
bool labConfigKeyExists = labConfig.KeyExists;
|
||||
bool moSetupBypass = moSetup.Value == 1;
|
||||
bool pchcBypass = pchc.AnyUserWithFlag;
|
||||
bool scriptsPresent = setupScripts.Any(s => s.Exists);
|
||||
|
||||
// Critical: bypass + corresponding feature missing
|
||||
if (labConfig.ActiveBypasses.Contains("BypassTPMCheck") && tpmPresent.HasValue && tpmPresent.Value == false)
|
||||
{
|
||||
severity = Escalate(severity, EventSeverity.Critical);
|
||||
findings.Add("tpm_bypass_with_missing_tpm");
|
||||
}
|
||||
if (labConfig.ActiveBypasses.Contains("BypassSecureBootCheck") && secureBootEnabled.HasValue && secureBootEnabled.Value == false)
|
||||
{
|
||||
severity = Escalate(severity, EventSeverity.Critical);
|
||||
findings.Add("secureboot_bypass_with_disabled_secureboot");
|
||||
}
|
||||
if (moSetupBypass && ((tpmPresent.HasValue && tpmPresent.Value == false) || (secureBootEnabled.HasValue && secureBootEnabled.Value == false)))
|
||||
{
|
||||
severity = Escalate(severity, EventSeverity.Critical);
|
||||
findings.Add("upgrade_bypass_with_missing_feature");
|
||||
}
|
||||
|
||||
// Warning: bypass active but feature currently still OK, or scripts present, or PCHC flag
|
||||
if (anyLabConfigBypass && severity < EventSeverity.Warning)
|
||||
{
|
||||
severity = Escalate(severity, EventSeverity.Warning);
|
||||
findings.Add("labconfig_bypass_active");
|
||||
}
|
||||
if (moSetupBypass && severity < EventSeverity.Warning)
|
||||
{
|
||||
severity = Escalate(severity, EventSeverity.Warning);
|
||||
findings.Add("mosetup_upgrade_bypass_active");
|
||||
}
|
||||
if (pchcBypass)
|
||||
{
|
||||
severity = Escalate(severity, EventSeverity.Warning);
|
||||
findings.Add("pchc_upgrade_eligibility_forced");
|
||||
}
|
||||
if (scriptsPresent)
|
||||
{
|
||||
severity = Escalate(severity, EventSeverity.Warning);
|
||||
findings.Add("suspicious_setup_scripts_present");
|
||||
}
|
||||
|
||||
// Info: key exists but nothing active
|
||||
if (severity == EventSeverity.Info)
|
||||
{
|
||||
if (labConfigKeyExists)
|
||||
findings.Add("labconfig_key_present_inactive");
|
||||
else
|
||||
findings.Add("no_bypass_indicators");
|
||||
}
|
||||
|
||||
string findingLabel = findings.FirstOrDefault() ?? "no_bypass_indicators";
|
||||
string message = severity >= EventSeverity.Warning
|
||||
? $"{Name}: Integrity bypass indicators detected ({string.Join(",", findings)})"
|
||||
: $"{Name}: No integrity bypass indicators";
|
||||
|
||||
_logger.Info(
|
||||
$"{Name}: severity={severity}, findings=[{string.Join(",", findings)}], " +
|
||||
$"labConfigBypasses={labConfig.ActiveBypasses.Count}, " +
|
||||
$"moSetupBypass={moSetupBypass}, pchcBypass={pchcBypass}, " +
|
||||
$"scriptsPresent={scriptsPresent}, tpm={FormatNullableBool(tpmPresent)}, " +
|
||||
$"secureBoot={FormatNullableBool(secureBootEnabled)}");
|
||||
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
{ "severity", severity.ToString().ToLowerInvariant() },
|
||||
{ "finding", findingLabel },
|
||||
{ "findings", findings },
|
||||
{ "triggered_at", trigger },
|
||||
{ "enrollment_phase_at_check", EnrollmentPhase.Unknown.ToString() },
|
||||
{ "checks", new Dictionary<string, object>
|
||||
{
|
||||
{ "lab_config", new Dictionary<string, object>
|
||||
{
|
||||
{ "key_exists", labConfig.KeyExists },
|
||||
{ "values", labConfig.Values },
|
||||
{ "active_bypasses", labConfig.ActiveBypasses }
|
||||
}
|
||||
},
|
||||
{ "mo_setup", new Dictionary<string, object>
|
||||
{
|
||||
{ "key_exists", moSetup.KeyExists },
|
||||
{ "value", moSetup.Value },
|
||||
{ "flagged", moSetupBypass }
|
||||
}
|
||||
},
|
||||
{ "pchc_upgrade_eligibility", new Dictionary<string, object>
|
||||
{
|
||||
{ "users_checked", pchc.UsersChecked },
|
||||
{ "users_with_flag", pchc.UsersWithFlag },
|
||||
{ "any_user_with_flag", pchc.AnyUserWithFlag }
|
||||
}
|
||||
},
|
||||
{ "setup_scripts", setupScripts.Select(s => new Dictionary<string, object>
|
||||
{
|
||||
{ "path", s.Path },
|
||||
{ "exists", s.Exists },
|
||||
{ "size_bytes", s.SizeBytes },
|
||||
{ "last_modified_utc", s.LastModifiedUtc.HasValue
|
||||
? s.LastModifiedUtc.Value.ToString("O")
|
||||
: null }
|
||||
}).ToList()
|
||||
},
|
||||
{ "correlation", new Dictionary<string, object>
|
||||
{
|
||||
{ "tpm_present_and_enabled", tpmPresent },
|
||||
{ "secure_boot_enabled", secureBootEnabled }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_emitEvent(new EnrollmentEvent
|
||||
{
|
||||
SessionId = _sessionId,
|
||||
TenantId = _tenantId,
|
||||
EventType = "integrity_bypass_analysis",
|
||||
Severity = severity,
|
||||
Source = Name,
|
||||
Phase = EnrollmentPhase.Unknown,
|
||||
Message = message,
|
||||
Data = data
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"{Name}: Analysis failed unexpectedly", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static EventSeverity Escalate(EventSeverity current, EventSeverity candidate)
|
||||
=> candidate > current ? candidate : current;
|
||||
|
||||
private static string FormatNullableBool(bool? value)
|
||||
=> value.HasValue ? value.Value.ToString() : "unknown";
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Individual checks
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private LabConfigCheckResult CheckLabConfig()
|
||||
{
|
||||
var result = new LabConfigCheckResult();
|
||||
try
|
||||
{
|
||||
using (var key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\Setup\LabConfig", writable: false))
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
_logger.Debug($"{Name}: HKLM\\SYSTEM\\Setup\\LabConfig not present");
|
||||
return result;
|
||||
}
|
||||
|
||||
result.KeyExists = true;
|
||||
foreach (var valueName in LabConfigBypassValues)
|
||||
{
|
||||
var raw = key.GetValue(valueName);
|
||||
int intValue = 0;
|
||||
if (raw != null)
|
||||
{
|
||||
try { intValue = Convert.ToInt32(raw); }
|
||||
catch { intValue = 0; }
|
||||
}
|
||||
result.Values[valueName] = intValue;
|
||||
if (intValue == 1)
|
||||
{
|
||||
result.ActiveBypasses.Add(valueName);
|
||||
_logger.Debug($"{Name}: LabConfig {valueName}=1 (active bypass)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"{Name}: Failed to read LabConfig registry: {ex.Message}");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private MoSetupCheckResult CheckMoSetupAllowUnsupported()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\Setup\MoSetup", writable: false))
|
||||
{
|
||||
if (key == null)
|
||||
return new MoSetupCheckResult { KeyExists = false, Value = 0 };
|
||||
|
||||
var raw = key.GetValue("AllowUpgradesWithUnsupportedTPMOrCPU");
|
||||
if (raw == null)
|
||||
return new MoSetupCheckResult { KeyExists = true, Value = 0 };
|
||||
|
||||
int intValue = 0;
|
||||
try { intValue = Convert.ToInt32(raw); } catch { intValue = 0; }
|
||||
return new MoSetupCheckResult { KeyExists = true, Value = intValue };
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"{Name}: Failed to read MoSetup registry: {ex.Message}");
|
||||
return new MoSetupCheckResult { KeyExists = false, Value = 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates HKEY_USERS and checks SOFTWARE\Microsoft\PCHC\UpgradeEligibility for each
|
||||
/// real user profile. Agent runs as SYSTEM — loaded hives include all interactive user
|
||||
/// profiles that Windows has mounted. SIDs for .DEFAULT and system accounts are skipped.
|
||||
/// Profiles whose hive is not currently loaded are not visible here; that is acceptable —
|
||||
/// the relevant user during enrollment is the primary logged-in user whose hive is loaded.
|
||||
/// </summary>
|
||||
private PchcCheckResult CheckPchcUpgradeEligibility()
|
||||
{
|
||||
var result = new PchcCheckResult();
|
||||
try
|
||||
{
|
||||
using (var users = Registry.Users)
|
||||
{
|
||||
foreach (var sid in users.GetSubKeyNames())
|
||||
{
|
||||
if (string.IsNullOrEmpty(sid)) continue;
|
||||
if (SystemSids.Contains(sid)) continue;
|
||||
// Skip the Classes-hive companions
|
||||
if (sid.EndsWith("_Classes", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
|
||||
result.UsersChecked.Add(sid);
|
||||
|
||||
try
|
||||
{
|
||||
using (var pchcKey = users.OpenSubKey($@"{sid}\SOFTWARE\Microsoft\PCHC", writable: false))
|
||||
{
|
||||
if (pchcKey == null) continue;
|
||||
|
||||
var raw = pchcKey.GetValue("UpgradeEligibility");
|
||||
if (raw == null) continue;
|
||||
|
||||
int intValue = 0;
|
||||
try { intValue = Convert.ToInt32(raw); } catch { intValue = 0; }
|
||||
|
||||
if (intValue == 1)
|
||||
{
|
||||
result.UsersWithFlag.Add(sid);
|
||||
_logger.Debug($"{Name}: PCHC UpgradeEligibility=1 for SID {sid}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug($"{Name}: Failed to read PCHC for SID {sid}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"{Name}: Failed to enumerate HKEY_USERS: {ex.Message}");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<SetupScriptCheckResult> CheckSetupScripts()
|
||||
{
|
||||
var results = new List<SetupScriptCheckResult>();
|
||||
var windir = Environment.GetEnvironmentVariable("WINDIR");
|
||||
if (string.IsNullOrEmpty(windir))
|
||||
windir = @"C:\Windows";
|
||||
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(windir, @"Setup\Scripts\SetupComplete.cmd"),
|
||||
Path.Combine(windir, @"Setup\Scripts\ErrorHandler.cmd")
|
||||
};
|
||||
|
||||
foreach (var path in candidates)
|
||||
{
|
||||
var entry = new SetupScriptCheckResult { Path = path, Exists = false };
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
entry.Exists = true;
|
||||
var fi = new FileInfo(path);
|
||||
entry.SizeBytes = fi.Length;
|
||||
entry.LastModifiedUtc = fi.LastWriteTimeUtc;
|
||||
_logger.Debug($"{Name}: Setup script present: {path} ({fi.Length} bytes)");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug($"{Name}: Failed to stat {path}: {ex.Message}");
|
||||
}
|
||||
results.Add(entry);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a TPM is present and enabled, false if definitely absent/disabled,
|
||||
/// null if indeterminate (query failed). Uses Win32_Tpm in root\CIMV2\Security\MicrosoftTpm.
|
||||
/// </summary>
|
||||
private bool? CheckTpmPresentAndEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var searcher = new ManagementObjectSearcher(
|
||||
@"root\CIMV2\Security\MicrosoftTpm",
|
||||
"SELECT IsEnabled_InitialValue, IsActivated_InitialValue FROM Win32_Tpm"))
|
||||
{
|
||||
foreach (ManagementObject tpm in searcher.Get())
|
||||
{
|
||||
var isEnabled = tpm["IsEnabled_InitialValue"] != null
|
||||
&& Convert.ToBoolean(tpm["IsEnabled_InitialValue"]);
|
||||
// At least one Tpm instance exists — return its enabled state
|
||||
return isEnabled;
|
||||
}
|
||||
}
|
||||
// No Win32_Tpm instance returned — no TPM present
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug($"{Name}: TPM WMI query failed: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if UEFI Secure Boot is enabled, false if disabled, null if not
|
||||
/// queryable (e.g. BIOS firmware without the registry key).
|
||||
/// </summary>
|
||||
private bool? CheckSecureBootEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var key = Registry.LocalMachine.OpenSubKey(
|
||||
@"SYSTEM\CurrentControlSet\Control\SecureBoot\State", writable: false))
|
||||
{
|
||||
if (key == null) return null;
|
||||
var raw = key.GetValue("UEFISecureBootEnabled");
|
||||
if (raw == null) return null;
|
||||
return Convert.ToInt32(raw) == 1;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug($"{Name}: SecureBoot registry read failed: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Private result types
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private class LabConfigCheckResult
|
||||
{
|
||||
public bool KeyExists { get; set; }
|
||||
public Dictionary<string, int> Values { get; set; } = new Dictionary<string, int>();
|
||||
public List<string> ActiveBypasses { get; set; } = new List<string>();
|
||||
}
|
||||
|
||||
private class MoSetupCheckResult
|
||||
{
|
||||
public bool KeyExists { get; set; }
|
||||
public int Value { get; set; }
|
||||
}
|
||||
|
||||
private class PchcCheckResult
|
||||
{
|
||||
public List<string> UsersChecked { get; set; } = new List<string>();
|
||||
public List<string> UsersWithFlag { get; set; } = new List<string>();
|
||||
public bool AnyUserWithFlag => UsersWithFlag.Count > 0;
|
||||
}
|
||||
|
||||
private class SetupScriptCheckResult
|
||||
{
|
||||
public string Path { get; set; }
|
||||
public bool Exists { get; set; }
|
||||
public long SizeBytes { get; set; }
|
||||
public DateTime? LastModifiedUtc { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
1026
|
||||
1028
|
||||
|
||||
+2
@@ -424,6 +424,7 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage
|
||||
{ "SendTraceEvents", config.SendTraceEvents },
|
||||
{ "EnableLocalAdminAnalyzer", config.EnableLocalAdminAnalyzer },
|
||||
{ "EnableSoftwareInventoryAnalyzer", config.EnableSoftwareInventoryAnalyzer },
|
||||
{ "EnableIntegrityBypassAnalyzer", config.EnableIntegrityBypassAnalyzer },
|
||||
{ "LocalAdminAllowedAccountsJson", config.LocalAdminAllowedAccountsJson },
|
||||
{ "BootstrapTokenEnabled", config.BootstrapTokenEnabled },
|
||||
{ "UnrestrictedModeEnabled", config.UnrestrictedModeEnabled },
|
||||
@@ -503,6 +504,7 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage
|
||||
SendTraceEvents = entity.GetBoolean("SendTraceEvents") ?? true,
|
||||
EnableLocalAdminAnalyzer = entity.GetBoolean("EnableLocalAdminAnalyzer"),
|
||||
EnableSoftwareInventoryAnalyzer = entity.GetBoolean("EnableSoftwareInventoryAnalyzer"),
|
||||
EnableIntegrityBypassAnalyzer = entity.GetBoolean("EnableIntegrityBypassAnalyzer"),
|
||||
LocalAdminAllowedAccountsJson = entity.GetString("LocalAdminAllowedAccountsJson"),
|
||||
BootstrapTokenEnabled = entity.GetBoolean("BootstrapTokenEnabled") ?? false,
|
||||
UnrestrictedModeEnabled = entity.GetBoolean("UnrestrictedModeEnabled") ?? false,
|
||||
|
||||
@@ -141,7 +141,7 @@ namespace AutopilotMonitor.Functions.Functions.Config
|
||||
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new AgentConfigResponse
|
||||
{
|
||||
ConfigVersion = 24, // Configurable ModernDeploymentHarmlessEventIds (noise suppression)
|
||||
ConfigVersion = 25, // IntegrityBypassAnalyzer flag
|
||||
UploadIntervalSeconds = 10,
|
||||
SelfDestructOnComplete = tenantConfig.SelfDestructOnComplete ?? true,
|
||||
KeepLogFile = tenantConfig.KeepLogFile ?? false,
|
||||
@@ -165,7 +165,8 @@ namespace AutopilotMonitor.Functions.Functions.Config
|
||||
{
|
||||
EnableLocalAdminAnalyzer = tenantConfig.EnableLocalAdminAnalyzer ?? true,
|
||||
LocalAdminAllowedAccounts = tenantConfig.GetLocalAdminAllowedAccounts(),
|
||||
EnableSoftwareInventoryAnalyzer = tenantConfig.EnableSoftwareInventoryAnalyzer ?? false
|
||||
EnableSoftwareInventoryAnalyzer = tenantConfig.EnableSoftwareInventoryAnalyzer ?? false,
|
||||
EnableIntegrityBypassAnalyzer = tenantConfig.EnableIntegrityBypassAnalyzer ?? true
|
||||
},
|
||||
LatestAgentSha256 = adminConfig.LatestAgentSha256,
|
||||
LatestAgentExeSha256 = adminConfig.LatestAgentExeSha256,
|
||||
|
||||
@@ -397,5 +397,14 @@ namespace AutopilotMonitor.Shared.Models
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool EnableSoftwareInventoryAnalyzer { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to run the IntegrityBypassAnalyzer at startup and shutdown.
|
||||
/// Detects Windows 11 TPM/SecureBoot/CPU/RAM/Disk install-time bypass keys,
|
||||
/// the MoSetup upgrade bypass flag, the per-user PCHC UpgradeEligibility flag,
|
||||
/// and suspicious post-OOBE SetupComplete/ErrorHandler scripts.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool EnableIntegrityBypassAnalyzer { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +308,12 @@ namespace AutopilotMonitor.Shared.Models
|
||||
/// </summary>
|
||||
public bool? EnableSoftwareInventoryAnalyzer { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the IntegrityBypassAnalyzer is enabled for this tenant's devices.
|
||||
/// null = use agent default (true).
|
||||
/// </summary>
|
||||
public bool? EnableIntegrityBypassAnalyzer { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// JSON-serialized list of additional local account names that are considered expected
|
||||
/// on a newly enrolled device (merged with built-in defaults on the agent).
|
||||
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import type { EnrollmentEvent } from "@/types";
|
||||
|
||||
interface IntegrityBypassSectionProps {
|
||||
events: EnrollmentEvent[];
|
||||
expanded: boolean;
|
||||
setExpanded: (v: boolean) => void;
|
||||
}
|
||||
|
||||
interface LabConfigPayload {
|
||||
key_exists?: boolean;
|
||||
values?: Record<string, number>;
|
||||
active_bypasses?: string[];
|
||||
}
|
||||
|
||||
interface MoSetupPayload {
|
||||
key_exists?: boolean;
|
||||
value?: number;
|
||||
flagged?: boolean;
|
||||
}
|
||||
|
||||
interface PchcPayload {
|
||||
users_checked?: string[];
|
||||
users_with_flag?: string[];
|
||||
any_user_with_flag?: boolean;
|
||||
}
|
||||
|
||||
interface SetupScriptEntry {
|
||||
path?: string;
|
||||
exists?: boolean;
|
||||
size_bytes?: number;
|
||||
last_modified_utc?: string | null;
|
||||
}
|
||||
|
||||
interface CorrelationPayload {
|
||||
tpm_present_and_enabled?: boolean | null;
|
||||
secure_boot_enabled?: boolean | null;
|
||||
}
|
||||
|
||||
interface ChecksPayload {
|
||||
lab_config?: LabConfigPayload;
|
||||
mo_setup?: MoSetupPayload;
|
||||
pchc_upgrade_eligibility?: PchcPayload;
|
||||
setup_scripts?: SetupScriptEntry[];
|
||||
correlation?: CorrelationPayload;
|
||||
}
|
||||
|
||||
interface AnalysisPayload {
|
||||
severity?: string;
|
||||
finding?: string;
|
||||
findings?: string[];
|
||||
triggered_at?: string;
|
||||
checks?: ChecksPayload;
|
||||
}
|
||||
|
||||
const severityOrder: Record<string, number> = {
|
||||
critical: 4,
|
||||
error: 3,
|
||||
warning: 2,
|
||||
info: 1,
|
||||
debug: 0,
|
||||
trace: -1,
|
||||
};
|
||||
|
||||
const severityBadgeColors: Record<string, string> = {
|
||||
critical: "bg-red-100 text-red-700 border-red-200",
|
||||
error: "bg-red-100 text-red-700 border-red-200",
|
||||
warning: "bg-amber-100 text-amber-700 border-amber-200",
|
||||
info: "bg-blue-100 text-blue-700 border-blue-200",
|
||||
clean: "bg-emerald-100 text-emerald-700 border-emerald-200",
|
||||
};
|
||||
|
||||
function pickDominant(events: EnrollmentEvent[]): { shutdown: EnrollmentEvent | null; startup: EnrollmentEvent | null; any: EnrollmentEvent | null } {
|
||||
const analyses = events
|
||||
.filter((e) => e.eventType === "integrity_bypass_analysis")
|
||||
.sort((a, b) => a.sequence - b.sequence);
|
||||
const shutdown = [...analyses].reverse().find((e) => (e.data as AnalysisPayload | undefined)?.triggered_at === "shutdown") ?? null;
|
||||
const startup = analyses.find((e) => (e.data as AnalysisPayload | undefined)?.triggered_at === "startup") ?? null;
|
||||
const any = shutdown ?? startup ?? (analyses.length > 0 ? analyses[analyses.length - 1] : null);
|
||||
return { shutdown, startup, any };
|
||||
}
|
||||
|
||||
function severityLabel(sev?: string, findings?: string[]): string {
|
||||
if (!sev) return "unknown";
|
||||
const lower = sev.toLowerCase();
|
||||
if (lower === "info" && findings && findings.includes("no_bypass_indicators")) return "clean";
|
||||
return lower;
|
||||
}
|
||||
|
||||
function describeTpm(v: boolean | null | undefined): string {
|
||||
if (v === true) return "TPM enabled";
|
||||
if (v === false) return "TPM missing / disabled";
|
||||
return "TPM status unknown";
|
||||
}
|
||||
|
||||
function describeSecureBoot(v: boolean | null | undefined): string {
|
||||
if (v === true) return "SecureBoot enabled";
|
||||
if (v === false) return "SecureBoot disabled";
|
||||
return "SecureBoot status unknown";
|
||||
}
|
||||
|
||||
export default function IntegrityBypassSection({ events, expanded, setExpanded }: IntegrityBypassSectionProps) {
|
||||
const { shutdown, startup, any } = useMemo(() => pickDominant(events), [events]);
|
||||
const primary = shutdown ?? any;
|
||||
|
||||
if (!primary) return null;
|
||||
|
||||
const payload = (primary.data ?? {}) as AnalysisPayload;
|
||||
const severity = severityLabel(payload.severity, payload.findings);
|
||||
const badgeClass = severityBadgeColors[severity] ?? "bg-gray-100 text-gray-700 border-gray-200";
|
||||
|
||||
const activeBypasses = payload.checks?.lab_config?.active_bypasses ?? [];
|
||||
const labValues = payload.checks?.lab_config?.values ?? {};
|
||||
const moSetupFlagged = payload.checks?.mo_setup?.flagged === true;
|
||||
const pchcFlagged = payload.checks?.pchc_upgrade_eligibility?.any_user_with_flag === true;
|
||||
const pchcUsers = payload.checks?.pchc_upgrade_eligibility?.users_with_flag ?? [];
|
||||
const setupScripts = (payload.checks?.setup_scripts ?? []).filter((s) => s.exists === true);
|
||||
const corr = payload.checks?.correlation ?? {};
|
||||
|
||||
// Delta summary (startup vs shutdown) — focus on the facts that change in practice
|
||||
const startupPayload = (startup?.data ?? {}) as AnalysisPayload | undefined;
|
||||
const startupActive = startupPayload?.checks?.lab_config?.active_bypasses ?? [];
|
||||
const newlyAppeared = activeBypasses.filter((b) => !startupActive.includes(b));
|
||||
const newScripts = setupScripts.filter((s) => {
|
||||
const startScripts = startupPayload?.checks?.setup_scripts ?? [];
|
||||
return !startScripts.some((ss) => ss.path === s.path && ss.exists === true);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-6 mb-6">
|
||||
<div
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center justify-between w-full text-left cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-rose-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Integrity Bypass Analyzer</h2>
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold border ${badgeClass}`}>
|
||||
{severity.toUpperCase()}
|
||||
</span>
|
||||
{payload.finding && (
|
||||
<span className="text-xs text-gray-500 font-mono">{payload.finding}</span>
|
||||
)}
|
||||
</div>
|
||||
<svg className={`w-5 h-5 text-gray-400 transition-transform duration-200 ${expanded ? 'rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="mt-4 space-y-4 text-sm text-gray-700">
|
||||
{/* Correlation row — always visible to give context */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="inline-flex items-center px-2 py-1 rounded bg-gray-50 border border-gray-200 text-xs">
|
||||
{describeTpm(corr.tpm_present_and_enabled)}
|
||||
</span>
|
||||
<span className="inline-flex items-center px-2 py-1 rounded bg-gray-50 border border-gray-200 text-xs">
|
||||
{describeSecureBoot(corr.secure_boot_enabled)}
|
||||
</span>
|
||||
{startup && shutdown && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded bg-gray-50 border border-gray-200 text-xs text-gray-500">
|
||||
Startup + Shutdown checks available
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* LabConfig bypass keys */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-800 mb-1">LabConfig — Install-time bypass keys</h3>
|
||||
{!payload.checks?.lab_config?.key_exists ? (
|
||||
<p className="text-xs text-gray-500">HKLM\SYSTEM\Setup\LabConfig not present — normal on standard Windows installs.</p>
|
||||
) : activeBypasses.length === 0 ? (
|
||||
<p className="text-xs text-gray-500">Key exists but no bypass values set to 1 (possible manipulated install medium, currently dormant).</p>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{Object.entries(labValues).map(([name, value]) => {
|
||||
const active = value === 1;
|
||||
const newlyActive = active && newlyAppeared.includes(name);
|
||||
return (
|
||||
<li key={name} className="flex items-center gap-2">
|
||||
<span className={`inline-block w-2 h-2 rounded-full ${active ? 'bg-rose-500' : 'bg-gray-300'}`} />
|
||||
<span className={`font-mono text-xs ${active ? 'text-rose-700' : 'text-gray-500'}`}>{name} = {value}</span>
|
||||
{newlyActive && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-100 text-amber-700 border border-amber-200">appeared during enrollment</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* MoSetup upgrade bypass */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-800 mb-1">MoSetup — In-place upgrade bypass</h3>
|
||||
<p className="text-xs">
|
||||
<span className="font-mono">AllowUpgradesWithUnsupportedTPMOrCPU</span>{" "}
|
||||
{moSetupFlagged ? (
|
||||
<span className="text-rose-700 font-semibold">= 1 (bypass active)</span>
|
||||
) : (
|
||||
<span className="text-gray-500">not set / = 0</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* PCHC UpgradeEligibility */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-800 mb-1">PCHC — Windows Update eligibility bypass</h3>
|
||||
{pchcFlagged ? (
|
||||
<div>
|
||||
<p className="text-xs text-rose-700">
|
||||
<span className="font-mono">HKU\<SID>\SOFTWARE\Microsoft\PCHC\UpgradeEligibility = 1</span> found for {pchcUsers.length} user{pchcUsers.length === 1 ? '' : 's'}:
|
||||
</p>
|
||||
<ul className="ml-3 mt-1 space-y-0.5">
|
||||
{pchcUsers.map((sid) => (
|
||||
<li key={sid} className="font-mono text-xs text-gray-600">{sid}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500">No user hive has UpgradeEligibility = 1.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Setup scripts */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-800 mb-1">Setup Scripts — %WINDIR%\Setup\Scripts\</h3>
|
||||
{setupScripts.length === 0 ? (
|
||||
<p className="text-xs text-gray-500">No SetupComplete.cmd / ErrorHandler.cmd present.</p>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{setupScripts.map((s) => {
|
||||
const isNew = newScripts.some((ns) => ns.path === s.path);
|
||||
return (
|
||||
<li key={s.path ?? ''} className="flex items-center gap-2">
|
||||
<span className="inline-block w-2 h-2 rounded-full bg-amber-500" />
|
||||
<span className="font-mono text-xs text-amber-800">{s.path}</span>
|
||||
<span className="text-xs text-gray-500">({s.size_bytes ?? 0} bytes{s.last_modified_utc ? `, modified ${new Date(s.last_modified_utc).toISOString().replace('T', ' ').substring(0, 19)} UTC` : ""})</span>
|
||||
{isNew && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-100 text-amber-700 border border-amber-200">appeared during enrollment</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Findings list */}
|
||||
{payload.findings && payload.findings.length > 0 && payload.findings[0] !== "no_bypass_indicators" && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-800 mb-1">Findings</h3>
|
||||
<ul className="list-disc list-inside text-xs text-gray-600 ml-2 space-y-0.5">
|
||||
{payload.findings.map((f) => (
|
||||
<li key={f} className="font-mono">{f}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+4
-1
@@ -7,6 +7,7 @@ import { authenticatedFetch } from "@/lib/authenticatedFetch";
|
||||
interface UseSessionTenantConfigReturn {
|
||||
showScriptOutput: boolean;
|
||||
enableSoftwareInventoryAnalyzer: boolean;
|
||||
enableIntegrityBypassAnalyzer: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,6 +21,7 @@ export function useSessionTenantConfig(
|
||||
): UseSessionTenantConfigReturn {
|
||||
const [showScriptOutput, setShowScriptOutput] = useState(true);
|
||||
const [enableSoftwareInventoryAnalyzer, setEnableSoftwareInventoryAnalyzer] = useState(false);
|
||||
const [enableIntegrityBypassAnalyzer, setEnableIntegrityBypassAnalyzer] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionTenantId) return;
|
||||
@@ -32,10 +34,11 @@ export function useSessionTenantConfig(
|
||||
if (cancelled) return;
|
||||
setShowScriptOutput(cfg.showScriptOutput ?? true);
|
||||
setEnableSoftwareInventoryAnalyzer(cfg.enableSoftwareInventoryAnalyzer ?? false);
|
||||
setEnableIntegrityBypassAnalyzer(cfg.enableIntegrityBypassAnalyzer ?? true);
|
||||
} catch { /* non-fatal */ }
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, [sessionTenantId, getAccessToken]);
|
||||
|
||||
return { showScriptOutput, enableSoftwareInventoryAnalyzer };
|
||||
return { showScriptOutput, enableSoftwareInventoryAnalyzer, enableIntegrityBypassAnalyzer };
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import PhaseTimeline from "./components/PhaseTimeline";
|
||||
import EventTimeline from "./components/EventTimeline";
|
||||
import AnalysisResultsSection from "./components/AnalysisResultsSection";
|
||||
import VulnerabilityReportSection from "./components/VulnerabilityReportSection";
|
||||
import IntegrityBypassSection from "./components/IntegrityBypassSection";
|
||||
import AdminOverrideModal from "./components/AdminOverrideModal";
|
||||
import ReportSessionModal from "./components/ReportSessionModal";
|
||||
import { usePageSections } from "../../../hooks/usePageSections";
|
||||
@@ -51,6 +52,7 @@ export default function SessionDetailPage() {
|
||||
const [reportSubmitting, setReportSubmitting] = useState(false);
|
||||
const [analysisExpanded, setAnalysisExpanded] = useState(true);
|
||||
const [vulnerabilityReportExpanded, setVulnerabilityReportExpanded] = useState(false);
|
||||
const [integrityBypassExpanded, setIntegrityBypassExpanded] = useState(false);
|
||||
const [phaseTimelineExpanded, setPhaseTimelineExpanded] = useState(true);
|
||||
const [perfExpanded, setPerfExpanded] = useState(true);
|
||||
const [timelineExpanded, setTimelineExpanded] = useState(true);
|
||||
@@ -103,7 +105,7 @@ export default function SessionDetailPage() {
|
||||
const { session, setSession, sessionTenantId, loading } = detail;
|
||||
const events = eventsApi.events;
|
||||
const { analysisResults, loadingAnalysis, vulnerabilityReport, fetchAnalysisResults, fetchVulnerabilityReport } = analysis;
|
||||
const { showScriptOutput, enableSoftwareInventoryAnalyzer } = tenantConfig;
|
||||
const { showScriptOutput, enableSoftwareInventoryAnalyzer, enableIntegrityBypassAnalyzer } = tenantConfig;
|
||||
const {
|
||||
filteredEvents,
|
||||
appSummaryStats,
|
||||
@@ -512,6 +514,17 @@ export default function SessionDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Integrity Bypass — only when analyzer is enabled for this tenant and the session has produced at least one event */}
|
||||
{!isGatherRulesSession && enableIntegrityBypassAnalyzer && (
|
||||
<div id="section-integrity-bypass">
|
||||
<IntegrityBypassSection
|
||||
events={events}
|
||||
expanded={integrityBypassExpanded}
|
||||
setExpanded={setIntegrityBypassExpanded}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Performance Metrics (from performance_snapshot events) */}
|
||||
{!isGatherRulesSession && (
|
||||
<div id="section-performance">
|
||||
|
||||
@@ -92,6 +92,8 @@ interface TenantConfigContextValue {
|
||||
setNewAllowedAccount: (v: string) => void;
|
||||
enableSoftwareInventoryAnalyzer: boolean;
|
||||
setEnableSoftwareInventoryAnalyzer: (v: boolean) => void;
|
||||
enableIntegrityBypassAnalyzer: boolean;
|
||||
setEnableIntegrityBypassAnalyzer: (v: boolean) => void;
|
||||
handleSaveAgentAnalyzers: () => void;
|
||||
handleResetAgentAnalyzers: () => void;
|
||||
|
||||
@@ -307,6 +309,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
const [localAdminAllowedAccounts, setLocalAdminAllowedAccounts] = useState<string[]>([]);
|
||||
const [newAllowedAccount, setNewAllowedAccount] = useState("");
|
||||
const [enableSoftwareInventoryAnalyzer, setEnableSoftwareInventoryAnalyzer] = useState(false);
|
||||
const [enableIntegrityBypassAnalyzer, setEnableIntegrityBypassAnalyzer] = useState(true);
|
||||
|
||||
// Unrestricted mode
|
||||
const [unrestrictedMode, setUnrestrictedMode] = useState(false);
|
||||
@@ -413,6 +416,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
setLocalAdminAllowedAccounts([]);
|
||||
}
|
||||
setEnableSoftwareInventoryAnalyzer(data.enableSoftwareInventoryAnalyzer ?? false);
|
||||
setEnableIntegrityBypassAnalyzer(data.enableIntegrityBypassAnalyzer ?? true);
|
||||
setUnrestrictedMode(data.unrestrictedMode ?? false);
|
||||
|
||||
// Parse SAS expiry and fire notification to bell if needed
|
||||
@@ -599,6 +603,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
enableLocalAdminAnalyzer,
|
||||
localAdminAllowedAccountsJson: localAdminAllowedAccounts.length > 0 ? JSON.stringify(localAdminAllowedAccounts) : "",
|
||||
enableSoftwareInventoryAnalyzer,
|
||||
enableIntegrityBypassAnalyzer,
|
||||
unrestrictedMode,
|
||||
};
|
||||
|
||||
@@ -646,7 +651,8 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
slaNotifyOnSuccessRateBreach, slaSuccessRateNotifyThreshold, slaNotifyOnDurationBreach,
|
||||
slaNotifyOnAppInstallBreach, slaNotifyOnConsecutiveFailures, slaConsecutiveFailureThreshold,
|
||||
diagnosticsBlobSasUrl, diagnosticsUploadMode, tenantDiagPaths,
|
||||
enableLocalAdminAnalyzer, localAdminAllowedAccounts, enableSoftwareInventoryAnalyzer, unrestrictedMode,
|
||||
enableLocalAdminAnalyzer, localAdminAllowedAccounts, enableSoftwareInventoryAnalyzer,
|
||||
enableIntegrityBypassAnalyzer, unrestrictedMode,
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -837,6 +843,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
} catch { setLocalAdminAllowedAccounts([]); }
|
||||
setNewAllowedAccount("");
|
||||
setEnableSoftwareInventoryAnalyzer(config.enableSoftwareInventoryAnalyzer ?? false);
|
||||
setEnableIntegrityBypassAnalyzer(config.enableIntegrityBypassAnalyzer ?? true);
|
||||
}, [config]);
|
||||
|
||||
const handleSaveNotifications = useCallback(() => saveConfiguration("notifications"), [saveConfiguration]);
|
||||
@@ -1191,6 +1198,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
localAdminAllowedAccounts, setLocalAdminAllowedAccounts,
|
||||
newAllowedAccount, setNewAllowedAccount,
|
||||
enableSoftwareInventoryAnalyzer, setEnableSoftwareInventoryAnalyzer,
|
||||
enableIntegrityBypassAnalyzer, setEnableIntegrityBypassAnalyzer,
|
||||
handleSaveAgentAnalyzers, handleResetAgentAnalyzers,
|
||||
|
||||
// Unrestricted mode
|
||||
|
||||
@@ -10,6 +10,7 @@ export function SectionAgentAnalyzers() {
|
||||
localAdminAllowedAccounts, setLocalAdminAllowedAccounts,
|
||||
newAllowedAccount, setNewAllowedAccount,
|
||||
enableSoftwareInventoryAnalyzer, setEnableSoftwareInventoryAnalyzer,
|
||||
enableIntegrityBypassAnalyzer, setEnableIntegrityBypassAnalyzer,
|
||||
handleSaveAgentAnalyzers, handleResetAgentAnalyzers,
|
||||
savingSection,
|
||||
} = useTenantConfig();
|
||||
@@ -26,6 +27,8 @@ export function SectionAgentAnalyzers() {
|
||||
setNewAllowedAccount={setNewAllowedAccount}
|
||||
enableSoftwareInventoryAnalyzer={enableSoftwareInventoryAnalyzer}
|
||||
setEnableSoftwareInventoryAnalyzer={setEnableSoftwareInventoryAnalyzer}
|
||||
enableIntegrityBypassAnalyzer={enableIntegrityBypassAnalyzer}
|
||||
setEnableIntegrityBypassAnalyzer={setEnableIntegrityBypassAnalyzer}
|
||||
onSave={handleSaveAgentAnalyzers}
|
||||
onReset={handleResetAgentAnalyzers}
|
||||
saving={savingSection === "agentAnalyzers"}
|
||||
|
||||
@@ -11,6 +11,8 @@ interface AgentAnalyzersSectionProps {
|
||||
setNewAllowedAccount: (value: string) => void;
|
||||
enableSoftwareInventoryAnalyzer: boolean;
|
||||
setEnableSoftwareInventoryAnalyzer: (value: boolean) => void;
|
||||
enableIntegrityBypassAnalyzer: boolean;
|
||||
setEnableIntegrityBypassAnalyzer: (value: boolean) => void;
|
||||
onSave: () => Promise<void> | void;
|
||||
onReset: () => void;
|
||||
saving: boolean;
|
||||
@@ -39,6 +41,8 @@ export default function AgentAnalyzersSection({
|
||||
setNewAllowedAccount,
|
||||
enableSoftwareInventoryAnalyzer,
|
||||
setEnableSoftwareInventoryAnalyzer,
|
||||
enableIntegrityBypassAnalyzer,
|
||||
setEnableIntegrityBypassAnalyzer,
|
||||
onSave,
|
||||
onReset,
|
||||
saving,
|
||||
@@ -193,6 +197,34 @@ export default function AgentAnalyzersSection({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Divider ─────────────────────────────────────── */}
|
||||
<div className="border-t border-gray-200" />
|
||||
|
||||
{/* ── Integrity Bypass Analyzer ───────────────────── */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider mb-3">Integrity Bypass Analyzer</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Detects Windows 11 "unsupported hardware" installations where PC Health Check / Setup-time gates were bypassed
|
||||
(LabConfig TPM / SecureBoot / CPU / RAM / Disk bypass keys, MoSetup upgrade flag, PCHC UpgradeEligibility).
|
||||
Also flags suspicious post-OOBE <code className="px-1 bg-gray-100 rounded">SetupComplete.cmd</code> / <code className="px-1 bg-gray-100 rounded">ErrorHandler.cmd</code> scripts.
|
||||
Results are correlated against current TPM and SecureBoot state.
|
||||
</p>
|
||||
|
||||
{/* Enable toggle */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-rose-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Enable Integrity Bypass Analyzer</p>
|
||||
<p className="text-sm text-gray-500">Run at enrollment start and completion to surface devices with bypassed Win11 hardware gates</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEnableIntegrityBypassAnalyzer(!enableIntegrityBypassAnalyzer)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableIntegrityBypassAnalyzer ? 'bg-rose-500' : 'bg-gray-300'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableIntegrityBypassAnalyzer ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save / Reset */}
|
||||
<SaveResetBar onSave={onSave} onReset={onReset} saving={saving} />
|
||||
</div>
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface TenantConfiguration {
|
||||
enableLocalAdminAnalyzer?: boolean;
|
||||
localAdminAllowedAccountsJson?: string;
|
||||
enableSoftwareInventoryAnalyzer?: boolean;
|
||||
enableIntegrityBypassAnalyzer?: boolean;
|
||||
// Bootstrap token
|
||||
bootstrapTokenEnabled?: boolean;
|
||||
// Unrestricted mode
|
||||
|
||||
Reference in New Issue
Block a user