Skip to content

Commit

Permalink
Added compliance check result export and other improvements (#571)
Browse files Browse the repository at this point in the history
You can now export the results of compliance check in the GUI using a new button that was added.

Improved Username detection, making it more resilient.

Further improved the GUI and code behinds to be more consistent.

Improved the comments in the code to be more accurate.
  • Loading branch information
HotCakeX authored Jan 27, 2025
1 parent f42dd4f commit 07dad4d
Show file tree
Hide file tree
Showing 24 changed files with 257 additions and 344 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ internal static void Enable(string DriveLetter, bool FreePlusUsedSpace)


// Make sure the OS Drive is encrypted first, or else we would add recovery password key protector and then get error about the same problem during auto-unlock key protector enablement
BitLockerVolume OSDriveVolumeInfo = GetEncryptedVolumeInfo(Environment.GetEnvironmentVariable("SystemDrive") ?? "C:\\");
BitLockerVolume OSDriveVolumeInfo = GetEncryptedVolumeInfo(GlobalVars.SystemDrive);
if (OSDriveVolumeInfo.ProtectionStatus is not ProtectionStatus.Protected)
{
Logger.LogMessage($"Operation System drive must be encrypted first before encrypting Non-OS drives.", LogTypeIntel.ErrorInteractionRequired);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using static HardenWindowsSecurity.BitLocker;

Expand Down Expand Up @@ -77,7 +76,6 @@ public static void CreateBitLockerVolumeViewModel(bool ExportToFile)
// List of BitLockerVolumeViewModel objects
List<BitLockerVolumeViewModel> viewModelList = [];


foreach (BitLockerVolume Volume in AllBitLockerVolumes)
{
if (Volume.KeyProtector is not null)
Expand Down Expand Up @@ -153,8 +151,7 @@ public static void CreateBitLockerVolumeViewModel(bool ExportToFile)
}

// Notify the user
_ = MessageBox.Show($"BitLocker Recovery Keys have been successfully backed up to {filePath}");

Logger.LogMessage($"BitLocker Recovery Keys have been successfully backed up to {filePath}", LogTypeIntel.InformationInteractionRequired);
}
}

Expand Down
229 changes: 141 additions & 88 deletions Harden-Windows-Security Module/Main files/C#/GUI/Confirm/View.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,15 @@ private void ConfirmView(object obj)
// Parse the XAML content to create a UserControl object
UserControl View = (UserControl)XamlReader.Parse(xamlContent);

// Find the SecOpsDataGrid
// Finding elements
DataGrid SecOpsDataGrid = (DataGrid)View.FindName("SecOpsDataGrid");

TextBlock TotalCurrentlyDisplayedSecOpsTextBlock = (TextBlock)View.FindName("TotalCurrentlyDisplayedSecOps");

#region ToggleButtons
ToggleButton CompliantItemsToggleButton = (ToggleButton)View.FindName("CompliantItemsToggleButton");
ToggleButton NonCompliantItemsToggleButton = (ToggleButton)View.FindName("NonCompliantItemsToggleButton");

CompliantItemsToggleButton.IsChecked = true;
NonCompliantItemsToggleButton.IsChecked = true;
#endregion
Button ExportResultsButton = (Button)View.FindName("ExportResultsButton");
TextBox textBoxFilter = (TextBox)View.FindName("textBoxFilter");
TextBlock TotalCountTextBlock = (TextBlock)View.FindName("TotalCountTextBlock");
ComboBox ComplianceCategoriesSelectionComboBox = (ComboBox)View.FindName("ComplianceCategoriesSelectionComboBox");

// Initialize an empty security options collection
ObservableCollection<SecOp> SecOpsObservableCollection = [];
Expand All @@ -73,7 +70,7 @@ private void ConfirmView(object obj)
void UpdateCurrentVisibleItemsTextBlock()
{
// Get the count of all of the current items in the CollectionView
int totalDisplayedItemsCount = SecOpsCollectionView!.OfType<SecOp>().Count();
int totalDisplayedItemsCount = SecOpsCollectionView.OfType<SecOp>().Count();

// Display the count in a text box in the GUI
TotalCurrentlyDisplayedSecOpsTextBlock.Text = $"Showing {totalDisplayedItemsCount} Items";
Expand All @@ -83,7 +80,7 @@ void UpdateCurrentVisibleItemsTextBlock()
void ApplyFilters(string filterText, bool includeCompliant, bool includeNonCompliant)
{
// Apply a filter to the collection view based on the filter text and toggle buttons
SecOpsCollectionView!.Filter = memberObj =>
SecOpsCollectionView.Filter = memberObj =>
{
if (memberObj is SecOp member)
{
Expand All @@ -109,9 +106,6 @@ void ApplyFilters(string filterText, bool includeCompliant, bool includeNonCompl
UpdateCurrentVisibleItemsTextBlock();
}

// Finding the textboxFilter element
TextBox textBoxFilter = (TextBox)View.FindName("textBoxFilter");

#region event handlers for data filtration
// Attach event handlers to the text box filter and toggle buttons
textBoxFilter.TextChanged += (sender, e) => ApplyFilters(textBoxFilter.Text, CompliantItemsToggleButton.IsChecked ?? false, NonCompliantItemsToggleButton.IsChecked ?? false);
Expand Down Expand Up @@ -149,101 +143,83 @@ void ApplyFilters(string filterText, bool includeCompliant, bool includeNonCompl

#endregion


#region ComboBox
// Finding the ComplianceCategoriesSelectionComboBox ComboBox
ComboBox ComplianceCategoriesSelectionComboBox = (ComboBox)View.FindName("ComplianceCategoriesSelectionComboBox");

// Get the valid compliance category names
List<string> catsList = [.. Enum.GetNames<ComplianceCategories>()];

// Add an empty item to the list at the beginning
catsList.Insert(0, "");
// Add an item to the list at the beginning to be the default value
catsList.Insert(0, "All Categories");

// Set the ComboBox's ItemsSource to the updated list
ComplianceCategoriesSelectionComboBox.ItemsSource = catsList;

#endregion

// Register the RefreshButton as an element that will be enabled/disabled based on current activity
// Register the elements that will be enabled/disabled based on current global activity
ActivityTracker.RegisterUIElement(RefreshButton);
ActivityTracker.RegisterUIElement(ComplianceCategoriesSelectionComboBox);

// Set up the Click event handler for the Refresh button
RefreshButton.Click += async (sender, e) =>
{

// Only continue if there is no activity other places
if (!ActivityTracker.IsActive)
if (ActivityTracker.IsActive)
{
// mark as activity started
ActivityTracker.IsActive = true;

// Clear the current security options before starting data generation
SecOpsObservableCollection.Clear();
SecOpsCollectionView.Refresh(); // Refresh the collection view to clear the DataGrid
return;
}

// Disable the Refresh button while processing
// Set text blocks to empty while new data is being generated
Application.Current.Dispatcher.Invoke(() =>
{
TextBlock TotalCountTextBlock = (TextBlock)View.FindName("TotalCountTextBlock");
// mark as activity started
ActivityTracker.IsActive = true;

if (TotalCountTextBlock is not null)
{
// Update the text of the TextBlock to show the total count
TotalCountTextBlock.Text = "Loading...";
}
// Clear the current security options before starting data generation
SecOpsObservableCollection.Clear();
SecOpsCollectionView.Refresh(); // Refresh the collection view to clear the DataGrid

UpdateCurrentVisibleItemsTextBlock();
});
// Set text blocks to empty while new data is being generated
Application.Current.Dispatcher.Invoke(() =>
{
TotalCountTextBlock.Text = "Loading...";
CompliantItemsToggleButton.Content = "Compliant Items";
NonCompliantItemsToggleButton.Content = "Non-Compliant Items";

// Run the method asynchronously in a different thread
await Task.Run(() =>
{
// Get fresh data for compliance checking
Initializer.Initialize(null, true);
UpdateCurrentVisibleItemsTextBlock();
});

// initialize the variable to null
string? SelectedCategory = null;
// Run the method asynchronously in a different thread
await Task.Run(() =>
{
// Get fresh data for compliance checking
Initializer.Initialize(null, true);

// Use the App dispatcher since this is being done in a different thread
app.Dispatcher.Invoke(() =>
{
if (ComplianceCategoriesSelectionComboBox.SelectedItem is not null)
{
// Get the currently selected value in the Compliance Checking category ComboBox if it exists
var SelectedComplianceCategories = ComplianceCategoriesSelectionComboBox.SelectedItem;

// Get the currently selected compliance category
SelectedCategory = SelectedComplianceCategories?.ToString();
}
});

// if user selected a category for compliance checking
if (!string.IsNullOrEmpty(SelectedCategory))
{
// Perform the compliance check using the selected compliance category
InvokeConfirmation.Invoke([SelectedCategory]);
}
else
{
// Perform the compliance check for all categories
InvokeConfirmation.Invoke(null);
}
});
// Get the currently selected compliance category - Use the App dispatcher since this is being done in a different thread
string SelectedCategory = app.Dispatcher.Invoke(() => ComplianceCategoriesSelectionComboBox.SelectedItem.ToString()!);

// After InvokeConfirmation is completed, update the security options collection
await Application.Current.Dispatcher.InvokeAsync(() =>
// If all categories is selected which is the default
if (string.Equals(SelectedCategory, "All Categories", StringComparison.OrdinalIgnoreCase))
{
// Perform the compliance check for all categories
InvokeConfirmation.Invoke(null);
}
// if user selected a category for compliance checking
else
{
LoadMembers(); // Load updated security options
RefreshButton.IsChecked = false; // Uncheck the Refresh button
// Perform the compliance check using the selected compliance category
InvokeConfirmation.Invoke([SelectedCategory]);
}
});

UpdateCurrentVisibleItemsTextBlock();
});
// After InvokeConfirmation is completed, update the security options collection
await Application.Current.Dispatcher.InvokeAsync(() =>
{
LoadMembers(); // Load updated security options
RefreshButton.IsChecked = false; // Uncheck the Refresh button

// mark as activity completed
ActivityTracker.IsActive = false;
}
UpdateCurrentVisibleItemsTextBlock();
});

// mark as activity completed
ActivityTracker.IsActive = false;
};

/// <summary>
Expand Down Expand Up @@ -289,17 +265,11 @@ void LoadMembers()
/// <param name="ShowNotification">If set to true, this method will display end of confirmation toast notification</param>
void UpdateTotalCount(bool ShowNotification)
{

// calculates the total number of all security options across all lists, so all the items in each category that exist in the values of the main dictionary object
int totalCount = GlobalVars.FinalMegaObject.Values.Sum(list => list.Count);

// Find the TextBlock used to display the total count
TextBlock TotalCountTextBlock = (TextBlock)View.FindName("TotalCountTextBlock");
if (TotalCountTextBlock is not null)
{
// Update the text of the TextBlock to show the total count
TotalCountTextBlock.Text = $"{totalCount} Total Verifiable Security Checks";
}
// Update the text of the TextBlock to show the total count
TotalCountTextBlock.Text = $"{totalCount} Total Verifiable Security Checks";

// Get the count of the compliant items
string CompliantItemsCount = SecOpsCollectionView.SourceCollection
Expand All @@ -324,6 +294,89 @@ void UpdateTotalCount(bool ShowNotification)
}
}

// Event handler for the Export Results button
ExportResultsButton.Click += async (sender, e) =>
{
try
{
ExportResultsButton.IsEnabled = false;

await Task.Run(() =>
{
// Show the save file dialog to let the user pick the save location
Microsoft.Win32.SaveFileDialog saveFileDialog = new()
{
FileName = $"Harden Windows Security Compliance Check Results at {DateTime.Now:yyyy-MM-dd HH-mm-ss}", // Default file name
DefaultExt = ".csv", // Default file extension
Filter = "CSV File (.csv)|*.csv" // Filter files by extension
};

// Show the dialog and check if the user picked a file
bool? result = saveFileDialog.ShowDialog();

if (result == true)
{
// Get the selected file path from the dialog
string filePath = saveFileDialog.FileName;

try
{
ExportSecOpsToCsv(filePath, SecOpsObservableCollection);
}
catch (Exception ex)
{
Logger.LogMessage($"Failed to export the results to the file: {ex.Message}", LogTypeIntel.ErrorInteractionRequired);
}

Logger.LogMessage($"Compliance check results have been successfully exported.", LogTypeIntel.InformationInteractionRequired);
}
});
}
finally
{
ExportResultsButton.IsEnabled = true;
}
};


// To Export the results of the compliance checking to a file
void ExportSecOpsToCsv(string filePath, ObservableCollection<SecOp> secOps)
{
// Defining the header row
string[] headers = ["FriendlyName", "Compliant", "Value", "Name", "Category", "Method"];

// Open the file for writing
using StreamWriter writer = new(filePath);

// Write the header row
writer.WriteLine(string.Join(",", headers.Select(header => $"\"{header}\"")));

// Write each SecOp object as a row in the CSV
foreach (SecOp secOp in secOps)
{
string[] row = [
EscapeForCsv(secOp.FriendlyName),
EscapeForCsv(secOp.Compliant.ToString(CultureInfo.InvariantCulture)),
EscapeForCsv(secOp.Value),
EscapeForCsv(secOp.Name),
EscapeForCsv(secOp.Category.ToString()),
EscapeForCsv(secOp.Method)
];

writer.WriteLine(string.Join(",", row));
}
}

// Local method to enclose values in double quotes
string EscapeForCsv(string? value)
{
// If the value is null, empty or whitespace, return an empty string
if (string.IsNullOrWhiteSpace(value)) return string.Empty;

// Otherwise, escape double quotes and wrap the value in double quotes
return $"\"{value.Replace("\"", "\"\"", StringComparison.OrdinalIgnoreCase)}\"";
}

// Cache the Confirm view for future use
_viewCache["ConfirmView"] = View;

Expand Down
3 changes: 1 addition & 2 deletions Harden-Windows-Security Module/Main files/C#/GUI/Log/View.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Markup;
Expand Down Expand Up @@ -74,7 +73,7 @@ private void LogsView(object obj)
// Write the text content from the TextBox to the file
File.WriteAllText(filePath, GUILogs.MainLoggerTextBox.Text);

_ = MessageBox.Show("Logs successfully saved.", "Success", MessageBoxButton.OK, MessageBoxImage.Information);
Logger.LogMessage("Logs successfully saved.", LogTypeIntel.InformationInteractionRequired);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -609,9 +609,7 @@ void AddEventHandlers()

#region Display a Welcome message

string nameToDisplay = (!string.IsNullOrWhiteSpace(GlobalVars.userFullName)) ? GlobalVars.userFullName : GlobalVars.userName;

Logger.LogMessage(Environment.IsPrivilegedProcess ? $"Hello {nameToDisplay}, you have Administrator privileges" : $"Hello {nameToDisplay}, you don't have Administrator privileges, some categories are disabled", LogTypeIntel.Information);
Logger.LogMessage(Environment.IsPrivilegedProcess ? $"Hello {Environment.UserName}, you have Administrator privileges" : $"Hello {Environment.UserName}, you don't have Administrator privileges, some categories are disabled", LogTypeIntel.Information);
#endregion

// Use Dispatcher.Invoke to update the UI thread
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ private static Task VerifyBitLockerSettings()
// OS Drive encryption verifications
// Check if BitLocker is on for the OS Drive
// The ProtectionStatus remains off while the drive is encrypting or decrypting
BitLocker.BitLockerVolume volumeInfo = BitLocker.GetEncryptedVolumeInfo(Environment.GetEnvironmentVariable("SystemDrive") ?? "C:\\");
BitLocker.BitLockerVolume volumeInfo = BitLocker.GetEncryptedVolumeInfo(GlobalVars.SystemDrive);

if (volumeInfo.ProtectionStatus is BitLocker.ProtectionStatus.Protected)
{
Expand Down
Loading

0 comments on commit 07dad4d

Please sign in to comment.