Skip to content

Commit 07dad4d

Browse files
authored
Added compliance check result export and other improvements (#571)
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.
1 parent f42dd4f commit 07dad4d

24 files changed

+257
-344
lines changed

Harden-Windows-Security Module/Main files/C#/CimInstances/BitLocker-Enable.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ internal static void Enable(string DriveLetter, bool FreePlusUsedSpace)
345345

346346

347347
// 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
348-
BitLockerVolume OSDriveVolumeInfo = GetEncryptedVolumeInfo(Environment.GetEnvironmentVariable("SystemDrive") ?? "C:\\");
348+
BitLockerVolume OSDriveVolumeInfo = GetEncryptedVolumeInfo(GlobalVars.SystemDrive);
349349
if (OSDriveVolumeInfo.ProtectionStatus is not ProtectionStatus.Protected)
350350
{
351351
Logger.LogMessage($"Operation System drive must be encrypted first before encrypting Non-OS drives.", LogTypeIntel.ErrorInteractionRequired);

Harden-Windows-Security Module/Main files/C#/GUI/BitLocker/Variables.cs

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System.Collections.Generic;
22
using System.IO;
33
using System.Linq;
4-
using System.Windows;
54
using System.Windows.Controls;
65
using static HardenWindowsSecurity.BitLocker;
76

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

80-
8179
foreach (BitLockerVolume Volume in AllBitLockerVolumes)
8280
{
8381
if (Volume.KeyProtector is not null)
@@ -153,8 +151,7 @@ public static void CreateBitLockerVolumeViewModel(bool ExportToFile)
153151
}
154152

155153
// Notify the user
156-
_ = MessageBox.Show($"BitLocker Recovery Keys have been successfully backed up to {filePath}");
157-
154+
Logger.LogMessage($"BitLocker Recovery Keys have been successfully backed up to {filePath}", LogTypeIntel.InformationInteractionRequired);
158155
}
159156
}
160157

Harden-Windows-Security Module/Main files/C#/GUI/Confirm/View.cs

+141-88
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,15 @@ private void ConfirmView(object obj)
4646
// Parse the XAML content to create a UserControl object
4747
UserControl View = (UserControl)XamlReader.Parse(xamlContent);
4848

49-
// Find the SecOpsDataGrid
49+
// Finding elements
5050
DataGrid SecOpsDataGrid = (DataGrid)View.FindName("SecOpsDataGrid");
51-
5251
TextBlock TotalCurrentlyDisplayedSecOpsTextBlock = (TextBlock)View.FindName("TotalCurrentlyDisplayedSecOps");
53-
54-
#region ToggleButtons
5552
ToggleButton CompliantItemsToggleButton = (ToggleButton)View.FindName("CompliantItemsToggleButton");
5653
ToggleButton NonCompliantItemsToggleButton = (ToggleButton)View.FindName("NonCompliantItemsToggleButton");
57-
58-
CompliantItemsToggleButton.IsChecked = true;
59-
NonCompliantItemsToggleButton.IsChecked = true;
60-
#endregion
54+
Button ExportResultsButton = (Button)View.FindName("ExportResultsButton");
55+
TextBox textBoxFilter = (TextBox)View.FindName("textBoxFilter");
56+
TextBlock TotalCountTextBlock = (TextBlock)View.FindName("TotalCountTextBlock");
57+
ComboBox ComplianceCategoriesSelectionComboBox = (ComboBox)View.FindName("ComplianceCategoriesSelectionComboBox");
6158

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

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

112-
// Finding the textboxFilter element
113-
TextBox textBoxFilter = (TextBox)View.FindName("textBoxFilter");
114-
115109
#region event handlers for data filtration
116110
// Attach event handlers to the text box filter and toggle buttons
117111
textBoxFilter.TextChanged += (sender, e) => ApplyFilters(textBoxFilter.Text, CompliantItemsToggleButton.IsChecked ?? false, NonCompliantItemsToggleButton.IsChecked ?? false);
@@ -149,101 +143,83 @@ void ApplyFilters(string filterText, bool includeCompliant, bool includeNonCompl
149143

150144
#endregion
151145

152-
153146
#region ComboBox
154-
// Finding the ComplianceCategoriesSelectionComboBox ComboBox
155-
ComboBox ComplianceCategoriesSelectionComboBox = (ComboBox)View.FindName("ComplianceCategoriesSelectionComboBox");
156147

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

160-
// Add an empty item to the list at the beginning
161-
catsList.Insert(0, "");
151+
// Add an item to the list at the beginning to be the default value
152+
catsList.Insert(0, "All Categories");
162153

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

166157
#endregion
167158

168-
// Register the RefreshButton as an element that will be enabled/disabled based on current activity
159+
// Register the elements that will be enabled/disabled based on current global activity
169160
ActivityTracker.RegisterUIElement(RefreshButton);
161+
ActivityTracker.RegisterUIElement(ComplianceCategoriesSelectionComboBox);
170162

171163
// Set up the Click event handler for the Refresh button
172164
RefreshButton.Click += async (sender, e) =>
173165
{
174-
175166
// Only continue if there is no activity other places
176-
if (!ActivityTracker.IsActive)
167+
if (ActivityTracker.IsActive)
177168
{
178-
// mark as activity started
179-
ActivityTracker.IsActive = true;
180-
181-
// Clear the current security options before starting data generation
182-
SecOpsObservableCollection.Clear();
183-
SecOpsCollectionView.Refresh(); // Refresh the collection view to clear the DataGrid
169+
return;
170+
}
184171

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

191-
if (TotalCountTextBlock is not null)
192-
{
193-
// Update the text of the TextBlock to show the total count
194-
TotalCountTextBlock.Text = "Loading...";
195-
}
175+
// Clear the current security options before starting data generation
176+
SecOpsObservableCollection.Clear();
177+
SecOpsCollectionView.Refresh(); // Refresh the collection view to clear the DataGrid
196178

197-
UpdateCurrentVisibleItemsTextBlock();
198-
});
179+
// Set text blocks to empty while new data is being generated
180+
Application.Current.Dispatcher.Invoke(() =>
181+
{
182+
TotalCountTextBlock.Text = "Loading...";
183+
CompliantItemsToggleButton.Content = "Compliant Items";
184+
NonCompliantItemsToggleButton.Content = "Non-Compliant Items";
199185

200-
// Run the method asynchronously in a different thread
201-
await Task.Run(() =>
202-
{
203-
// Get fresh data for compliance checking
204-
Initializer.Initialize(null, true);
186+
UpdateCurrentVisibleItemsTextBlock();
187+
});
205188

206-
// initialize the variable to null
207-
string? SelectedCategory = null;
189+
// Run the method asynchronously in a different thread
190+
await Task.Run(() =>
191+
{
192+
// Get fresh data for compliance checking
193+
Initializer.Initialize(null, true);
208194

209-
// Use the App dispatcher since this is being done in a different thread
210-
app.Dispatcher.Invoke(() =>
211-
{
212-
if (ComplianceCategoriesSelectionComboBox.SelectedItem is not null)
213-
{
214-
// Get the currently selected value in the Compliance Checking category ComboBox if it exists
215-
var SelectedComplianceCategories = ComplianceCategoriesSelectionComboBox.SelectedItem;
216-
217-
// Get the currently selected compliance category
218-
SelectedCategory = SelectedComplianceCategories?.ToString();
219-
}
220-
});
221-
222-
// if user selected a category for compliance checking
223-
if (!string.IsNullOrEmpty(SelectedCategory))
224-
{
225-
// Perform the compliance check using the selected compliance category
226-
InvokeConfirmation.Invoke([SelectedCategory]);
227-
}
228-
else
229-
{
230-
// Perform the compliance check for all categories
231-
InvokeConfirmation.Invoke(null);
232-
}
233-
});
195+
// Get the currently selected compliance category - Use the App dispatcher since this is being done in a different thread
196+
string SelectedCategory = app.Dispatcher.Invoke(() => ComplianceCategoriesSelectionComboBox.SelectedItem.ToString()!);
234197

235-
// After InvokeConfirmation is completed, update the security options collection
236-
await Application.Current.Dispatcher.InvokeAsync(() =>
198+
// If all categories is selected which is the default
199+
if (string.Equals(SelectedCategory, "All Categories", StringComparison.OrdinalIgnoreCase))
200+
{
201+
// Perform the compliance check for all categories
202+
InvokeConfirmation.Invoke(null);
203+
}
204+
// if user selected a category for compliance checking
205+
else
237206
{
238-
LoadMembers(); // Load updated security options
239-
RefreshButton.IsChecked = false; // Uncheck the Refresh button
207+
// Perform the compliance check using the selected compliance category
208+
InvokeConfirmation.Invoke([SelectedCategory]);
209+
}
210+
});
240211

241-
UpdateCurrentVisibleItemsTextBlock();
242-
});
212+
// After InvokeConfirmation is completed, update the security options collection
213+
await Application.Current.Dispatcher.InvokeAsync(() =>
214+
{
215+
LoadMembers(); // Load updated security options
216+
RefreshButton.IsChecked = false; // Uncheck the Refresh button
243217

244-
// mark as activity completed
245-
ActivityTracker.IsActive = false;
246-
}
218+
UpdateCurrentVisibleItemsTextBlock();
219+
});
220+
221+
// mark as activity completed
222+
ActivityTracker.IsActive = false;
247223
};
248224

249225
/// <summary>
@@ -289,17 +265,11 @@ void LoadMembers()
289265
/// <param name="ShowNotification">If set to true, this method will display end of confirmation toast notification</param>
290266
void UpdateTotalCount(bool ShowNotification)
291267
{
292-
293268
// 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
294269
int totalCount = GlobalVars.FinalMegaObject.Values.Sum(list => list.Count);
295270

296-
// Find the TextBlock used to display the total count
297-
TextBlock TotalCountTextBlock = (TextBlock)View.FindName("TotalCountTextBlock");
298-
if (TotalCountTextBlock is not null)
299-
{
300-
// Update the text of the TextBlock to show the total count
301-
TotalCountTextBlock.Text = $"{totalCount} Total Verifiable Security Checks";
302-
}
271+
// Update the text of the TextBlock to show the total count
272+
TotalCountTextBlock.Text = $"{totalCount} Total Verifiable Security Checks";
303273

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

297+
// Event handler for the Export Results button
298+
ExportResultsButton.Click += async (sender, e) =>
299+
{
300+
try
301+
{
302+
ExportResultsButton.IsEnabled = false;
303+
304+
await Task.Run(() =>
305+
{
306+
// Show the save file dialog to let the user pick the save location
307+
Microsoft.Win32.SaveFileDialog saveFileDialog = new()
308+
{
309+
FileName = $"Harden Windows Security Compliance Check Results at {DateTime.Now:yyyy-MM-dd HH-mm-ss}", // Default file name
310+
DefaultExt = ".csv", // Default file extension
311+
Filter = "CSV File (.csv)|*.csv" // Filter files by extension
312+
};
313+
314+
// Show the dialog and check if the user picked a file
315+
bool? result = saveFileDialog.ShowDialog();
316+
317+
if (result == true)
318+
{
319+
// Get the selected file path from the dialog
320+
string filePath = saveFileDialog.FileName;
321+
322+
try
323+
{
324+
ExportSecOpsToCsv(filePath, SecOpsObservableCollection);
325+
}
326+
catch (Exception ex)
327+
{
328+
Logger.LogMessage($"Failed to export the results to the file: {ex.Message}", LogTypeIntel.ErrorInteractionRequired);
329+
}
330+
331+
Logger.LogMessage($"Compliance check results have been successfully exported.", LogTypeIntel.InformationInteractionRequired);
332+
}
333+
});
334+
}
335+
finally
336+
{
337+
ExportResultsButton.IsEnabled = true;
338+
}
339+
};
340+
341+
342+
// To Export the results of the compliance checking to a file
343+
void ExportSecOpsToCsv(string filePath, ObservableCollection<SecOp> secOps)
344+
{
345+
// Defining the header row
346+
string[] headers = ["FriendlyName", "Compliant", "Value", "Name", "Category", "Method"];
347+
348+
// Open the file for writing
349+
using StreamWriter writer = new(filePath);
350+
351+
// Write the header row
352+
writer.WriteLine(string.Join(",", headers.Select(header => $"\"{header}\"")));
353+
354+
// Write each SecOp object as a row in the CSV
355+
foreach (SecOp secOp in secOps)
356+
{
357+
string[] row = [
358+
EscapeForCsv(secOp.FriendlyName),
359+
EscapeForCsv(secOp.Compliant.ToString(CultureInfo.InvariantCulture)),
360+
EscapeForCsv(secOp.Value),
361+
EscapeForCsv(secOp.Name),
362+
EscapeForCsv(secOp.Category.ToString()),
363+
EscapeForCsv(secOp.Method)
364+
];
365+
366+
writer.WriteLine(string.Join(",", row));
367+
}
368+
}
369+
370+
// Local method to enclose values in double quotes
371+
string EscapeForCsv(string? value)
372+
{
373+
// If the value is null, empty or whitespace, return an empty string
374+
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
375+
376+
// Otherwise, escape double quotes and wrap the value in double quotes
377+
return $"\"{value.Replace("\"", "\"\"", StringComparison.OrdinalIgnoreCase)}\"";
378+
}
379+
327380
// Cache the Confirm view for future use
328381
_viewCache["ConfirmView"] = View;
329382

Harden-Windows-Security Module/Main files/C#/GUI/Log/View.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.IO;
3-
using System.Windows;
43
using System.Windows.Controls;
54
using System.Windows.Controls.Primitives;
65
using System.Windows.Markup;
@@ -74,7 +73,7 @@ private void LogsView(object obj)
7473
// Write the text content from the TextBox to the file
7574
File.WriteAllText(filePath, GUILogs.MainLoggerTextBox.Text);
7675

77-
_ = MessageBox.Show("Logs successfully saved.", "Success", MessageBoxButton.OK, MessageBoxImage.Information);
76+
Logger.LogMessage("Logs successfully saved.", LogTypeIntel.InformationInteractionRequired);
7877
}
7978
};
8079

Harden-Windows-Security Module/Main files/C#/GUI/Protection/View.cs

+1-3
Original file line numberDiff line numberDiff line change
@@ -609,9 +609,7 @@ void AddEventHandlers()
609609

610610
#region Display a Welcome message
611611

612-
string nameToDisplay = (!string.IsNullOrWhiteSpace(GlobalVars.userFullName)) ? GlobalVars.userFullName : GlobalVars.userName;
613-
614-
Logger.LogMessage(Environment.IsPrivilegedProcess ? $"Hello {nameToDisplay}, you have Administrator privileges" : $"Hello {nameToDisplay}, you don't have Administrator privileges, some categories are disabled", LogTypeIntel.Information);
612+
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);
615613
#endregion
616614

617615
// Use Dispatcher.Invoke to update the UI thread

Harden-Windows-Security Module/Main files/C#/Others/ConfirmSystemComplianceMethods.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ private static Task VerifyBitLockerSettings()
529529
// OS Drive encryption verifications
530530
// Check if BitLocker is on for the OS Drive
531531
// The ProtectionStatus remains off while the drive is encrypting or decrypting
532-
BitLocker.BitLockerVolume volumeInfo = BitLocker.GetEncryptedVolumeInfo(Environment.GetEnvironmentVariable("SystemDrive") ?? "C:\\");
532+
BitLocker.BitLockerVolume volumeInfo = BitLocker.GetEncryptedVolumeInfo(GlobalVars.SystemDrive);
533533

534534
if (volumeInfo.ProtectionStatus is BitLocker.ProtectionStatus.Protected)
535535
{

0 commit comments

Comments
 (0)