diff --git a/AppControl Manager/Logic/Main/UserConfiguration.cs b/AppControl Manager/Logic/Main/UserConfiguration.cs index a07c41d8b..a00e289e9 100644 --- a/AppControl Manager/Logic/Main/UserConfiguration.cs +++ b/AppControl Manager/Logic/Main/UserConfiguration.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using AppControlManager.Logging; @@ -33,7 +34,8 @@ public sealed partial class UserConfiguration( Guid? strictKernelNoFlightRootsPolicyGUID, DateTime? lastUpdateCheck, DateTime? strictKernelModePolicyTimeOfDeployment, - bool? autoUpdateCheck + bool? autoUpdateCheck, + Dictionary? signedPolicyStage1RemovalTimes = null ) { [JsonPropertyOrder(1)] @@ -66,6 +68,9 @@ public sealed partial class UserConfiguration( [JsonPropertyOrder(10)] public bool? AutoUpdateCheck { get; set; } = autoUpdateCheck; + [JsonPropertyOrder(11)] + public Dictionary? SignedPolicyStage1RemovalTimes { get; set; } = signedPolicyStage1RemovalTimes; + /// @@ -82,6 +87,7 @@ public sealed partial class UserConfiguration( /// /// /// + /// /// /// internal static UserConfiguration Set( @@ -94,7 +100,8 @@ internal static UserConfiguration Set( Guid? StrictKernelNoFlightRootsPolicyGUID = null, DateTime? LastUpdateCheck = null, DateTime? StrictKernelModePolicyTimeOfDeployment = null, - bool? AutoUpdateCheck = null + bool? AutoUpdateCheck = null, + Dictionary? SignedPolicyStage1RemovalTimes = null ) { // Validate certificateCommonName @@ -154,6 +161,11 @@ internal static UserConfiguration Set( if (StrictKernelModePolicyTimeOfDeployment.HasValue) UserConfiguration.StrictKernelModePolicyTimeOfDeployment = StrictKernelModePolicyTimeOfDeployment; if (AutoUpdateCheck.HasValue) UserConfiguration.AutoUpdateCheck = AutoUpdateCheck; + if (SignedPolicyStage1RemovalTimes is not null) + { + UserConfiguration.SignedPolicyStage1RemovalTimes = SignedPolicyStage1RemovalTimes; + } + // Write the updated properties back to the JSON file WriteUserConfiguration(UserConfiguration); @@ -186,6 +198,7 @@ internal static UserConfiguration Get() /// /// /// + /// internal static void Remove( bool SignedPolicyPath = false, bool UnsignedPolicyPath = false, @@ -196,7 +209,8 @@ internal static void Remove( bool StrictKernelNoFlightRootsPolicyGUID = false, bool LastUpdateCheck = false, bool StrictKernelModePolicyTimeOfDeployment = false, - bool AutoUpdateCheck = false + bool AutoUpdateCheck = false, + bool SignedPolicyStage1RemovalTimes = false ) { // Read the current configuration @@ -213,6 +227,7 @@ internal static void Remove( if (LastUpdateCheck) currentConfig.LastUpdateCheck = null; if (StrictKernelModePolicyTimeOfDeployment) currentConfig.StrictKernelModePolicyTimeOfDeployment = null; if (AutoUpdateCheck) currentConfig.AutoUpdateCheck = null; + if (SignedPolicyStage1RemovalTimes) currentConfig.SignedPolicyStage1RemovalTimes = null; // Write the updated configuration back to the JSON file WriteUserConfiguration(currentConfig); @@ -220,6 +235,7 @@ internal static void Remove( Logger.Write("The specified properties have been removed and set to null in the UserConfigurations.json file."); } + private static UserConfiguration ReadUserConfiguration() { try @@ -276,7 +292,8 @@ private static UserConfiguration ParseJson(string json) TryGetGuidProperty(root, nameof(StrictKernelNoFlightRootsPolicyGUID)), TryGetDateTimeProperty(root, nameof(LastUpdateCheck)), TryGetDateTimeProperty(root, nameof(StrictKernelModePolicyTimeOfDeployment)), - TryGetBoolProperty(root, nameof(AutoUpdateCheck)) + TryGetBoolProperty(root, nameof(AutoUpdateCheck)), + TryGetKeyValuePairsProperty(root, nameof(SignedPolicyStage1RemovalTimes)) ); static string? TryGetStringProperty(JsonElement root, string propertyName) @@ -326,6 +343,21 @@ private static UserConfiguration ParseJson(string json) return null; } } + + static Dictionary? TryGetKeyValuePairsProperty(JsonElement root, string propertyName) + { + try + { + return root.TryGetProperty(propertyName, out var propertyValue) && propertyValue.ValueKind == JsonValueKind.Object + ? propertyValue.EnumerateObject().ToDictionary(e => e.Name, e => e.Value.GetDateTime().ToUniversalTime()) + : null; + } + catch + { + return null; + } + } + } @@ -341,4 +373,81 @@ private static void WriteUserConfiguration(UserConfiguration userConfiguration) File.WriteAllText(GlobalVars.UserConfigJson, jsonString); Logger.Write("The UserConfigurations.json file has been updated successfully."); } + + + + + /// + /// Adds a new key-value pair to the SignedPolicyStage1RemovalTimes dictionary. + /// + /// The key to add. + /// The value to associate with the key. + internal static void Add(string key, DateTime value) + { + // Get the current user configuration + UserConfiguration currentConfig = ReadUserConfiguration(); + + // Initialize the dictionary if it doesn't exist + currentConfig.SignedPolicyStage1RemovalTimes ??= []; + + // Add the key-value pair to the dictionary + currentConfig.SignedPolicyStage1RemovalTimes[key] = value; // This will add or update the value for the key + + // Write the updated configuration back to the JSON file + WriteUserConfiguration(currentConfig); + + Logger.Write($"Key-value pair added to the SignedPolicyStage1RemovalTimes: {key} = {value}"); + } + + + + /// + /// Queries the SignedPolicyStage1RemovalTimes dictionary by key and returns the corresponding value. + /// + /// The key to query. + /// The value associated with the key, or null if the key does not exist. + internal static DateTime? Query(string key) + { + // Get the current user configuration + UserConfiguration currentConfig = ReadUserConfiguration(); + + // Return the value if the key exists, otherwise return null + if (currentConfig.SignedPolicyStage1RemovalTimes is not null && currentConfig.SignedPolicyStage1RemovalTimes.TryGetValue(key, out DateTime value)) + { + return value; + } + + // Return null if the key doesn't exist + return null; + } + + + + /// + /// Removes a key-value pair from the SignedPolicyStage1RemovalTimes dictionary by key. + /// + /// The key to remove. + /// True if the key was successfully removed; false if the key was not found. + internal static void RemoveKey(string key) + { + // Get the current user configuration + UserConfiguration currentConfig = ReadUserConfiguration(); + + // Check if the dictionary exists and contains the key + if (currentConfig.SignedPolicyStage1RemovalTimes is not null && currentConfig.SignedPolicyStage1RemovalTimes.ContainsKey(key)) + { + // Remove the key-value pair + _ = currentConfig.SignedPolicyStage1RemovalTimes.Remove(key); + + // Write the updated configuration back to the JSON file + WriteUserConfiguration(currentConfig); + + Logger.Write($"Key '{key}' removed from the SignedPolicyStage1RemovalTimes dictionary."); + } + else + { + Logger.Write($"Key '{key}' not found in the SignedPolicyStage1RemovalTimes dictionary."); + } + } + } diff --git a/AppControl Manager/Pages/SystemInformation/ViewCurrentPolicies.xaml.cs b/AppControl Manager/Pages/SystemInformation/ViewCurrentPolicies.xaml.cs index 05d22bb2a..93ade2cb3 100644 --- a/AppControl Manager/Pages/SystemInformation/ViewCurrentPolicies.xaml.cs +++ b/AppControl Manager/Pages/SystemInformation/ViewCurrentPolicies.xaml.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using AppControlManager.CustomUIElements; +using AppControlManager.Logging; using AppControlManager.SiPolicyIntel; using CommunityToolkit.WinUI.UI.Controls; using Microsoft.UI; @@ -322,14 +323,8 @@ await Task.Run(() => foreach (CiPolicyInfo policy in policiesToRemove) { - // Remove the policy directly from the system if it's unsigned - if (!policy.IsSignedPolicy || - // or supplemental - !string.Equals(policy.PolicyID, policy.BasePolicyID, StringComparison.OrdinalIgnoreCase) || - // or signed base policy with the EnabledUnsignedSystemIntegrityPolicy policy rule option - (policy.IsSignedPolicy && string.Equals(policy.PolicyID, policy.BasePolicyID, StringComparison.OrdinalIgnoreCase) && - policy.PolicyOptions is not null && policy.PolicyOptionsDisplay.Contains("Enabled:Unsigned System Integrity Policy", StringComparison.OrdinalIgnoreCase) - )) + // Remove the policy directly from the system if it's unsigned or supplemental + if (!policy.IsSignedPolicy || !string.Equals(policy.PolicyID, policy.BasePolicyID, StringComparison.OrdinalIgnoreCase)) { await Task.Run(() => { @@ -337,78 +332,120 @@ await Task.Run(() => }); } - // If the policy is base and signed + // At this point the policy is definitely a Signed Base policy else { - #region Signing Details acquisition + // If the EnabledUnsignedSystemIntegrityPolicy policy rule option exists + // Which means 1st stage already happened + if (policy.PolicyOptions is not null && policy.PolicyOptionsDisplay.Contains("Enabled:Unsigned System Integrity Policy", StringComparison.OrdinalIgnoreCase)) + { + // And if system was rebooted once after performing the 1st removal stage + if (VerifyRemovalEligibility(policy.PolicyID!)) + { + CiToolHelper.RemovePolicy(policy.PolicyID!); + + // Remove the PolicyID from the SignedPolicyStage1RemovalTimes dictionary + UserConfiguration.RemoveKey(policy.PolicyID!); + } + else + { + // Create and display a ContentDialog + ContentDialog dialog = new() + { + Title = "WARNING", + Content = $"Before you can safely remove the signed policy named '{policy.FriendlyName}' with the ID '{policy.PolicyID}', you must restart your system.", + PrimaryButtonText = "I Understand", + BorderBrush = Application.Current.Resources["AccentFillColorDefaultBrush"] as Brush ?? new SolidColorBrush(Colors.Transparent), + BorderThickness = new Thickness(1), + XamlRoot = this.XamlRoot // Set XamlRoot to the current page's XamlRoot + }; + + // Show the dialog and wait for user response + _ = await dialog.ShowAsync(); + + // Exit the method, nothing more can be done about the selected policy + return; + } + } + else + { - string CertCN; - string CertPath; - string SignToolPath; - string XMLPolicyPath; - // Instantiate the Content Dialog - SigningDetailsDialogForRemoval customDialog = new(currentlyDeployedBasePolicyIDs, policy.PolicyID!); + #region Signing Details acquisition - // Show the dialog and await its result - ContentDialogResult result = await customDialog.ShowAsync(); + string CertCN; + string CertPath; + string SignToolPath; + string XMLPolicyPath; - // Ensure primary button was selected - if (result is ContentDialogResult.Primary) - { - SignToolPath = customDialog.SignToolPath!; - CertPath = customDialog.CertificatePath!; - CertCN = customDialog.CertificateCommonName!; - XMLPolicyPath = customDialog.XMLPolicyPath!; + // Instantiate the Content Dialog + SigningDetailsDialogForRemoval customDialog = new(currentlyDeployedBasePolicyIDs, policy.PolicyID!); - // Sometimes the content dialog lingers on or re-appears so making sure it hides - customDialog.Hide(); + // Show the dialog and await its result + ContentDialogResult result = await customDialog.ShowAsync(); - } - else - { - return; - } + // Ensure primary button was selected + if (result is ContentDialogResult.Primary) + { + SignToolPath = customDialog.SignToolPath!; + CertPath = customDialog.CertificatePath!; + CertCN = customDialog.CertificateCommonName!; + XMLPolicyPath = customDialog.XMLPolicyPath!; - #endregion + // Sometimes the content dialog lingers on or re-appears so making sure it hides + customDialog.Hide(); - // Add the unsigned policy rule option to the policy - CiRuleOptions.Set(filePath: XMLPolicyPath, rulesToAdd: [CiRuleOptions.PolicyRuleOptions.EnabledUnsignedSystemIntegrityPolicy]); + } + else + { + return; + } - // Making sure SupplementalPolicySigners do not exist in the XML policy - CiPolicyHandler.RemoveSupplementalSigners(XMLPolicyPath); + #endregion - // Define the path for the CIP file - string randomString = GUIDGenerator.GenerateUniqueGUID(); - string xmlFileName = Path.GetFileName(XMLPolicyPath); - string CIPFilePath = Path.Combine(stagingArea.FullName, $"{xmlFileName}-{randomString}.cip"); + // Add the unsigned policy rule option to the policy + CiRuleOptions.Set(filePath: XMLPolicyPath, rulesToAdd: [CiRuleOptions.PolicyRuleOptions.EnabledUnsignedSystemIntegrityPolicy]); - string CIPp7SignedFilePath = Path.Combine(stagingArea.FullName, $"{xmlFileName}-{randomString}.cip.p7"); + // Making sure SupplementalPolicySigners do not exist in the XML policy + CiPolicyHandler.RemoveSupplementalSigners(XMLPolicyPath); - // Convert the XML file to CIP, overwriting the unsigned one - PolicyToCIPConverter.Convert(XMLPolicyPath, CIPFilePath); + // Define the path for the CIP file + string randomString = GUIDGenerator.GenerateUniqueGUID(); + string xmlFileName = Path.GetFileName(XMLPolicyPath); + string CIPFilePath = Path.Combine(stagingArea.FullName, $"{xmlFileName}-{randomString}.cip"); - // Sign the CIP - SignToolHelper.Sign(new FileInfo(CIPFilePath), new FileInfo(SignToolPath), CertCN); + string CIPp7SignedFilePath = Path.Combine(stagingArea.FullName, $"{xmlFileName}-{randomString}.cip.p7"); - // Rename the .p7 signed file to .cip - File.Move(CIPp7SignedFilePath, CIPFilePath, true); + // Convert the XML file to CIP, overwriting the unsigned one + PolicyToCIPConverter.Convert(XMLPolicyPath, CIPFilePath); - // Deploy the signed CIP file - CiToolHelper.UpdatePolicy(CIPFilePath); + // Sign the CIP + SignToolHelper.Sign(new FileInfo(CIPFilePath), new FileInfo(SignToolPath), CertCN); - } - } + // Rename the .p7 signed file to .cip + File.Move(CIPp7SignedFilePath, CIPFilePath, true); + // Deploy the signed CIP file + CiToolHelper.UpdatePolicy(CIPFilePath); - // Refresh the DataGrid's policies and their count - RetrievePolicies(); + SiPolicy.SiPolicy policyObj = SiPolicy.Management.Initialize(XMLPolicyPath); + + // The time of first stage of the signed policy removal + // Since policy object has the full ID, in upper case with curly brackets, + // We need to normalize them to match what the CiPolicyInfo class uses + UserConfiguration.Add(policyObj.PolicyID.Trim('{', '}').ToLowerInvariant(), DateTime.UtcNow); + } + } + } } } } finally { + // Refresh the DataGrid's policies and their count + RetrievePolicies(); + DeployedPolicies.IsHitTestVisible = true; RetrievePoliciesButton.IsEnabled = true; SearchBox.IsEnabled = true; @@ -659,4 +696,36 @@ private void MenuFlyout_Closing(FlyoutBase sender, FlyoutBaseClosingEventArgs ar #pragma warning restore CA1822 + + /// + /// If returns true, the signed policy can be removed + /// + /// + /// + private static bool VerifyRemovalEligibility(string policyID) + { + // When system was last reboot + DateTime lastRebootTimeUtc = DateTime.UtcNow - TimeSpan.FromMilliseconds(Environment.TickCount64); + + Logger.Write($"System's last reboot was {lastRebootTimeUtc} (UTC)"); + + // When the policy's 1st stage was completed + DateTime? stage1RemovalTime = UserConfiguration.Query(policyID); + + if (stage1RemovalTime is not null) + { + Logger.Write($"Signed policy with the ID '{policyID}' completed its 1st state at {stage1RemovalTime} (UTC)"); + + if (stage1RemovalTime < lastRebootTimeUtc) + { + Logger.Write("Signed policy is safe to be removed because system was restarted after 1st stage"); + + return true; + } + } + + return false; + } + + }