From 162dd8803751610b1c8cedab6bf3abca2dc2e8d8 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Mon, 4 May 2026 17:44:43 -0400 Subject: [PATCH 1/6] =?UTF-8?q?New=20Files=20=E2=80=A2=20FolderHashScanFor?= =?UTF-8?q?m.cs=20=E2=80=94=20New=20dialog=20for=20folder-based=20hash=20f?= =?UTF-8?q?ile=20scanning=20with=20browse,=20subfolder=20toggle,=20file=20?= =?UTF-8?q?checklist,=20and=20hash=20type=20selection=20Modified=20Files?= =?UTF-8?q?=20CustomRuleConditionsPanel.cs=20=E2=80=A2=20Added=20hash=20mo?= =?UTF-8?q?de=20panel=20with=20Single=20File,=20Multiple=20Files=20radio?= =?UTF-8?q?=20buttons=20and=20Folder=20Scan...=20button=20=E2=80=A2=20"Fol?= =?UTF-8?q?der=20Scan..."=20button=20directly=20opens=20FolderHashScanForm?= =?UTF-8?q?=20dialog=20=E2=80=A2=20Multi-file=20and=20folder=20scan=20mode?= =?UTF-8?q?s=20batch=20files=20into=20a=20single=20FolderScan-type=20rule?= =?UTF-8?q?=20using=20New-CIPolicy=20-ScanPath=20(instead=20of=20one=20Pow?= =?UTF-8?q?erShell=20call=20per=20file)=20=E2=80=A2=20Selected=20files=20a?= =?UTF-8?q?re=20copied=20to=20a=20temp=20folder=20preserving=20subfolder?= =?UTF-8?q?=20structure=20from=20the=20source=20=E2=80=A2=20Stores=20Sourc?= =?UTF-8?q?eFolderPath=20and=20HashTypesToKeep=20on=20the=20rule=20for=20d?= =?UTF-8?q?ownstream=20processing=20FolderHashScanForm.cs=20=E2=80=A2=20Br?= =?UTF-8?q?owse=20folder=20with=20optional=20Include=20subfolders=20checkb?= =?UTF-8?q?ox=20=E2=80=A2=20Scan=20Folder=20button=20enumerates=20files=20?= =?UTF-8?q?by=20common=20PE/script=20extensions=20=E2=80=A2=20Select=20All?= =?UTF-8?q?=20/=20Deselect=20All=20for=20the=20file=20checklist=20?= =?UTF-8?q?=E2=80=A2=20Hash=20type=20checkboxes:=20Hash=20SHA1,=20Hash=20S?= =?UTF-8?q?HA256,=20Hash=20Page=20SHA1,=20Hash=20Page=20SHA256,=20and=20Al?= =?UTF-8?q?l=20(toggle)=20=E2=80=A2=20Exposes=20SelectedFiles,=20SelectedH?= =?UTF-8?q?ashTypes,=20SourceFolderPath,=20IncludeSubfolders,=20AllFilesSe?= =?UTF-8?q?lected=20=E2=80=A2=20Dark=20mode=20support=20SigningRules=5FCon?= =?UTF-8?q?trol.cs=20=E2=80=A2=20Added=20AddRuleToTableWithoutClosing()=20?= =?UTF-8?q?method=20to=20support=20batch=20rule=20insertion=20without=20cl?= =?UTF-8?q?osing=20the=20custom=20rules=20panel=20Policy.cs=20(PolicyCusto?= =?UTF-8?q?mRules=20class)=20=E2=80=A2=20Added=20HashTypesToKeep=20propert?= =?UTF-8?q?y=20(HashSet)=20=E2=80=94=20hash=20types=20to=20retain?= =?UTF-8?q?=20when=20filtering=20generated=20policy=20XML=20=E2=80=A2=20Ad?= =?UTF-8?q?ded=20SourceFolderPath=20property=20(string)=20=E2=80=94=20orig?= =?UTF-8?q?inal=20folder=20path=20for=20FriendlyName=20correction=20MainFo?= =?UTF-8?q?rm.cs=20=E2=80=A2=20Added=20using=20System.Linq=20=E2=80=A2=20F?= =?UTF-8?q?riendlyName=20fix:=20after=20scan,=20replaces=20temp=20folder?= =?UTF-8?q?=20path=20in=20each=20rule's=20FriendlyName=20with=20the=20orig?= =?UTF-8?q?inal=20source=20folder=20path=20(preserving=20subfolders)=20?= =?UTF-8?q?=E2=80=A2=20Hash=20type=20filtering:=20removes=20unwanted=20has?= =?UTF-8?q?h=20types=20(Hash=20SHA1,=20Hash=20Page=20SHA256,=20etc.)=20fro?= =?UTF-8?q?m=20generated=20policy=20based=20on=20user's=20checkbox=20selec?= =?UTF-8?q?tions=20=E2=80=A2=20Progress=20bar=20improvements:=20=E2=80=A2?= =?UTF-8?q?=20ProcessCustomValueRules(BackgroundWorker,=20SiPolicy)=20now?= =?UTF-8?q?=20reports=20incremental=20progress=200=E2=80=9325%=20per=20rul?= =?UTF-8?q?e=20=E2=80=A2=20ProcessSignerRules(BackgroundWorker,=20SiPolicy?= =?UTF-8?q?)=20shows=20"Processing=20rule=20X=20of=20Y=20..."=20with=20acc?= =?UTF-8?q?urate=20counts=20=E2=80=A2=20FolderScan=20shows=20phased=20stat?= =?UTF-8?q?us:=20"Scanning=20folder:=20...",=20"Scan=20complete.=20Applyin?= =?UTF-8?q?g=20hash=20type=20filters=20...",=20"Filtered:=20kept=20X=20of?= =?UTF-8?q?=20Y=20hash=20rules.",=20"Merging=20scanned=20policy=20rules=20?= =?UTF-8?q?..."=20=E2=80=A2=20ProgressChanged=20handler=20respects=20custo?= =?UTF-8?q?m=20UserState=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/app/src/FolderHashScanForm.cs | 465 ++++++++++++++++++ .../app/src/CustomRuleConditionsPanel.cs | 319 +++++++++++- WDAC-Policy-Wizard/app/src/MainForm.cs | 101 +++- WDAC-Policy-Wizard/app/src/Policy.cs | 6 + .../app/src/SigningRules_Control.cs | 30 ++ 5 files changed, 911 insertions(+), 10 deletions(-) create mode 100644 WDAC-Policy-Wizard/app/app/src/FolderHashScanForm.cs diff --git a/WDAC-Policy-Wizard/app/app/src/FolderHashScanForm.cs b/WDAC-Policy-Wizard/app/app/src/FolderHashScanForm.cs new file mode 100644 index 00000000..195911ad --- /dev/null +++ b/WDAC-Policy-Wizard/app/app/src/FolderHashScanForm.cs @@ -0,0 +1,465 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Windows.Forms; + +namespace WDAC_Wizard +{ + /// + /// Form that allows users to scan a folder for files and select which ones to create hash rules for. + /// + public class FolderHashScanForm : Form + { + private Label labelFolderPath; + private TextBox textBoxFolderPath; + private Button buttonBrowseFolder; + private CheckBox checkBoxSubfolders; + private Button buttonScan; + private CheckedListBox checkedListBoxFiles; + private Button buttonSelectAll; + private Button buttonDeselectAll; + private Button buttonOK; + private Button buttonCancel; + private Label labelStatus; + private Label labelHashTypes; + private CheckBox checkBoxHashSha1; + private CheckBox checkBoxHashSha256; + private CheckBox checkBoxHashPageSha1; + private CheckBox checkBoxHashPageSha256; + private CheckBox checkBoxHashAll; + private Panel panelHashTypes; + + /// + /// The list of file paths the user selected + /// + public List SelectedFiles { get; private set; } + + /// + /// The original folder path that was scanned + /// + public string SourceFolderPath { get; private set; } + + /// + /// Whether subfolders were included in the scan + /// + public bool IncludeSubfolders { get; private set; } + + /// + /// Whether all scanned files were selected (no unchecking) + /// + public bool AllFilesSelected { get; private set; } + + /// + /// The hash types selected by the user to keep in the generated policy. + /// Values match FriendlyName patterns: "Hash Sha1", "Hash Sha256", "Hash Page Sha1", "Hash Page Sha256" + /// + public HashSet SelectedHashTypes { get; private set; } + + public FolderHashScanForm() + { + SelectedFiles = new List(); + SelectedHashTypes = new HashSet(StringComparer.OrdinalIgnoreCase); + InitializeComponents(); + } + + private void InitializeComponents() + { + this.Text = "Folder Hash Scan"; + this.Size = new Size(700, 600); + this.FormBorderStyle = FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.StartPosition = FormStartPosition.CenterParent; + + int yPos = 15; + + // Folder path label + labelFolderPath = new Label + { + Text = "Folder Path:", + Location = new Point(15, yPos), + Size = new Size(80, 20), + Font = new Font("Tahoma", 9F) + }; + this.Controls.Add(labelFolderPath); + + // Folder path textbox + textBoxFolderPath = new TextBox + { + Location = new Point(100, yPos), + Size = new Size(460, 24), + Font = new Font("Tahoma", 9F), + ReadOnly = true + }; + this.Controls.Add(textBoxFolderPath); + + // Browse button + buttonBrowseFolder = new Button + { + Text = "Browse...", + Location = new Point(570, yPos - 2), + Size = new Size(90, 26), + Font = new Font("Tahoma", 9F) + }; + buttonBrowseFolder.Click += ButtonBrowseFolder_Click; + this.Controls.Add(buttonBrowseFolder); + + yPos += 35; + + // Include subfolders checkbox + checkBoxSubfolders = new CheckBox + { + Text = "Include subfolders", + Location = new Point(100, yPos), + Size = new Size(160, 22), + Font = new Font("Tahoma", 9F), + Checked = false + }; + this.Controls.Add(checkBoxSubfolders); + + // Hash type checkboxes + labelHashTypes = new Label + { + Text = "Hash Types to Keep:", + Location = new Point(280, yPos), + Size = new Size(130, 20), + Font = new Font("Tahoma", 9F) + }; + this.Controls.Add(labelHashTypes); + + panelHashTypes = new Panel + { + Location = new Point(15, yPos + 25), + Size = new Size(650, 28) + }; + + checkBoxHashAll = new CheckBox + { + Text = "All", + Location = new Point(0, 2), + Size = new Size(50, 22), + Font = new Font("Tahoma", 9F), + Checked = true + }; + checkBoxHashAll.CheckedChanged += CheckBoxHashAll_CheckedChanged; + panelHashTypes.Controls.Add(checkBoxHashAll); + + checkBoxHashSha1 = new CheckBox + { + Text = "Hash SHA1", + Location = new Point(55, 2), + Size = new Size(100, 22), + Font = new Font("Tahoma", 9F), + Checked = true + }; + panelHashTypes.Controls.Add(checkBoxHashSha1); + + checkBoxHashSha256 = new CheckBox + { + Text = "Hash SHA256", + Location = new Point(160, 2), + Size = new Size(115, 22), + Font = new Font("Tahoma", 9F), + Checked = true + }; + panelHashTypes.Controls.Add(checkBoxHashSha256); + + checkBoxHashPageSha1 = new CheckBox + { + Text = "Hash Page SHA1", + Location = new Point(280, 2), + Size = new Size(130, 22), + Font = new Font("Tahoma", 9F), + Checked = true + }; + panelHashTypes.Controls.Add(checkBoxHashPageSha1); + + checkBoxHashPageSha256 = new CheckBox + { + Text = "Hash Page SHA256", + Location = new Point(415, 2), + Size = new Size(145, 22), + Font = new Font("Tahoma", 9F), + Checked = true + }; + panelHashTypes.Controls.Add(checkBoxHashPageSha256); + + this.Controls.Add(panelHashTypes); + + yPos += 55; + + // Scan button - on its own row for visibility + buttonScan = new Button + { + Text = "Scan Folder", + Location = new Point(100, yPos - 2), + Size = new Size(120, 28), + Font = new Font("Tahoma", 9F, FontStyle.Bold) + }; + buttonScan.Click += ButtonScan_Click; + this.Controls.Add(buttonScan); + + yPos += 35; + + // Status label + labelStatus = new Label + { + Text = "Select a folder and click Scan to find files.", + Location = new Point(15, yPos), + Size = new Size(650, 20), + Font = new Font("Tahoma", 9F) + }; + this.Controls.Add(labelStatus); + + yPos += 25; + + // Checked list box for files + checkedListBoxFiles = new CheckedListBox + { + Location = new Point(15, yPos), + Size = new Size(645, 320), + Font = new Font("Tahoma", 8.5F), + CheckOnClick = true, + HorizontalScrollbar = true + }; + this.Controls.Add(checkedListBoxFiles); + + yPos += 330; + + // Select All / Deselect All buttons + buttonSelectAll = new Button + { + Text = "Select All", + Location = new Point(15, yPos), + Size = new Size(90, 28), + Font = new Font("Tahoma", 9F) + }; + buttonSelectAll.Click += (s, e) => + { + for (int i = 0; i < checkedListBoxFiles.Items.Count; i++) + checkedListBoxFiles.SetItemChecked(i, true); + }; + this.Controls.Add(buttonSelectAll); + + buttonDeselectAll = new Button + { + Text = "Deselect All", + Location = new Point(115, yPos), + Size = new Size(100, 28), + Font = new Font("Tahoma", 9F) + }; + buttonDeselectAll.Click += (s, e) => + { + for (int i = 0; i < checkedListBoxFiles.Items.Count; i++) + checkedListBoxFiles.SetItemChecked(i, false); + }; + this.Controls.Add(buttonDeselectAll); + + // OK / Cancel buttons + buttonCancel = new Button + { + Text = "Cancel", + Location = new Point(560, yPos), + Size = new Size(100, 28), + Font = new Font("Tahoma", 9F), + DialogResult = DialogResult.Cancel + }; + this.Controls.Add(buttonCancel); + + buttonOK = new Button + { + Text = "Add Selected", + Location = new Point(450, yPos), + Size = new Size(100, 28), + Font = new Font("Tahoma", 9F) + }; + buttonOK.Click += ButtonOK_Click; + this.Controls.Add(buttonOK); + + this.AcceptButton = buttonOK; + this.CancelButton = buttonCancel; + + // Apply dark mode if needed + if (Properties.Settings.Default.useDarkMode) + { + ApplyDarkMode(); + } + } + + private void ApplyDarkMode() + { + this.BackColor = Color.FromArgb(30, 30, 30); + this.ForeColor = Color.White; + + foreach (Control ctrl in this.Controls) + { + ctrl.ForeColor = Color.White; + if (ctrl is TextBox tb) + { + tb.BackColor = Color.FromArgb(15, 15, 15); + } + else if (ctrl is CheckedListBox clb) + { + clb.BackColor = Color.FromArgb(15, 15, 15); + } + else if (ctrl is Button btn) + { + btn.BackColor = Color.FromArgb(50, 50, 50); + btn.FlatStyle = FlatStyle.Flat; + } + else if (ctrl is Panel pnl) + { + pnl.ForeColor = Color.White; + foreach (Control child in pnl.Controls) + { + child.ForeColor = Color.White; + } + } + } + } + + private void ButtonBrowseFolder_Click(object sender, EventArgs e) + { + // Use OpenFileDialog in folder-picker mode via CommonDialog workaround + // FolderBrowserDialog in .NET 8 supports UseDescriptionForTitle for a modern look + FolderBrowserDialog folderDialog = new FolderBrowserDialog(); + folderDialog.Description = "Select a folder to scan for files"; + folderDialog.UseDescriptionForTitle = true; + folderDialog.ShowNewFolderButton = false; + + if (!string.IsNullOrEmpty(textBoxFolderPath.Text) && Directory.Exists(textBoxFolderPath.Text)) + { + folderDialog.InitialDirectory = textBoxFolderPath.Text; + } + + if (folderDialog.ShowDialog() == DialogResult.OK) + { + textBoxFolderPath.Text = folderDialog.SelectedPath; + folderDialog.Dispose(); + } + } + + private void ButtonScan_Click(object sender, EventArgs e) + { + string folderPath = textBoxFolderPath.Text; + if (string.IsNullOrEmpty(folderPath) || !Directory.Exists(folderPath)) + { + labelStatus.Text = "Please select a valid folder path."; + return; + } + + checkedListBoxFiles.Items.Clear(); + + try + { + SearchOption searchOption = checkBoxSubfolders.Checked + ? SearchOption.AllDirectories + : SearchOption.TopDirectoryOnly; + + // Get all files - filter for common PE and script types + string[] extensions = new[] + { + "*.exe", "*.dll", "*.sys", "*.rll", "*.bin", + "*.ps1", "*.bat", "*.vbs", "*.js", + "*.hxs", "*.mui", "*.lex", "*.mof", + "*.msi", "*.msp", "*.ocx", "*.drv", "*.scr", "*.cpl" + }; + + var files = new List(); + foreach (string ext in extensions) + { + try + { + files.AddRange(Directory.GetFiles(folderPath, ext, searchOption)); + } + catch (UnauthorizedAccessException) + { + // Skip folders we can't access + } + } + + // Also allow all files option - if no PE files found, get all files + if (files.Count == 0) + { + try + { + files.AddRange(Directory.GetFiles(folderPath, "*.*", searchOption)); + } + catch (UnauthorizedAccessException) + { + } + } + + files = files.Distinct().OrderBy(f => f).ToList(); + + foreach (string file in files) + { + checkedListBoxFiles.Items.Add(file, true); // Checked by default + } + + labelStatus.Text = $"Found {files.Count} file(s). Select the files to create hash rules for."; + } + catch (Exception ex) + { + labelStatus.Text = $"Error scanning folder: {ex.Message}"; + Logger.Log.AddErrorMsg($"FolderHashScanForm scan error: {ex}"); + } + } + + private void CheckBoxHashAll_CheckedChanged(object sender, EventArgs e) + { + bool isChecked = checkBoxHashAll.Checked; + checkBoxHashSha1.Checked = isChecked; + checkBoxHashSha256.Checked = isChecked; + checkBoxHashPageSha1.Checked = isChecked; + checkBoxHashPageSha256.Checked = isChecked; + } + + private void ButtonOK_Click(object sender, EventArgs e) + { + SelectedFiles.Clear(); + for (int i = 0; i < checkedListBoxFiles.Items.Count; i++) + { + if (checkedListBoxFiles.GetItemChecked(i)) + { + SelectedFiles.Add(checkedListBoxFiles.Items[i].ToString()); + } + } + + // Build the set of hash types to keep + SelectedHashTypes.Clear(); + if (checkBoxHashSha1.Checked) + SelectedHashTypes.Add("Hash Sha1"); + if (checkBoxHashSha256.Checked) + SelectedHashTypes.Add("Hash Sha256"); + if (checkBoxHashPageSha1.Checked) + SelectedHashTypes.Add("Hash Page Sha1"); + if (checkBoxHashPageSha256.Checked) + SelectedHashTypes.Add("Hash Page Sha256"); + // Also handle Authenticode SIP variants (for non-PE files like .js) + if (checkBoxHashSha256.Checked) + SelectedHashTypes.Add("Hash Authenticode SIP Sha256"); + + SourceFolderPath = textBoxFolderPath.Text; + IncludeSubfolders = checkBoxSubfolders.Checked; + AllFilesSelected = (SelectedFiles.Count == checkedListBoxFiles.Items.Count); + + if (SelectedFiles.Count == 0) + { + labelStatus.Text = "Please select at least one file."; + return; + } + + if (SelectedHashTypes.Count == 0) + { + labelStatus.Text = "Please select at least one hash type."; + return; + } + + this.DialogResult = DialogResult.OK; + this.Close(); + } + } +} diff --git a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs index 579b9b3f..0079e0dd 100644 --- a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs +++ b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs @@ -36,6 +36,21 @@ public partial class CustomRuleConditionsPanel : Form private string PrevComText = String.Empty; private bool IgnoreInput = false; + // Hash mode UI controls + private Panel panelHashMode; + private RadioButton radioButton_HashSingleFile; + private RadioButton radioButton_HashMultipleFiles; + private Button button_HashFolderScan; + + private enum HashMode + { + SingleFile, + MultipleFiles, + FolderScan + } + + private HashMode hashMode = HashMode.SingleFile; + private enum UIState { RuleConditions = 0, @@ -60,6 +75,117 @@ public CustomRuleConditionsPanel(SigningRules_Control pControl) this.exceptionsControl = null; this.DefaultValues = new string[5]; this.FoundPackages = new List(); + + // Create hash mode panel programmatically + CreateHashModePanel(); + } + + /// + /// Creates the hash mode selection panel (Single File / Multiple Files / Folder Scan) + /// + private void CreateHashModePanel() + { + panelHashMode = new Panel(); + // Position below the checkBox_CustomPath row, left-aligned with the reference file textbox + panelHashMode.Location = new Point( + this.checkBox_CustomPath.Location.X, + this.checkBox_CustomPath.Location.Y + this.checkBox_CustomPath.Height + 4); + panelHashMode.Size = new Size(560, 30); + panelHashMode.Visible = false; + + Label labelHashMode = new Label + { + Text = "Mode:", + Location = new Point(0, 5), + AutoSize = true, + Font = new Font("Tahoma", 9F, FontStyle.Bold) + }; + panelHashMode.Controls.Add(labelHashMode); + + radioButton_HashSingleFile = new RadioButton + { + Text = "Single File", + Location = new Point(55, 3), + AutoSize = true, + Font = new Font("Tahoma", 9F), + Checked = true + }; + radioButton_HashSingleFile.CheckedChanged += HashModeButton_CheckedChanged; + + radioButton_HashMultipleFiles = new RadioButton + { + Text = "Multiple Files", + Location = new Point(170, 3), + AutoSize = true, + Font = new Font("Tahoma", 9F) + }; + radioButton_HashMultipleFiles.CheckedChanged += HashModeButton_CheckedChanged; + + button_HashFolderScan = new Button + { + Text = "Folder Scan...", + Location = new Point(310, 1), + Size = new Size(110, 26), + Font = new Font("Tahoma", 9F, FontStyle.Bold), + FlatStyle = FlatStyle.Standard + }; + button_HashFolderScan.Click += Button_HashFolderScan_Click; + + panelHashMode.Controls.Add(radioButton_HashSingleFile); + panelHashMode.Controls.Add(radioButton_HashMultipleFiles); + panelHashMode.Controls.Add(button_HashFolderScan); + + // Add to the same parent container as panel_FileFolder + this.panel_FileFolder.Parent.Controls.Add(panelHashMode); + panelHashMode.BringToFront(); + + // Apply styling based on mode + if (Properties.Settings.Default.useDarkMode) + { + panelHashMode.BackColor = Color.FromArgb(15, 15, 15); + panelHashMode.ForeColor = Color.White; + foreach (Control ctrl in panelHashMode.Controls) + { + ctrl.ForeColor = Color.White; + ctrl.BackColor = Color.FromArgb(15, 15, 15); + } + button_HashFolderScan.FlatStyle = FlatStyle.Flat; + button_HashFolderScan.BackColor = Color.FromArgb(50, 50, 50); + } + else + { + panelHashMode.BackColor = Color.White; + panelHashMode.ForeColor = Color.Black; + foreach (Control ctrl in panelHashMode.Controls) + { + ctrl.ForeColor = Color.Black; + ctrl.BackColor = Color.White; + } + } + } + + /// + /// Handles hash mode radio button changes + /// + private void HashModeButton_CheckedChanged(object sender, EventArgs e) + { + if (radioButton_HashSingleFile.Checked) + { + hashMode = HashMode.SingleFile; + } + else if (radioButton_HashMultipleFiles.Checked) + { + hashMode = HashMode.MultipleFiles; + } + } + + /// + /// Folder Scan button click - opens FolderHashScanForm directly + /// + private void Button_HashFolderScan_Click(object sender, EventArgs e) + { + hashMode = HashMode.FolderScan; + HandleHashMultiFileOrFolderBrowse(); } /// @@ -785,9 +911,16 @@ private void RuleType_ComboboxChanged(object sender, EventArgs e) this.PolicyCustomRule.SetRuleType(PolicyCustomRules.RuleType.Hash); this.PolicyCustomRule.SetRuleLevel(PolicyCustomRules.RuleLevel.Hash); label_Info.Text = "Creates a rule for a file that is not signed. \r\n" + - "Select the file for which you wish to create a hash rule."; + "Select single file, multiple files, or scan a folder."; this.checkBox_CustomPath.Visible = true; this.checkBox_CustomPath.Text = "Use Custom Hash Values"; + this.panelHashMode.Location = new Point( + this.checkBox_CustomPath.Location.X, + this.checkBox_CustomPath.Location.Y + this.checkBox_CustomPath.Height + 4); + this.panelHashMode.Visible = true; + this.panelHashMode.BringToFront(); + this.radioButton_HashSingleFile.Checked = true; + this.hashMode = HashMode.SingleFile; break; case "COM Object": @@ -882,6 +1015,7 @@ private void ClearCustomRulesPanel(bool clearComboBox = false) //File Path: panel_FileFolder.Visible = false; + panelHashMode.Visible = false; textBox_ReferenceFile.Clear(); // Reset the rule type combobox @@ -916,6 +1050,15 @@ private void Button_Browse_Click(object sender, EventArgs e) return; } + // Handle Hash rule multi-file and folder scan modes + if (this.PolicyCustomRule.Type == PolicyCustomRules.RuleType.Hash + && this.hashMode != HashMode.SingleFile + && !this.PolicyCustomRule.UsingCustomValues) + { + HandleHashMultiFileOrFolderBrowse(); + return; + } + if (this.PolicyCustomRule.Type != PolicyCustomRules.RuleType.FolderPath && this.PolicyCustomRule.Type != PolicyCustomRules.RuleType.FolderScan) { @@ -963,6 +1106,180 @@ private void Button_Browse_Click(object sender, EventArgs e) } } + /// + /// Handles the browse action for Hash rules in Multiple Files or Folder Scan mode. + /// Creates a single FolderScan rule with Hash level for efficient batch processing + /// instead of one PS process per file. + /// + private void HandleHashMultiFileOrFolderBrowse() + { + string scanFolderPath = null; + HashSet hashTypesFilter = null; + string sourceFolderPath = null; + List omitPaths = new List(); + + if (this.hashMode == HashMode.MultipleFiles) + { + // Open multi-select file dialog + OpenFileDialog openFileDialog = new OpenFileDialog(); + openFileDialog.Title = "Select files to create hash rules for"; + openFileDialog.CheckPathExists = true; + openFileDialog.Filter = "Portable Executable Files (*.exe; *.dll; *.rll; *.bin)|*.EXE;*.DLL;*.RLL;*.BIN|" + + "Script Files (*.ps1, *.bat, *.vbs, *.js)|*.PS1;*.BAT;*.VBS;*.JS|" + + "System Files (*.sys, *.hxs, *.mui, *.lex, *.mof)|*.SYS;*.HXS;*.MUI;*.LEX;*.MOF|" + + "All Binary and Script Files (*.exe, ...) |*.EXE;*.DLL;*.RLL;*.BIN;*.PS1;*.BAT;*.VBS;*.JS;*.SYS;*.HXS;*.MUI;*.LEX;*.MOF|" + + "All files (*.*)|*.*"; + openFileDialog.FilterIndex = 4; + openFileDialog.RestoreDirectory = true; + openFileDialog.Multiselect = true; + + if (openFileDialog.ShowDialog() == DialogResult.OK) + { + var selectedFiles = openFileDialog.FileNames; + openFileDialog.Dispose(); + + if (selectedFiles.Length == 0) + { + return; + } + + // Copy selected files to a temp folder for batch scanning + scanFolderPath = Path.Combine(Helper.GetTempFolderPathRoot(), "HashScan_" + DateTime.Now.ToString("HHmmss")); + Directory.CreateDirectory(scanFolderPath); + + foreach (string file in selectedFiles) + { + try + { + string destFile = Path.Combine(scanFolderPath, Path.GetFileName(file)); + // Handle duplicate filenames by appending a guid + if (File.Exists(destFile)) + { + string nameNoExt = Path.GetFileNameWithoutExtension(file); + string ext = Path.GetExtension(file); + destFile = Path.Combine(scanFolderPath, nameNoExt + "_" + Guid.NewGuid().ToString("N").Substring(0, 6) + ext); + } + File.Copy(file, destFile); + } + catch (Exception ex) + { + Logger.Log.AddErrorMsg($"Failed to copy file {file} for hash scan: {ex.Message}"); + } + } + } + else + { + return; + } + } + else if (this.hashMode == HashMode.FolderScan) + { + // Open the folder hash scan form + HashSet hashTypesToKeep = null; + using (var folderScanForm = new FolderHashScanForm()) + { + if (folderScanForm.ShowDialog() == DialogResult.OK) + { + var selectedFiles = folderScanForm.SelectedFiles; + hashTypesToKeep = folderScanForm.SelectedHashTypes; + if (selectedFiles.Count == 0) + { + label_Error.Visible = true; + label_Error.Text = "No files selected."; + return; + } + + // Copy selected files to temp preserving subfolder structure + scanFolderPath = Path.Combine(Helper.GetTempFolderPathRoot(), "HashScan_" + DateTime.Now.ToString("HHmmss")); + Directory.CreateDirectory(scanFolderPath); + + sourceFolderPath = folderScanForm.SourceFolderPath; + + foreach (string file in selectedFiles) + { + try + { + // Preserve relative path from source folder + string relativePath = string.IsNullOrEmpty(sourceFolderPath) || !file.StartsWith(sourceFolderPath, StringComparison.OrdinalIgnoreCase) + ? Path.GetFileName(file) + : file.Substring(sourceFolderPath.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + + string destFile = Path.Combine(scanFolderPath, relativePath); + string destDir = Path.GetDirectoryName(destFile); + if (!Directory.Exists(destDir)) + Directory.CreateDirectory(destDir); + + if (File.Exists(destFile)) + { + string nameNoExt = Path.GetFileNameWithoutExtension(destFile); + string ext = Path.GetExtension(destFile); + destFile = Path.Combine(destDir, nameNoExt + "_" + Guid.NewGuid().ToString("N").Substring(0, 6) + ext); + } + File.Copy(file, destFile); + } + catch (Exception ex) + { + Logger.Log.AddErrorMsg($"Failed to copy file {file} for hash scan: {ex.Message}"); + } + } + } + else + { + return; + } + } + + // Store hash types for filtering after scan + if (hashTypesToKeep != null) + { + hashTypesFilter = hashTypesToKeep; + } + } + + if (scanFolderPath == null || !Directory.Exists(scanFolderPath)) + { + label_Error.Visible = true; + label_Error.Text = "Failed to prepare files for scanning."; + return; + } + + // Create a single FolderScan rule with Hash level - this uses New-CIPolicy -ScanPath + // which processes ALL files in one PowerShell call (much faster than one call per file) + PolicyCustomRules rule = new PolicyCustomRules(); + rule.SetRuleType(PolicyCustomRules.RuleType.FolderScan); + rule.SetRuleLevel(PolicyCustomRules.RuleLevel.Hash); + rule.Permission = this.PolicyCustomRule.Permission; + rule.SigningScenarioCheckStates = this.PolicyCustomRule.SigningScenarioCheckStates; + rule.ReferenceFile = scanFolderPath; + rule.Scan.Levels.Add("Hash"); + rule.HashTypesToKeep = hashTypesFilter; + rule.SourceFolderPath = sourceFolderPath; + + int fileCount = Directory.GetFiles(scanFolderPath, "*", SearchOption.TopDirectoryOnly).Length; + string displayPath = sourceFolderPath ?? scanFolderPath; + string[] displayString = new string[5] + { + rule.Permission.ToString(), + "Hash", + $"Hash Folder Scan ({fileCount} files): " + displayPath, + String.Empty, + String.Empty + }; + + this.SigningControl.AddRuleToTableWithoutClosing(displayString, rule, false); + + Logger.Log.AddInfoMsg($"Added folder scan hash rule for {fileCount} files at {scanFolderPath}"); + + // Reset UI + this.PolicyCustomRule = new PolicyCustomRules(); + ClearCustomRulesPanel(true); + this._MainWindow.CustomRuleinProgress = false; + + // Close the panel + this.RuleInEdit = false; + this.SigningControl.CloseCustomRulesPanel(); + } + /// /// Retrieves the file attribute and signer info /// diff --git a/WDAC-Policy-Wizard/app/src/MainForm.cs b/WDAC-Policy-Wizard/app/src/MainForm.cs index 1c81bcd1..68781537 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Drawing; +using System.Linq; using System.Windows.Forms; using System.IO; using WDAC_Wizard.src; @@ -851,7 +852,13 @@ private void BackgroundWorker1_ProgressChanged(object sender, ProgressChangedEve { string process = ""; int progressPercent = e.ProgressPercentage; - if (progressPercent <= 10) + + // Use custom status message if provided via UserState + if (e.UserState is string customMsg && !string.IsNullOrEmpty(customMsg)) + { + process = customMsg; + } + else if (progressPercent <= 10) process = "Building policy rules ..."; else if (progressPercent <= 70) process = "Configuring policy signer and file rules ..."; @@ -1249,23 +1256,28 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) int nCustomRules = this.Policy.CustomRules.Count; int progressVal = 0; + // Count how many rules will actually be processed (Publisher, Hash, FolderScan, Certificate) + int rulesToProcess = this.Policy.CustomRules.Count(r => + !r.UsingCustomValues && + (r.Type == PolicyCustomRules.RuleType.Publisher + || r.Type == PolicyCustomRules.RuleType.Hash + || r.Type == PolicyCustomRules.RuleType.FolderScan + || r.Type == PolicyCustomRules.RuleType.Certificate)); + int rulesProcessed = 0; + // Iterate through all of the custom rules and update the progress bar for (int i = 0; i < nCustomRules; i++) { - progressVal = 25 + i * 60 / nCustomRules; - worker.ReportProgress(progressVal); //Assumes the operations involved with this step take about 70% -- probably should be a little higher - var customRule = this.Policy.CustomRules[i]; // Skip; already handled ALL custom value rules - if(customRule.UsingCustomValues) + if (customRule.UsingCustomValues) { continue; } - // Skip the following rules that are handled by custom rules method - - // File Attributes, PFN rules, file/folder path rules - if (!(customRule.Type == PolicyCustomRules.RuleType.Publisher + // Skip the following rules that are handled by custom rules method + if (!(customRule.Type == PolicyCustomRules.RuleType.Publisher || customRule.Type == PolicyCustomRules.RuleType.Hash || customRule.Type == PolicyCustomRules.RuleType.FolderScan || customRule.Type == PolicyCustomRules.RuleType.Certificate)) @@ -1273,6 +1285,11 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) continue; } + rulesProcessed++; + progressVal = 25 + rulesProcessed * 60 / Math.Max(rulesToProcess, 1); + string statusMsg = $"Processing rule {rulesProcessed} of {rulesToProcess} ..."; + worker.ReportProgress(progressVal, statusMsg); + string tmpPolicyPath = Helper.GetUniquePolicyPath(this.TempFolderPath); // Create a single policy per rule using the Powershell cmdlets with Level=PCACertificate or Publisher @@ -1303,6 +1320,9 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) // Folder Scan -- Invoke the New-CiPolicy PS cmd to generate a CI policy if(customRule.Type == PolicyCustomRules.RuleType.FolderScan) { + string scanPath = customRule.ReferenceFile ?? "folder"; + worker.ReportProgress(30, $"Scanning folder: {scanPath} ..."); + SiPolicy signerSiPolicy; if (this.Policy._PolicyType == WDAC_Policy.PolicyType.BasePolicy) { @@ -1312,10 +1332,65 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) { signerSiPolicy = PSCmdlets.CreateScannedPolicyFromPS(customRule, tmpPolicyPath, this.Policy.BaseToSupplementPath); } - + + worker.ReportProgress(70, "Scan complete. Applying hash type filters ..."); + // Successful Scan completed if (signerSiPolicy != null) { + // Fix FriendlyName: replace temp scan path with original source folder path + if (!string.IsNullOrEmpty(customRule.SourceFolderPath) + && signerSiPolicy.FileRules != null) + { + string tempPath = customRule.ReferenceFile; + foreach (var item in signerSiPolicy.FileRules) + { + if (item is Allow allow && !string.IsNullOrEmpty(allow.FriendlyName) + && allow.FriendlyName.Contains(tempPath, StringComparison.OrdinalIgnoreCase)) + { + allow.FriendlyName = allow.FriendlyName.Replace(tempPath, customRule.SourceFolderPath, StringComparison.OrdinalIgnoreCase); + } + else if (item is Deny deny && !string.IsNullOrEmpty(deny.FriendlyName) + && deny.FriendlyName.Contains(tempPath, StringComparison.OrdinalIgnoreCase)) + { + deny.FriendlyName = deny.FriendlyName.Replace(tempPath, customRule.SourceFolderPath, StringComparison.OrdinalIgnoreCase); + } + } + } + + // Filter hash types if the user selected specific types to keep + if (customRule.HashTypesToKeep != null && customRule.HashTypesToKeep.Count > 0 + && signerSiPolicy.FileRules != null) + { + int beforeCount = signerSiPolicy.FileRules.Length; + signerSiPolicy.FileRules = signerSiPolicy.FileRules.Where(item => + { + string friendlyName = null; + if (item is Allow allow) + friendlyName = allow.FriendlyName; + else if (item is Deny deny) + friendlyName = deny.FriendlyName; + + // If it's not a hash rule (no FriendlyName with hash pattern), keep it + if (string.IsNullOrEmpty(friendlyName)) + return true; + + // Check if FriendlyName ends with one of the selected hash type patterns + foreach (string hashType in customRule.HashTypesToKeep) + { + if (friendlyName.EndsWith(hashType, StringComparison.OrdinalIgnoreCase)) + return true; + } + + // If it doesn't match any selected hash type pattern, remove it + return false; + }).ToArray(); + + int afterCount = signerSiPolicy.FileRules.Length; + worker.ReportProgress(75, $"Filtered: kept {afterCount} of {beforeCount} hash rules."); + } + + worker.ReportProgress(80, "Merging scanned policy rules ..."); siPolicy = PolicyHelper.MergePolicies(signerSiPolicy, siPolicy); } } @@ -1345,9 +1420,17 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) /// public SiPolicy ProcessCustomValueRules(BackgroundWorker worker, SiPolicy siPolicyCustomValueRules) { + int totalRules = this.Policy.CustomRules.Count; + int processed = 0; + // Iterate through all of the custom rules and PFN rules and update the progress bar foreach (var customRule in this.Policy.CustomRules) { + processed++; + int progressVal = processed * 25 / Math.Max(totalRules, 1); + string statusMsg = $"Processing custom value rule {processed} of {totalRules} ..."; + worker.ReportProgress(progressVal, statusMsg); + if(customRule.UsingCustomValues) { siPolicyCustomValueRules = HandleCustomValues(customRule, siPolicyCustomValueRules); diff --git a/WDAC-Policy-Wizard/app/src/Policy.cs b/WDAC-Policy-Wizard/app/src/Policy.cs index 19476ebb..00b947af 100644 --- a/WDAC-Policy-Wizard/app/src/Policy.cs +++ b/WDAC-Policy-Wizard/app/src/Policy.cs @@ -640,6 +640,12 @@ public enum RulePermission { Allow, Deny }; // Folder Scan public FolderScan Scan { get; set; } + // Hash types to keep when filtering generated policy XML (e.g., "Hash Sha1", "Hash Sha256", "Hash Page Sha1", "Hash Page Sha256") + public HashSet HashTypesToKeep { get; set; } + + // Original source folder path (used to fix FriendlyName in generated policy when scanning from temp) + public string SourceFolderPath { get; set; } + // Constructors public PolicyCustomRules() { diff --git a/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs b/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs index 41fb16fd..1d48fed4 100644 --- a/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs +++ b/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs @@ -1196,6 +1196,36 @@ public void AddRuleToTable(string [] displayObjectArray, PolicyCustomRules custo this.customRuleConditionsPanel = null; } + /// + /// Adds a new rule to the DataGrid Table without closing the custom rules panel. + /// Used when adding multiple rules at once (e.g., multi-file hash scan). + /// + /// + /// + /// + public void AddRuleToTableWithoutClosing(string[] displayObjectArray, PolicyCustomRules customRule, bool warnUser) + { + // Attach the int row number we added it to + customRule.RowNumber = this.rulesDataGrid.RowCount - 1; + string action = displayObjectArray[0]; + string level = displayObjectArray[1]; + string name = warnUser ? "*Hash Fallback Possible* " + displayObjectArray[2] : displayObjectArray[2]; + string files = displayObjectArray[3]; + string exceptions = displayObjectArray[4]; + + // Add to the DisplayObject + this.displayObjects.Add(new DisplayObject(action, level, name, files, exceptions)); + this.rulesDataGrid.RowCount += 1; + + // Add custom list to RulesList + this.Policy.CustomRules.Add(customRule); + + // Scroll to bottom to see new rule added to list + this.rulesDataGrid.FirstDisplayedScrollingRowIndex = this.rulesDataGrid.RowCount - 1; + + BubbleUp(); + } + /// /// Nullifies the custom rule conditions panel on form closing /// From 354c4f435be4ec40524ddfe9c15180670bb67c74 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Tue, 5 May 2026 12:45:35 -0400 Subject: [PATCH 2/6] Revert "New Files" This reverts commit 162dd8803751610b1c8cedab6bf3abca2dc2e8d8. --- .../app/app/src/FolderHashScanForm.cs | 465 ------------------ .../app/src/CustomRuleConditionsPanel.cs | 319 +----------- WDAC-Policy-Wizard/app/src/MainForm.cs | 101 +--- WDAC-Policy-Wizard/app/src/Policy.cs | 6 - .../app/src/SigningRules_Control.cs | 30 -- 5 files changed, 10 insertions(+), 911 deletions(-) delete mode 100644 WDAC-Policy-Wizard/app/app/src/FolderHashScanForm.cs diff --git a/WDAC-Policy-Wizard/app/app/src/FolderHashScanForm.cs b/WDAC-Policy-Wizard/app/app/src/FolderHashScanForm.cs deleted file mode 100644 index 195911ad..00000000 --- a/WDAC-Policy-Wizard/app/app/src/FolderHashScanForm.cs +++ /dev/null @@ -1,465 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Windows.Forms; - -namespace WDAC_Wizard -{ - /// - /// Form that allows users to scan a folder for files and select which ones to create hash rules for. - /// - public class FolderHashScanForm : Form - { - private Label labelFolderPath; - private TextBox textBoxFolderPath; - private Button buttonBrowseFolder; - private CheckBox checkBoxSubfolders; - private Button buttonScan; - private CheckedListBox checkedListBoxFiles; - private Button buttonSelectAll; - private Button buttonDeselectAll; - private Button buttonOK; - private Button buttonCancel; - private Label labelStatus; - private Label labelHashTypes; - private CheckBox checkBoxHashSha1; - private CheckBox checkBoxHashSha256; - private CheckBox checkBoxHashPageSha1; - private CheckBox checkBoxHashPageSha256; - private CheckBox checkBoxHashAll; - private Panel panelHashTypes; - - /// - /// The list of file paths the user selected - /// - public List SelectedFiles { get; private set; } - - /// - /// The original folder path that was scanned - /// - public string SourceFolderPath { get; private set; } - - /// - /// Whether subfolders were included in the scan - /// - public bool IncludeSubfolders { get; private set; } - - /// - /// Whether all scanned files were selected (no unchecking) - /// - public bool AllFilesSelected { get; private set; } - - /// - /// The hash types selected by the user to keep in the generated policy. - /// Values match FriendlyName patterns: "Hash Sha1", "Hash Sha256", "Hash Page Sha1", "Hash Page Sha256" - /// - public HashSet SelectedHashTypes { get; private set; } - - public FolderHashScanForm() - { - SelectedFiles = new List(); - SelectedHashTypes = new HashSet(StringComparer.OrdinalIgnoreCase); - InitializeComponents(); - } - - private void InitializeComponents() - { - this.Text = "Folder Hash Scan"; - this.Size = new Size(700, 600); - this.FormBorderStyle = FormBorderStyle.FixedDialog; - this.MaximizeBox = false; - this.MinimizeBox = false; - this.StartPosition = FormStartPosition.CenterParent; - - int yPos = 15; - - // Folder path label - labelFolderPath = new Label - { - Text = "Folder Path:", - Location = new Point(15, yPos), - Size = new Size(80, 20), - Font = new Font("Tahoma", 9F) - }; - this.Controls.Add(labelFolderPath); - - // Folder path textbox - textBoxFolderPath = new TextBox - { - Location = new Point(100, yPos), - Size = new Size(460, 24), - Font = new Font("Tahoma", 9F), - ReadOnly = true - }; - this.Controls.Add(textBoxFolderPath); - - // Browse button - buttonBrowseFolder = new Button - { - Text = "Browse...", - Location = new Point(570, yPos - 2), - Size = new Size(90, 26), - Font = new Font("Tahoma", 9F) - }; - buttonBrowseFolder.Click += ButtonBrowseFolder_Click; - this.Controls.Add(buttonBrowseFolder); - - yPos += 35; - - // Include subfolders checkbox - checkBoxSubfolders = new CheckBox - { - Text = "Include subfolders", - Location = new Point(100, yPos), - Size = new Size(160, 22), - Font = new Font("Tahoma", 9F), - Checked = false - }; - this.Controls.Add(checkBoxSubfolders); - - // Hash type checkboxes - labelHashTypes = new Label - { - Text = "Hash Types to Keep:", - Location = new Point(280, yPos), - Size = new Size(130, 20), - Font = new Font("Tahoma", 9F) - }; - this.Controls.Add(labelHashTypes); - - panelHashTypes = new Panel - { - Location = new Point(15, yPos + 25), - Size = new Size(650, 28) - }; - - checkBoxHashAll = new CheckBox - { - Text = "All", - Location = new Point(0, 2), - Size = new Size(50, 22), - Font = new Font("Tahoma", 9F), - Checked = true - }; - checkBoxHashAll.CheckedChanged += CheckBoxHashAll_CheckedChanged; - panelHashTypes.Controls.Add(checkBoxHashAll); - - checkBoxHashSha1 = new CheckBox - { - Text = "Hash SHA1", - Location = new Point(55, 2), - Size = new Size(100, 22), - Font = new Font("Tahoma", 9F), - Checked = true - }; - panelHashTypes.Controls.Add(checkBoxHashSha1); - - checkBoxHashSha256 = new CheckBox - { - Text = "Hash SHA256", - Location = new Point(160, 2), - Size = new Size(115, 22), - Font = new Font("Tahoma", 9F), - Checked = true - }; - panelHashTypes.Controls.Add(checkBoxHashSha256); - - checkBoxHashPageSha1 = new CheckBox - { - Text = "Hash Page SHA1", - Location = new Point(280, 2), - Size = new Size(130, 22), - Font = new Font("Tahoma", 9F), - Checked = true - }; - panelHashTypes.Controls.Add(checkBoxHashPageSha1); - - checkBoxHashPageSha256 = new CheckBox - { - Text = "Hash Page SHA256", - Location = new Point(415, 2), - Size = new Size(145, 22), - Font = new Font("Tahoma", 9F), - Checked = true - }; - panelHashTypes.Controls.Add(checkBoxHashPageSha256); - - this.Controls.Add(panelHashTypes); - - yPos += 55; - - // Scan button - on its own row for visibility - buttonScan = new Button - { - Text = "Scan Folder", - Location = new Point(100, yPos - 2), - Size = new Size(120, 28), - Font = new Font("Tahoma", 9F, FontStyle.Bold) - }; - buttonScan.Click += ButtonScan_Click; - this.Controls.Add(buttonScan); - - yPos += 35; - - // Status label - labelStatus = new Label - { - Text = "Select a folder and click Scan to find files.", - Location = new Point(15, yPos), - Size = new Size(650, 20), - Font = new Font("Tahoma", 9F) - }; - this.Controls.Add(labelStatus); - - yPos += 25; - - // Checked list box for files - checkedListBoxFiles = new CheckedListBox - { - Location = new Point(15, yPos), - Size = new Size(645, 320), - Font = new Font("Tahoma", 8.5F), - CheckOnClick = true, - HorizontalScrollbar = true - }; - this.Controls.Add(checkedListBoxFiles); - - yPos += 330; - - // Select All / Deselect All buttons - buttonSelectAll = new Button - { - Text = "Select All", - Location = new Point(15, yPos), - Size = new Size(90, 28), - Font = new Font("Tahoma", 9F) - }; - buttonSelectAll.Click += (s, e) => - { - for (int i = 0; i < checkedListBoxFiles.Items.Count; i++) - checkedListBoxFiles.SetItemChecked(i, true); - }; - this.Controls.Add(buttonSelectAll); - - buttonDeselectAll = new Button - { - Text = "Deselect All", - Location = new Point(115, yPos), - Size = new Size(100, 28), - Font = new Font("Tahoma", 9F) - }; - buttonDeselectAll.Click += (s, e) => - { - for (int i = 0; i < checkedListBoxFiles.Items.Count; i++) - checkedListBoxFiles.SetItemChecked(i, false); - }; - this.Controls.Add(buttonDeselectAll); - - // OK / Cancel buttons - buttonCancel = new Button - { - Text = "Cancel", - Location = new Point(560, yPos), - Size = new Size(100, 28), - Font = new Font("Tahoma", 9F), - DialogResult = DialogResult.Cancel - }; - this.Controls.Add(buttonCancel); - - buttonOK = new Button - { - Text = "Add Selected", - Location = new Point(450, yPos), - Size = new Size(100, 28), - Font = new Font("Tahoma", 9F) - }; - buttonOK.Click += ButtonOK_Click; - this.Controls.Add(buttonOK); - - this.AcceptButton = buttonOK; - this.CancelButton = buttonCancel; - - // Apply dark mode if needed - if (Properties.Settings.Default.useDarkMode) - { - ApplyDarkMode(); - } - } - - private void ApplyDarkMode() - { - this.BackColor = Color.FromArgb(30, 30, 30); - this.ForeColor = Color.White; - - foreach (Control ctrl in this.Controls) - { - ctrl.ForeColor = Color.White; - if (ctrl is TextBox tb) - { - tb.BackColor = Color.FromArgb(15, 15, 15); - } - else if (ctrl is CheckedListBox clb) - { - clb.BackColor = Color.FromArgb(15, 15, 15); - } - else if (ctrl is Button btn) - { - btn.BackColor = Color.FromArgb(50, 50, 50); - btn.FlatStyle = FlatStyle.Flat; - } - else if (ctrl is Panel pnl) - { - pnl.ForeColor = Color.White; - foreach (Control child in pnl.Controls) - { - child.ForeColor = Color.White; - } - } - } - } - - private void ButtonBrowseFolder_Click(object sender, EventArgs e) - { - // Use OpenFileDialog in folder-picker mode via CommonDialog workaround - // FolderBrowserDialog in .NET 8 supports UseDescriptionForTitle for a modern look - FolderBrowserDialog folderDialog = new FolderBrowserDialog(); - folderDialog.Description = "Select a folder to scan for files"; - folderDialog.UseDescriptionForTitle = true; - folderDialog.ShowNewFolderButton = false; - - if (!string.IsNullOrEmpty(textBoxFolderPath.Text) && Directory.Exists(textBoxFolderPath.Text)) - { - folderDialog.InitialDirectory = textBoxFolderPath.Text; - } - - if (folderDialog.ShowDialog() == DialogResult.OK) - { - textBoxFolderPath.Text = folderDialog.SelectedPath; - folderDialog.Dispose(); - } - } - - private void ButtonScan_Click(object sender, EventArgs e) - { - string folderPath = textBoxFolderPath.Text; - if (string.IsNullOrEmpty(folderPath) || !Directory.Exists(folderPath)) - { - labelStatus.Text = "Please select a valid folder path."; - return; - } - - checkedListBoxFiles.Items.Clear(); - - try - { - SearchOption searchOption = checkBoxSubfolders.Checked - ? SearchOption.AllDirectories - : SearchOption.TopDirectoryOnly; - - // Get all files - filter for common PE and script types - string[] extensions = new[] - { - "*.exe", "*.dll", "*.sys", "*.rll", "*.bin", - "*.ps1", "*.bat", "*.vbs", "*.js", - "*.hxs", "*.mui", "*.lex", "*.mof", - "*.msi", "*.msp", "*.ocx", "*.drv", "*.scr", "*.cpl" - }; - - var files = new List(); - foreach (string ext in extensions) - { - try - { - files.AddRange(Directory.GetFiles(folderPath, ext, searchOption)); - } - catch (UnauthorizedAccessException) - { - // Skip folders we can't access - } - } - - // Also allow all files option - if no PE files found, get all files - if (files.Count == 0) - { - try - { - files.AddRange(Directory.GetFiles(folderPath, "*.*", searchOption)); - } - catch (UnauthorizedAccessException) - { - } - } - - files = files.Distinct().OrderBy(f => f).ToList(); - - foreach (string file in files) - { - checkedListBoxFiles.Items.Add(file, true); // Checked by default - } - - labelStatus.Text = $"Found {files.Count} file(s). Select the files to create hash rules for."; - } - catch (Exception ex) - { - labelStatus.Text = $"Error scanning folder: {ex.Message}"; - Logger.Log.AddErrorMsg($"FolderHashScanForm scan error: {ex}"); - } - } - - private void CheckBoxHashAll_CheckedChanged(object sender, EventArgs e) - { - bool isChecked = checkBoxHashAll.Checked; - checkBoxHashSha1.Checked = isChecked; - checkBoxHashSha256.Checked = isChecked; - checkBoxHashPageSha1.Checked = isChecked; - checkBoxHashPageSha256.Checked = isChecked; - } - - private void ButtonOK_Click(object sender, EventArgs e) - { - SelectedFiles.Clear(); - for (int i = 0; i < checkedListBoxFiles.Items.Count; i++) - { - if (checkedListBoxFiles.GetItemChecked(i)) - { - SelectedFiles.Add(checkedListBoxFiles.Items[i].ToString()); - } - } - - // Build the set of hash types to keep - SelectedHashTypes.Clear(); - if (checkBoxHashSha1.Checked) - SelectedHashTypes.Add("Hash Sha1"); - if (checkBoxHashSha256.Checked) - SelectedHashTypes.Add("Hash Sha256"); - if (checkBoxHashPageSha1.Checked) - SelectedHashTypes.Add("Hash Page Sha1"); - if (checkBoxHashPageSha256.Checked) - SelectedHashTypes.Add("Hash Page Sha256"); - // Also handle Authenticode SIP variants (for non-PE files like .js) - if (checkBoxHashSha256.Checked) - SelectedHashTypes.Add("Hash Authenticode SIP Sha256"); - - SourceFolderPath = textBoxFolderPath.Text; - IncludeSubfolders = checkBoxSubfolders.Checked; - AllFilesSelected = (SelectedFiles.Count == checkedListBoxFiles.Items.Count); - - if (SelectedFiles.Count == 0) - { - labelStatus.Text = "Please select at least one file."; - return; - } - - if (SelectedHashTypes.Count == 0) - { - labelStatus.Text = "Please select at least one hash type."; - return; - } - - this.DialogResult = DialogResult.OK; - this.Close(); - } - } -} diff --git a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs index 0079e0dd..579b9b3f 100644 --- a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs +++ b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs @@ -36,21 +36,6 @@ public partial class CustomRuleConditionsPanel : Form private string PrevComText = String.Empty; private bool IgnoreInput = false; - // Hash mode UI controls - private Panel panelHashMode; - private RadioButton radioButton_HashSingleFile; - private RadioButton radioButton_HashMultipleFiles; - private Button button_HashFolderScan; - - private enum HashMode - { - SingleFile, - MultipleFiles, - FolderScan - } - - private HashMode hashMode = HashMode.SingleFile; - private enum UIState { RuleConditions = 0, @@ -75,117 +60,6 @@ public CustomRuleConditionsPanel(SigningRules_Control pControl) this.exceptionsControl = null; this.DefaultValues = new string[5]; this.FoundPackages = new List(); - - // Create hash mode panel programmatically - CreateHashModePanel(); - } - - /// - /// Creates the hash mode selection panel (Single File / Multiple Files / Folder Scan) - /// - private void CreateHashModePanel() - { - panelHashMode = new Panel(); - // Position below the checkBox_CustomPath row, left-aligned with the reference file textbox - panelHashMode.Location = new Point( - this.checkBox_CustomPath.Location.X, - this.checkBox_CustomPath.Location.Y + this.checkBox_CustomPath.Height + 4); - panelHashMode.Size = new Size(560, 30); - panelHashMode.Visible = false; - - Label labelHashMode = new Label - { - Text = "Mode:", - Location = new Point(0, 5), - AutoSize = true, - Font = new Font("Tahoma", 9F, FontStyle.Bold) - }; - panelHashMode.Controls.Add(labelHashMode); - - radioButton_HashSingleFile = new RadioButton - { - Text = "Single File", - Location = new Point(55, 3), - AutoSize = true, - Font = new Font("Tahoma", 9F), - Checked = true - }; - radioButton_HashSingleFile.CheckedChanged += HashModeButton_CheckedChanged; - - radioButton_HashMultipleFiles = new RadioButton - { - Text = "Multiple Files", - Location = new Point(170, 3), - AutoSize = true, - Font = new Font("Tahoma", 9F) - }; - radioButton_HashMultipleFiles.CheckedChanged += HashModeButton_CheckedChanged; - - button_HashFolderScan = new Button - { - Text = "Folder Scan...", - Location = new Point(310, 1), - Size = new Size(110, 26), - Font = new Font("Tahoma", 9F, FontStyle.Bold), - FlatStyle = FlatStyle.Standard - }; - button_HashFolderScan.Click += Button_HashFolderScan_Click; - - panelHashMode.Controls.Add(radioButton_HashSingleFile); - panelHashMode.Controls.Add(radioButton_HashMultipleFiles); - panelHashMode.Controls.Add(button_HashFolderScan); - - // Add to the same parent container as panel_FileFolder - this.panel_FileFolder.Parent.Controls.Add(panelHashMode); - panelHashMode.BringToFront(); - - // Apply styling based on mode - if (Properties.Settings.Default.useDarkMode) - { - panelHashMode.BackColor = Color.FromArgb(15, 15, 15); - panelHashMode.ForeColor = Color.White; - foreach (Control ctrl in panelHashMode.Controls) - { - ctrl.ForeColor = Color.White; - ctrl.BackColor = Color.FromArgb(15, 15, 15); - } - button_HashFolderScan.FlatStyle = FlatStyle.Flat; - button_HashFolderScan.BackColor = Color.FromArgb(50, 50, 50); - } - else - { - panelHashMode.BackColor = Color.White; - panelHashMode.ForeColor = Color.Black; - foreach (Control ctrl in panelHashMode.Controls) - { - ctrl.ForeColor = Color.Black; - ctrl.BackColor = Color.White; - } - } - } - - /// - /// Handles hash mode radio button changes - /// - private void HashModeButton_CheckedChanged(object sender, EventArgs e) - { - if (radioButton_HashSingleFile.Checked) - { - hashMode = HashMode.SingleFile; - } - else if (radioButton_HashMultipleFiles.Checked) - { - hashMode = HashMode.MultipleFiles; - } - } - - /// - /// Folder Scan button click - opens FolderHashScanForm directly - /// - private void Button_HashFolderScan_Click(object sender, EventArgs e) - { - hashMode = HashMode.FolderScan; - HandleHashMultiFileOrFolderBrowse(); } /// @@ -911,16 +785,9 @@ private void RuleType_ComboboxChanged(object sender, EventArgs e) this.PolicyCustomRule.SetRuleType(PolicyCustomRules.RuleType.Hash); this.PolicyCustomRule.SetRuleLevel(PolicyCustomRules.RuleLevel.Hash); label_Info.Text = "Creates a rule for a file that is not signed. \r\n" + - "Select single file, multiple files, or scan a folder."; + "Select the file for which you wish to create a hash rule."; this.checkBox_CustomPath.Visible = true; this.checkBox_CustomPath.Text = "Use Custom Hash Values"; - this.panelHashMode.Location = new Point( - this.checkBox_CustomPath.Location.X, - this.checkBox_CustomPath.Location.Y + this.checkBox_CustomPath.Height + 4); - this.panelHashMode.Visible = true; - this.panelHashMode.BringToFront(); - this.radioButton_HashSingleFile.Checked = true; - this.hashMode = HashMode.SingleFile; break; case "COM Object": @@ -1015,7 +882,6 @@ private void ClearCustomRulesPanel(bool clearComboBox = false) //File Path: panel_FileFolder.Visible = false; - panelHashMode.Visible = false; textBox_ReferenceFile.Clear(); // Reset the rule type combobox @@ -1050,15 +916,6 @@ private void Button_Browse_Click(object sender, EventArgs e) return; } - // Handle Hash rule multi-file and folder scan modes - if (this.PolicyCustomRule.Type == PolicyCustomRules.RuleType.Hash - && this.hashMode != HashMode.SingleFile - && !this.PolicyCustomRule.UsingCustomValues) - { - HandleHashMultiFileOrFolderBrowse(); - return; - } - if (this.PolicyCustomRule.Type != PolicyCustomRules.RuleType.FolderPath && this.PolicyCustomRule.Type != PolicyCustomRules.RuleType.FolderScan) { @@ -1106,180 +963,6 @@ private void Button_Browse_Click(object sender, EventArgs e) } } - /// - /// Handles the browse action for Hash rules in Multiple Files or Folder Scan mode. - /// Creates a single FolderScan rule with Hash level for efficient batch processing - /// instead of one PS process per file. - /// - private void HandleHashMultiFileOrFolderBrowse() - { - string scanFolderPath = null; - HashSet hashTypesFilter = null; - string sourceFolderPath = null; - List omitPaths = new List(); - - if (this.hashMode == HashMode.MultipleFiles) - { - // Open multi-select file dialog - OpenFileDialog openFileDialog = new OpenFileDialog(); - openFileDialog.Title = "Select files to create hash rules for"; - openFileDialog.CheckPathExists = true; - openFileDialog.Filter = "Portable Executable Files (*.exe; *.dll; *.rll; *.bin)|*.EXE;*.DLL;*.RLL;*.BIN|" + - "Script Files (*.ps1, *.bat, *.vbs, *.js)|*.PS1;*.BAT;*.VBS;*.JS|" + - "System Files (*.sys, *.hxs, *.mui, *.lex, *.mof)|*.SYS;*.HXS;*.MUI;*.LEX;*.MOF|" + - "All Binary and Script Files (*.exe, ...) |*.EXE;*.DLL;*.RLL;*.BIN;*.PS1;*.BAT;*.VBS;*.JS;*.SYS;*.HXS;*.MUI;*.LEX;*.MOF|" + - "All files (*.*)|*.*"; - openFileDialog.FilterIndex = 4; - openFileDialog.RestoreDirectory = true; - openFileDialog.Multiselect = true; - - if (openFileDialog.ShowDialog() == DialogResult.OK) - { - var selectedFiles = openFileDialog.FileNames; - openFileDialog.Dispose(); - - if (selectedFiles.Length == 0) - { - return; - } - - // Copy selected files to a temp folder for batch scanning - scanFolderPath = Path.Combine(Helper.GetTempFolderPathRoot(), "HashScan_" + DateTime.Now.ToString("HHmmss")); - Directory.CreateDirectory(scanFolderPath); - - foreach (string file in selectedFiles) - { - try - { - string destFile = Path.Combine(scanFolderPath, Path.GetFileName(file)); - // Handle duplicate filenames by appending a guid - if (File.Exists(destFile)) - { - string nameNoExt = Path.GetFileNameWithoutExtension(file); - string ext = Path.GetExtension(file); - destFile = Path.Combine(scanFolderPath, nameNoExt + "_" + Guid.NewGuid().ToString("N").Substring(0, 6) + ext); - } - File.Copy(file, destFile); - } - catch (Exception ex) - { - Logger.Log.AddErrorMsg($"Failed to copy file {file} for hash scan: {ex.Message}"); - } - } - } - else - { - return; - } - } - else if (this.hashMode == HashMode.FolderScan) - { - // Open the folder hash scan form - HashSet hashTypesToKeep = null; - using (var folderScanForm = new FolderHashScanForm()) - { - if (folderScanForm.ShowDialog() == DialogResult.OK) - { - var selectedFiles = folderScanForm.SelectedFiles; - hashTypesToKeep = folderScanForm.SelectedHashTypes; - if (selectedFiles.Count == 0) - { - label_Error.Visible = true; - label_Error.Text = "No files selected."; - return; - } - - // Copy selected files to temp preserving subfolder structure - scanFolderPath = Path.Combine(Helper.GetTempFolderPathRoot(), "HashScan_" + DateTime.Now.ToString("HHmmss")); - Directory.CreateDirectory(scanFolderPath); - - sourceFolderPath = folderScanForm.SourceFolderPath; - - foreach (string file in selectedFiles) - { - try - { - // Preserve relative path from source folder - string relativePath = string.IsNullOrEmpty(sourceFolderPath) || !file.StartsWith(sourceFolderPath, StringComparison.OrdinalIgnoreCase) - ? Path.GetFileName(file) - : file.Substring(sourceFolderPath.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - - string destFile = Path.Combine(scanFolderPath, relativePath); - string destDir = Path.GetDirectoryName(destFile); - if (!Directory.Exists(destDir)) - Directory.CreateDirectory(destDir); - - if (File.Exists(destFile)) - { - string nameNoExt = Path.GetFileNameWithoutExtension(destFile); - string ext = Path.GetExtension(destFile); - destFile = Path.Combine(destDir, nameNoExt + "_" + Guid.NewGuid().ToString("N").Substring(0, 6) + ext); - } - File.Copy(file, destFile); - } - catch (Exception ex) - { - Logger.Log.AddErrorMsg($"Failed to copy file {file} for hash scan: {ex.Message}"); - } - } - } - else - { - return; - } - } - - // Store hash types for filtering after scan - if (hashTypesToKeep != null) - { - hashTypesFilter = hashTypesToKeep; - } - } - - if (scanFolderPath == null || !Directory.Exists(scanFolderPath)) - { - label_Error.Visible = true; - label_Error.Text = "Failed to prepare files for scanning."; - return; - } - - // Create a single FolderScan rule with Hash level - this uses New-CIPolicy -ScanPath - // which processes ALL files in one PowerShell call (much faster than one call per file) - PolicyCustomRules rule = new PolicyCustomRules(); - rule.SetRuleType(PolicyCustomRules.RuleType.FolderScan); - rule.SetRuleLevel(PolicyCustomRules.RuleLevel.Hash); - rule.Permission = this.PolicyCustomRule.Permission; - rule.SigningScenarioCheckStates = this.PolicyCustomRule.SigningScenarioCheckStates; - rule.ReferenceFile = scanFolderPath; - rule.Scan.Levels.Add("Hash"); - rule.HashTypesToKeep = hashTypesFilter; - rule.SourceFolderPath = sourceFolderPath; - - int fileCount = Directory.GetFiles(scanFolderPath, "*", SearchOption.TopDirectoryOnly).Length; - string displayPath = sourceFolderPath ?? scanFolderPath; - string[] displayString = new string[5] - { - rule.Permission.ToString(), - "Hash", - $"Hash Folder Scan ({fileCount} files): " + displayPath, - String.Empty, - String.Empty - }; - - this.SigningControl.AddRuleToTableWithoutClosing(displayString, rule, false); - - Logger.Log.AddInfoMsg($"Added folder scan hash rule for {fileCount} files at {scanFolderPath}"); - - // Reset UI - this.PolicyCustomRule = new PolicyCustomRules(); - ClearCustomRulesPanel(true); - this._MainWindow.CustomRuleinProgress = false; - - // Close the panel - this.RuleInEdit = false; - this.SigningControl.CloseCustomRulesPanel(); - } - /// /// Retrieves the file attribute and signer info /// diff --git a/WDAC-Policy-Wizard/app/src/MainForm.cs b/WDAC-Policy-Wizard/app/src/MainForm.cs index 68781537..1c81bcd1 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.Drawing; -using System.Linq; using System.Windows.Forms; using System.IO; using WDAC_Wizard.src; @@ -852,13 +851,7 @@ private void BackgroundWorker1_ProgressChanged(object sender, ProgressChangedEve { string process = ""; int progressPercent = e.ProgressPercentage; - - // Use custom status message if provided via UserState - if (e.UserState is string customMsg && !string.IsNullOrEmpty(customMsg)) - { - process = customMsg; - } - else if (progressPercent <= 10) + if (progressPercent <= 10) process = "Building policy rules ..."; else if (progressPercent <= 70) process = "Configuring policy signer and file rules ..."; @@ -1256,28 +1249,23 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) int nCustomRules = this.Policy.CustomRules.Count; int progressVal = 0; - // Count how many rules will actually be processed (Publisher, Hash, FolderScan, Certificate) - int rulesToProcess = this.Policy.CustomRules.Count(r => - !r.UsingCustomValues && - (r.Type == PolicyCustomRules.RuleType.Publisher - || r.Type == PolicyCustomRules.RuleType.Hash - || r.Type == PolicyCustomRules.RuleType.FolderScan - || r.Type == PolicyCustomRules.RuleType.Certificate)); - int rulesProcessed = 0; - // Iterate through all of the custom rules and update the progress bar for (int i = 0; i < nCustomRules; i++) { + progressVal = 25 + i * 60 / nCustomRules; + worker.ReportProgress(progressVal); //Assumes the operations involved with this step take about 70% -- probably should be a little higher + var customRule = this.Policy.CustomRules[i]; // Skip; already handled ALL custom value rules - if (customRule.UsingCustomValues) + if(customRule.UsingCustomValues) { continue; } - // Skip the following rules that are handled by custom rules method - if (!(customRule.Type == PolicyCustomRules.RuleType.Publisher + // Skip the following rules that are handled by custom rules method - + // File Attributes, PFN rules, file/folder path rules + if (!(customRule.Type == PolicyCustomRules.RuleType.Publisher || customRule.Type == PolicyCustomRules.RuleType.Hash || customRule.Type == PolicyCustomRules.RuleType.FolderScan || customRule.Type == PolicyCustomRules.RuleType.Certificate)) @@ -1285,11 +1273,6 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) continue; } - rulesProcessed++; - progressVal = 25 + rulesProcessed * 60 / Math.Max(rulesToProcess, 1); - string statusMsg = $"Processing rule {rulesProcessed} of {rulesToProcess} ..."; - worker.ReportProgress(progressVal, statusMsg); - string tmpPolicyPath = Helper.GetUniquePolicyPath(this.TempFolderPath); // Create a single policy per rule using the Powershell cmdlets with Level=PCACertificate or Publisher @@ -1320,9 +1303,6 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) // Folder Scan -- Invoke the New-CiPolicy PS cmd to generate a CI policy if(customRule.Type == PolicyCustomRules.RuleType.FolderScan) { - string scanPath = customRule.ReferenceFile ?? "folder"; - worker.ReportProgress(30, $"Scanning folder: {scanPath} ..."); - SiPolicy signerSiPolicy; if (this.Policy._PolicyType == WDAC_Policy.PolicyType.BasePolicy) { @@ -1332,65 +1312,10 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) { signerSiPolicy = PSCmdlets.CreateScannedPolicyFromPS(customRule, tmpPolicyPath, this.Policy.BaseToSupplementPath); } - - worker.ReportProgress(70, "Scan complete. Applying hash type filters ..."); - + // Successful Scan completed if (signerSiPolicy != null) { - // Fix FriendlyName: replace temp scan path with original source folder path - if (!string.IsNullOrEmpty(customRule.SourceFolderPath) - && signerSiPolicy.FileRules != null) - { - string tempPath = customRule.ReferenceFile; - foreach (var item in signerSiPolicy.FileRules) - { - if (item is Allow allow && !string.IsNullOrEmpty(allow.FriendlyName) - && allow.FriendlyName.Contains(tempPath, StringComparison.OrdinalIgnoreCase)) - { - allow.FriendlyName = allow.FriendlyName.Replace(tempPath, customRule.SourceFolderPath, StringComparison.OrdinalIgnoreCase); - } - else if (item is Deny deny && !string.IsNullOrEmpty(deny.FriendlyName) - && deny.FriendlyName.Contains(tempPath, StringComparison.OrdinalIgnoreCase)) - { - deny.FriendlyName = deny.FriendlyName.Replace(tempPath, customRule.SourceFolderPath, StringComparison.OrdinalIgnoreCase); - } - } - } - - // Filter hash types if the user selected specific types to keep - if (customRule.HashTypesToKeep != null && customRule.HashTypesToKeep.Count > 0 - && signerSiPolicy.FileRules != null) - { - int beforeCount = signerSiPolicy.FileRules.Length; - signerSiPolicy.FileRules = signerSiPolicy.FileRules.Where(item => - { - string friendlyName = null; - if (item is Allow allow) - friendlyName = allow.FriendlyName; - else if (item is Deny deny) - friendlyName = deny.FriendlyName; - - // If it's not a hash rule (no FriendlyName with hash pattern), keep it - if (string.IsNullOrEmpty(friendlyName)) - return true; - - // Check if FriendlyName ends with one of the selected hash type patterns - foreach (string hashType in customRule.HashTypesToKeep) - { - if (friendlyName.EndsWith(hashType, StringComparison.OrdinalIgnoreCase)) - return true; - } - - // If it doesn't match any selected hash type pattern, remove it - return false; - }).ToArray(); - - int afterCount = signerSiPolicy.FileRules.Length; - worker.ReportProgress(75, $"Filtered: kept {afterCount} of {beforeCount} hash rules."); - } - - worker.ReportProgress(80, "Merging scanned policy rules ..."); siPolicy = PolicyHelper.MergePolicies(signerSiPolicy, siPolicy); } } @@ -1420,17 +1345,9 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) /// public SiPolicy ProcessCustomValueRules(BackgroundWorker worker, SiPolicy siPolicyCustomValueRules) { - int totalRules = this.Policy.CustomRules.Count; - int processed = 0; - // Iterate through all of the custom rules and PFN rules and update the progress bar foreach (var customRule in this.Policy.CustomRules) { - processed++; - int progressVal = processed * 25 / Math.Max(totalRules, 1); - string statusMsg = $"Processing custom value rule {processed} of {totalRules} ..."; - worker.ReportProgress(progressVal, statusMsg); - if(customRule.UsingCustomValues) { siPolicyCustomValueRules = HandleCustomValues(customRule, siPolicyCustomValueRules); diff --git a/WDAC-Policy-Wizard/app/src/Policy.cs b/WDAC-Policy-Wizard/app/src/Policy.cs index 00b947af..19476ebb 100644 --- a/WDAC-Policy-Wizard/app/src/Policy.cs +++ b/WDAC-Policy-Wizard/app/src/Policy.cs @@ -640,12 +640,6 @@ public enum RulePermission { Allow, Deny }; // Folder Scan public FolderScan Scan { get; set; } - // Hash types to keep when filtering generated policy XML (e.g., "Hash Sha1", "Hash Sha256", "Hash Page Sha1", "Hash Page Sha256") - public HashSet HashTypesToKeep { get; set; } - - // Original source folder path (used to fix FriendlyName in generated policy when scanning from temp) - public string SourceFolderPath { get; set; } - // Constructors public PolicyCustomRules() { diff --git a/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs b/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs index 1d48fed4..41fb16fd 100644 --- a/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs +++ b/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs @@ -1196,36 +1196,6 @@ public void AddRuleToTable(string [] displayObjectArray, PolicyCustomRules custo this.customRuleConditionsPanel = null; } - /// - /// Adds a new rule to the DataGrid Table without closing the custom rules panel. - /// Used when adding multiple rules at once (e.g., multi-file hash scan). - /// - /// - /// - /// - public void AddRuleToTableWithoutClosing(string[] displayObjectArray, PolicyCustomRules customRule, bool warnUser) - { - // Attach the int row number we added it to - customRule.RowNumber = this.rulesDataGrid.RowCount - 1; - string action = displayObjectArray[0]; - string level = displayObjectArray[1]; - string name = warnUser ? "*Hash Fallback Possible* " + displayObjectArray[2] : displayObjectArray[2]; - string files = displayObjectArray[3]; - string exceptions = displayObjectArray[4]; - - // Add to the DisplayObject - this.displayObjects.Add(new DisplayObject(action, level, name, files, exceptions)); - this.rulesDataGrid.RowCount += 1; - - // Add custom list to RulesList - this.Policy.CustomRules.Add(customRule); - - // Scroll to bottom to see new rule added to list - this.rulesDataGrid.FirstDisplayedScrollingRowIndex = this.rulesDataGrid.RowCount - 1; - - BubbleUp(); - } - /// /// Nullifies the custom rule conditions panel on form closing /// From e7f6333fe256c277d9c3ea709325292e495d92e8 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Tue, 5 May 2026 13:42:14 -0400 Subject: [PATCH 3/6] File Scanner UI updates, smoother controls --- .../src/CustomRuleConditionsPanel.Designer.cs | 24 ++++++--- .../app/src/CustomRuleConditionsPanel.cs | 51 ++++++++++++++++++- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs index 9da2f1a8..e65d9490 100644 --- a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs @@ -154,6 +154,7 @@ private void InitializeComponent() panel_CustomRules.Margin = new System.Windows.Forms.Padding(2); panel_CustomRules.Name = "panel_CustomRules"; panel_CustomRules.Size = new System.Drawing.Size(615, 719); + panel_CustomRules.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; panel_CustomRules.TabIndex = 86; // // appIdPanel @@ -371,17 +372,22 @@ private void InitializeComponent() panelFolderScanConditions.Location = new System.Drawing.Point(552, 630); panelFolderScanConditions.Margin = new System.Windows.Forms.Padding(2); panelFolderScanConditions.Name = "panelFolderScanConditions"; - panelFolderScanConditions.Size = new System.Drawing.Size(537, 359); + panelFolderScanConditions.Size = new System.Drawing.Size(560, 340); + panelFolderScanConditions.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; panelFolderScanConditions.TabIndex = 126; panelFolderScanConditions.Visible = false; // // checkedListBoxOmitPaths // - checkedListBoxOmitPaths.Font = new System.Drawing.Font("Tahoma", 8F); + checkedListBoxOmitPaths.Font = new System.Drawing.Font("Tahoma", 8.5F); checkedListBoxOmitPaths.FormattingEnabled = true; - checkedListBoxOmitPaths.Location = new System.Drawing.Point(8, 260); + checkedListBoxOmitPaths.CheckOnClick = true; + checkedListBoxOmitPaths.HorizontalScrollbar = true; + checkedListBoxOmitPaths.IntegralHeight = false; + checkedListBoxOmitPaths.Location = new System.Drawing.Point(8, 250); checkedListBoxOmitPaths.Name = "checkedListBoxOmitPaths"; - checkedListBoxOmitPaths.Size = new System.Drawing.Size(341, 80); + checkedListBoxOmitPaths.Size = new System.Drawing.Size(500, 80); + checkedListBoxOmitPaths.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; checkedListBoxOmitPaths.TabIndex = 116; // // label12 @@ -401,7 +407,7 @@ private void InitializeComponent() label11.AutoSize = true; label11.Font = new System.Drawing.Font("Tahoma", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, 0); label11.ForeColor = System.Drawing.Color.Black; - label11.Location = new System.Drawing.Point(5, 232); + label11.Location = new System.Drawing.Point(5, 228); label11.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); label11.Name = "label11"; label11.Size = new System.Drawing.Size(231, 18); @@ -429,13 +435,15 @@ private void InitializeComponent() // checkedListBoxRuleLevels // checkedListBoxRuleLevels.AllowDrop = true; + checkedListBoxRuleLevels.CheckOnClick = true; checkedListBoxRuleLevels.Font = new System.Drawing.Font("Tahoma", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0); checkedListBoxRuleLevels.FormattingEnabled = true; checkedListBoxRuleLevels.Items.AddRange(new object[] { "PcaCertificate", "Publisher", "SignedVersion", "FilePublisher", "FileName", "FilePath", "Hash" }); checkedListBoxRuleLevels.Location = new System.Drawing.Point(8, 121); checkedListBoxRuleLevels.MultiColumn = true; + checkedListBoxRuleLevels.ColumnWidth = 160; checkedListBoxRuleLevels.Name = "checkedListBoxRuleLevels"; - checkedListBoxRuleLevels.Size = new System.Drawing.Size(341, 88); + checkedListBoxRuleLevels.Size = new System.Drawing.Size(500, 100); checkedListBoxRuleLevels.TabIndex = 111; checkedListBoxRuleLevels.DragDrop += RuleLevelsList_DragDropDone; checkedListBoxRuleLevels.DragOver += RuleLevelsList_DragInProgress; @@ -1071,6 +1079,7 @@ private void InitializeComponent() button_CreateRule.Margin = new System.Windows.Forms.Padding(2); button_CreateRule.Name = "button_CreateRule"; button_CreateRule.Size = new System.Drawing.Size(110, 30); + button_CreateRule.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; button_CreateRule.TabIndex = 92; button_CreateRule.Text = "Create Rule"; button_CreateRule.UseVisualStyleBackColor = false; @@ -1083,6 +1092,7 @@ private void InitializeComponent() button_Next.Margin = new System.Windows.Forms.Padding(2); button_Next.Name = "button_Next"; button_Next.Size = new System.Drawing.Size(99, 30); + button_Next.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; button_Next.TabIndex = 107; button_Next.Text = "Next >"; button_Next.UseVisualStyleBackColor = false; @@ -1195,6 +1205,7 @@ private void InitializeComponent() button_AddException.Margin = new System.Windows.Forms.Padding(2); button_AddException.Name = "button_AddException"; button_AddException.Size = new System.Drawing.Size(110, 30); + button_AddException.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; button_AddException.TabIndex = 111; button_AddException.Text = "Add Exception"; button_AddException.UseVisualStyleBackColor = false; @@ -1209,6 +1220,7 @@ private void InitializeComponent() button_Back.Margin = new System.Windows.Forms.Padding(2); button_Back.Name = "button_Back"; button_Back.Size = new System.Drawing.Size(99, 30); + button_Back.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; button_Back.TabIndex = 110; button_Back.Text = "< Back"; button_Back.UseVisualStyleBackColor = false; diff --git a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs index 579b9b3f..1a92ff92 100644 --- a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs +++ b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs @@ -60,6 +60,44 @@ public CustomRuleConditionsPanel(SigningRules_Control pControl) this.exceptionsControl = null; this.DefaultValues = new string[5]; this.FoundPackages = new List(); + CreateOmitPathsButtons(); + } + + /// + /// Creates Select All / Deselect All buttons above the omit paths list. + /// + private void CreateOmitPathsButtons() + { + var btnSelectAll = new Button + { + Text = "Select All", + Size = new System.Drawing.Size(80, 25), + Font = new System.Drawing.Font("Tahoma", 7.5F), + Location = new System.Drawing.Point(checkedListBoxOmitPaths.Right - 165, checkedListBoxOmitPaths.Top - 28), + Anchor = AnchorStyles.Top | AnchorStyles.Right + }; + btnSelectAll.Click += (s, ev) => + { + for (int i = 0; i < checkedListBoxOmitPaths.Items.Count; i++) + checkedListBoxOmitPaths.SetItemChecked(i, true); + }; + + var btnDeselectAll = new Button + { + Text = "Deselect All", + Size = new System.Drawing.Size(80, 25), + Font = new System.Drawing.Font("Tahoma", 7.5F), + Location = new System.Drawing.Point(checkedListBoxOmitPaths.Right - 80, checkedListBoxOmitPaths.Top - 28), + Anchor = AnchorStyles.Top | AnchorStyles.Right + }; + btnDeselectAll.Click += (s, ev) => + { + for (int i = 0; i < checkedListBoxOmitPaths.Items.Count; i++) + checkedListBoxOmitPaths.SetItemChecked(i, false); + }; + + panelFolderScanConditions.Controls.Add(btnSelectAll); + panelFolderScanConditions.Controls.Add(btnDeselectAll); } /// @@ -817,6 +855,7 @@ private void RuleType_ComboboxChanged(object sender, EventArgs e) this.panelFolderScanConditions.Location = this.checkBox_CustomPath.Location; this.panelFolderScanConditions.Visible = true; this.label_condition.Text = "Scan Path:"; + this.button_Next.Visible = false; break; case "Certificate File": @@ -2734,7 +2773,17 @@ private void LabelFolderScanLearnMore_Click(object sender, EventArgs e) /// private void RuleLevelsList_MouseDown(object sender, MouseEventArgs e) { - if (this.checkedListBoxRuleLevels.SelectedItem == null || e.X < 15 || (e.X > 150 && e.X < 165)) return; // e.X < 15 - left most column checkboxes. 150 < e.X < 165 - right most checkboxes + if (this.checkedListBoxRuleLevels.SelectedItem == null) return; + + // Determine checkbox width relative to each column + int columnWidth = this.checkedListBoxRuleLevels.ColumnWidth > 0 + ? this.checkedListBoxRuleLevels.ColumnWidth + : this.checkedListBoxRuleLevels.Width; + int xInColumn = e.X % columnWidth; + + // If click is in the checkbox area (first ~18px of each column), let CheckOnClick handle it + if (xInColumn < 18) return; + this.checkedListBoxRuleLevels.DoDragDrop(this.checkedListBoxRuleLevels.SelectedItem, DragDropEffects.Move); } From 1b15dc38b4aa42f8cdfbdbd4b0667ed4c66fce4c Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Tue, 5 May 2026 15:37:49 -0400 Subject: [PATCH 4/6] =?UTF-8?q?PSCmdlets.cs=20=E2=80=A2=20Added=20-NoLogo?= =?UTF-8?q?=20-NonInteractive=20flags=20to=20PowerShell=20invocation=20to?= =?UTF-8?q?=20reduce=20startup=20overhead=20=E2=80=A2=20Fixed=20potential?= =?UTF-8?q?=20deadlock:=20moved=20StandardOutput.ReadToEnd()=20and=20Stand?= =?UTF-8?q?ardError.ReadToEnd()=20before=20WaitForExit()=20to=20prevent=20?= =?UTF-8?q?buffer-full=20hang?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MainForm.cs • Fixed progress bar stalling at 25% during Folder Scan by reporting progress after skipping non-applicable rules • Added mid-scan progress report (~55%) before CreateScannedPolicyFromPS(PolicyCustomRules, string, string) so UI shows activity during long scans • Updated progress status text: "Scanning and processing rules (this may take a few minutes) ..." for the 25-80% range --- .../app/MSIX/CreateScannedPolicy.ps1 | 15 ++++++--------- .../app/Scripts/CreateScannedPolicy.ps1 | 15 ++++++--------- WDAC-Policy-Wizard/app/src/MainForm.cs | 17 ++++++++++------- WDAC-Policy-Wizard/app/src/PSCmdlets.cs | 6 +++--- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/WDAC-Policy-Wizard/app/MSIX/CreateScannedPolicy.ps1 b/WDAC-Policy-Wizard/app/MSIX/CreateScannedPolicy.ps1 index 467c71f2..d35fb73a 100644 --- a/WDAC-Policy-Wizard/app/MSIX/CreateScannedPolicy.ps1 +++ b/WDAC-Policy-Wizard/app/MSIX/CreateScannedPolicy.ps1 @@ -16,31 +16,28 @@ param ( ) # Run New-CIPolicy -Scan to generate a policy from a directory -# The command needs to be run twice to generate the full policy. Otherwise, the "An item with the same key has already been added." WARNING prevents the full policy from being generated. +# Use -WarningAction SilentlyContinue to suppress the "An item with the same key has already been added." warning +# which previously required running the command twice as a workaround. if($Deny -eq "False") { if($UserPEs -eq "True") { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs -WarningAction SilentlyContinue } else { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -WarningAction SilentlyContinue } } else { if($UserPEs -eq "True") { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs -Deny - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs -Deny + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs -Deny -WarningAction SilentlyContinue } else { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -Deny - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -Deny + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -Deny -WarningAction SilentlyContinue } } # SIG # Begin signature block diff --git a/WDAC-Policy-Wizard/app/Scripts/CreateScannedPolicy.ps1 b/WDAC-Policy-Wizard/app/Scripts/CreateScannedPolicy.ps1 index 467c71f2..d35fb73a 100644 --- a/WDAC-Policy-Wizard/app/Scripts/CreateScannedPolicy.ps1 +++ b/WDAC-Policy-Wizard/app/Scripts/CreateScannedPolicy.ps1 @@ -16,31 +16,28 @@ param ( ) # Run New-CIPolicy -Scan to generate a policy from a directory -# The command needs to be run twice to generate the full policy. Otherwise, the "An item with the same key has already been added." WARNING prevents the full policy from being generated. +# Use -WarningAction SilentlyContinue to suppress the "An item with the same key has already been added." warning +# which previously required running the command twice as a workaround. if($Deny -eq "False") { if($UserPEs -eq "True") { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs -WarningAction SilentlyContinue } else { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -WarningAction SilentlyContinue } } else { if($UserPEs -eq "True") { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs -Deny - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs -Deny + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs -Deny -WarningAction SilentlyContinue } else { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -Deny - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -Deny + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -Deny -WarningAction SilentlyContinue } } # SIG # Begin signature block diff --git a/WDAC-Policy-Wizard/app/src/MainForm.cs b/WDAC-Policy-Wizard/app/src/MainForm.cs index 1c81bcd1..3eb9600a 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.cs @@ -853,10 +853,10 @@ private void BackgroundWorker1_ProgressChanged(object sender, ProgressChangedEve int progressPercent = e.ProgressPercentage; if (progressPercent <= 10) process = "Building policy rules ..."; - else if (progressPercent <= 70) + else if (progressPercent <= 25) process = "Configuring policy signer and file rules ..."; else if (progressPercent <= 80) - process = "Building custom policy file rules ..."; + process = "Scanning and processing rules (this may take a few minutes) ..."; else if (progressPercent <= 85) process = "Merging custom rules policies ..."; else if (progressPercent <= 95) @@ -1252,9 +1252,6 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) // Iterate through all of the custom rules and update the progress bar for (int i = 0; i < nCustomRules; i++) { - progressVal = 25 + i * 60 / nCustomRules; - worker.ReportProgress(progressVal); //Assumes the operations involved with this step take about 70% -- probably should be a little higher - var customRule = this.Policy.CustomRules[i]; // Skip; already handled ALL custom value rules @@ -1273,6 +1270,9 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) continue; } + progressVal = 25 + i * 60 / nCustomRules; + worker.ReportProgress(progressVal); + string tmpPolicyPath = Helper.GetUniquePolicyPath(this.TempFolderPath); // Create a single policy per rule using the Powershell cmdlets with Level=PCACertificate or Publisher @@ -1288,7 +1288,7 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) siPolicy = PolicyHelper.MergePolicies(signerSiPolicy, siPolicy); } } - + // Hash Rules -- Invoke Powershell cmd to generate if(customRule.Type == PolicyCustomRules.RuleType.Hash) { @@ -1303,6 +1303,9 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) // Folder Scan -- Invoke the New-CiPolicy PS cmd to generate a CI policy if(customRule.Type == PolicyCustomRules.RuleType.FolderScan) { + // Report a mid-range progress so the UI shows scanning activity + worker.ReportProgress(Math.Min(progressVal + 30, 80)); + SiPolicy signerSiPolicy; if (this.Policy._PolicyType == WDAC_Policy.PolicyType.BasePolicy) { @@ -1312,7 +1315,7 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) { signerSiPolicy = PSCmdlets.CreateScannedPolicyFromPS(customRule, tmpPolicyPath, this.Policy.BaseToSupplementPath); } - + // Successful Scan completed if (signerSiPolicy != null) { diff --git a/WDAC-Policy-Wizard/app/src/PSCmdlets.cs b/WDAC-Policy-Wizard/app/src/PSCmdlets.cs index 557e6c05..867cdfbc 100644 --- a/WDAC-Policy-Wizard/app/src/PSCmdlets.cs +++ b/WDAC-Policy-Wizard/app/src/PSCmdlets.cs @@ -176,7 +176,7 @@ internal static SiPolicy CreateScannedPolicyFromPS(PolicyCustomRules customRule, deny = "True"; } - string newPolicyScriptCmd = $"-NoProfile -ExecutionPolicy Bypass -File \"{ps1File}\" -ScanPath \"{scanPath}\" " + + string newPolicyScriptCmd = $"-NoProfile -NoLogo -NonInteractive -ExecutionPolicy Bypass -File \"{ps1File}\" -ScanPath \"{scanPath}\" " + $"-PolicyPath \"{policyPath}\" -Level {level} -Fallback {fallbacks} -PathsToOmit \"{pathsToOmit}\"" + $" -Deny {deny} -UserPEs {userPEs}"; @@ -201,10 +201,10 @@ internal static SiPolicy CreateScannedPolicyFromPS(PolicyCustomRules customRule, try { process.Start(); - process.WaitForExit(); - + // Read streams asynchronously to avoid deadlocks and allow PS to flush output string output = process.StandardOutput.ReadToEnd(); string error = process.StandardError.ReadToEnd(); + process.WaitForExit(); if (!string.IsNullOrEmpty(error)) { From 0e2e569a17100b4921b28d4cf5f7fb015f05d4a0 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Wed, 6 May 2026 16:33:40 -0400 Subject: [PATCH 5/6] Updated Scan Rule UI and making Hash Default fallback --- .../src/CustomRuleConditionsPanel.Designer.cs | 19 ++-- .../app/src/CustomRuleConditionsPanel.cs | 98 ++++++++++++++++++- WDAC-Policy-Wizard/app/src/PSCmdlets.cs | 7 +- 3 files changed, 112 insertions(+), 12 deletions(-) diff --git a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs index e65d9490..9e3a9c24 100644 --- a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs @@ -392,15 +392,16 @@ private void InitializeComponent() // // label12 // - label12.AutoSize = true; + label12.AutoSize = false; label12.Font = new System.Drawing.Font("Tahoma", 7F); label12.ForeColor = System.Drawing.Color.Black; - label12.Location = new System.Drawing.Point(6, 92); + label12.Location = new System.Drawing.Point(6, 88); label12.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); label12.Name = "label12"; - label12.Size = new System.Drawing.Size(469, 14); + label12.Size = new System.Drawing.Size(530, 32); + label12.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; label12.TabIndex = 115; - label12.Text = "Multiple levels can be selected. You can drag the levels to specify the fallback order."; + label12.Text = "First checked item = Level. Remaining checked items = Fallback.\r\nHash is always included as the final fallback for unsigned files."; // // label11 // @@ -439,15 +440,17 @@ private void InitializeComponent() checkedListBoxRuleLevels.Font = new System.Drawing.Font("Tahoma", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0); checkedListBoxRuleLevels.FormattingEnabled = true; checkedListBoxRuleLevels.Items.AddRange(new object[] { "PcaCertificate", "Publisher", "SignedVersion", "FilePublisher", "FileName", "FilePath", "Hash" }); - checkedListBoxRuleLevels.Location = new System.Drawing.Point(8, 121); + checkedListBoxRuleLevels.Location = new System.Drawing.Point(8, 125); checkedListBoxRuleLevels.MultiColumn = true; - checkedListBoxRuleLevels.ColumnWidth = 160; + checkedListBoxRuleLevels.ColumnWidth = 200; checkedListBoxRuleLevels.Name = "checkedListBoxRuleLevels"; - checkedListBoxRuleLevels.Size = new System.Drawing.Size(500, 100); + checkedListBoxRuleLevels.Size = new System.Drawing.Size(530, 100); + checkedListBoxRuleLevels.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; checkedListBoxRuleLevels.TabIndex = 111; checkedListBoxRuleLevels.DragDrop += RuleLevelsList_DragDropDone; checkedListBoxRuleLevels.DragOver += RuleLevelsList_DragInProgress; checkedListBoxRuleLevels.MouseDown += RuleLevelsList_MouseDown; + checkedListBoxRuleLevels.ItemCheck += RuleLevelsList_ItemCheck; // // label13 // @@ -1237,7 +1240,7 @@ private void InitializeComponent() AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; BackColor = System.Drawing.Color.White; - ClientSize = new System.Drawing.Size(766, 828); + ClientSize = new System.Drawing.Size(900, 828); Controls.Add(controlHighlight_Panel); Controls.Add(button_AddException); Controls.Add(button_Back); diff --git a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs index 1a92ff92..db02b92d 100644 --- a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs +++ b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs @@ -297,7 +297,8 @@ private void Button_CreateRule_Click(object sender, EventArgs e) { for (int i = 0; i < this.checkedListBoxRuleLevels.CheckedItems.Count; i++) { - this.PolicyCustomRule.Scan.Levels.Add(this.checkedListBoxRuleLevels.CheckedItems[i].ToString()); + string rawLevel = GetRawLevelName(this.checkedListBoxRuleLevels.CheckedItems[i].ToString()); + this.PolicyCustomRule.Scan.Levels.Add(rawLevel); } } @@ -2792,12 +2793,16 @@ private void RuleLevelsList_DragDropDone(object sender, DragEventArgs e) Point point = checkedListBoxRuleLevels.PointToClient(new Point(e.X, e.Y)); int index = this.checkedListBoxRuleLevels.IndexFromPoint(point); if (index < 0) index = this.checkedListBoxRuleLevels.Items.Count - 1; - bool isChecked = checkedListBoxRuleLevels.GetItemChecked(index); object data = checkedListBoxRuleLevels.SelectedItem; + int oldIndex = checkedListBoxRuleLevels.Items.IndexOf(data); + bool wasChecked = checkedListBoxRuleLevels.GetItemChecked(oldIndex); + this.checkedListBoxRuleLevels.Items.Remove(data); this.checkedListBoxRuleLevels.Items.Insert(index, data); - this.checkedListBoxRuleLevels.SetItemChecked(index, isChecked); + this.checkedListBoxRuleLevels.SetItemChecked(index, wasChecked); + + UpdateRuleLevelLabels(); } private void RuleLevelsList_DragInProgress(object sender, DragEventArgs e) @@ -2805,6 +2810,93 @@ private void RuleLevelsList_DragInProgress(object sender, DragEventArgs e) e.Effect = DragDropEffects.Move; } + /// + /// Updates the rule level items with (Level) and (Fallback) annotations, + /// ensures Hash is always checked as a fallback, and moves unchecked items to the back. + /// + private void UpdateRuleLevelLabels() + { + // Temporarily suppress events to avoid re-entrancy + this.checkedListBoxRuleLevels.ItemCheck -= RuleLevelsList_ItemCheck; + + // Collect items with their checked state + var checkedItems = new List(); + var uncheckedItems = new List(); + + for (int i = 0; i < checkedListBoxRuleLevels.Items.Count; i++) + { + string rawName = GetRawLevelName(checkedListBoxRuleLevels.Items[i].ToString()); + if (checkedListBoxRuleLevels.GetItemChecked(i)) + checkedItems.Add(rawName); + else + uncheckedItems.Add(rawName); + } + + // Ensure Hash is in checked list (unless it would be the only/first item making it Level) + if (!checkedItems.Contains("Hash")) + { + uncheckedItems.Remove("Hash"); + checkedItems.Add("Hash"); + } + + // Rebuild the list: checked items first, then unchecked + checkedListBoxRuleLevels.Items.Clear(); + + for (int i = 0; i < checkedItems.Count; i++) + { + string label = i == 0 + ? checkedItems[i] + " (Level)" + : checkedItems[i] + " (Fallback)"; + checkedListBoxRuleLevels.Items.Add(label); + checkedListBoxRuleLevels.SetItemChecked(checkedListBoxRuleLevels.Items.Count - 1, true); + } + + for (int i = 0; i < uncheckedItems.Count; i++) + { + checkedListBoxRuleLevels.Items.Add(uncheckedItems[i]); + } + + this.checkedListBoxRuleLevels.ItemCheck += RuleLevelsList_ItemCheck; + } + + /// + /// Strips the (Level) / (Fallback) annotation from item text to get the raw level name. + /// + private static string GetRawLevelName(string itemText) + { + return itemText.Replace(" (Level)", "").Replace(" (Fallback)", "").Trim(); + } + + /// + /// Handles item check state changes to update labels. + /// + private void RuleLevelsList_ItemCheck(object sender, ItemCheckEventArgs e) + { + // Prevent unchecking Hash when it's the forced fallback + string rawName = GetRawLevelName(checkedListBoxRuleLevels.Items[e.Index].ToString()); + if (rawName == "Hash" && e.NewValue == CheckState.Unchecked) + { + // Only allow unchecking if Hash is the Level (first checked item) + bool isLevel = true; + for (int i = 0; i < e.Index; i++) + { + if (checkedListBoxRuleLevels.GetItemChecked(i)) + { + isLevel = false; + break; + } + } + if (!isLevel) + { + e.NewValue = CheckState.Checked; + return; + } + } + + // Use BeginInvoke to update labels after the check state has actually changed + this.BeginInvoke(new Action(() => UpdateRuleLevelLabels())); + } + /// /// Opens the AppIdTagging Microsoft Learn doc diff --git a/WDAC-Policy-Wizard/app/src/PSCmdlets.cs b/WDAC-Policy-Wizard/app/src/PSCmdlets.cs index 867cdfbc..de2eb9ce 100644 --- a/WDAC-Policy-Wizard/app/src/PSCmdlets.cs +++ b/WDAC-Policy-Wizard/app/src/PSCmdlets.cs @@ -155,7 +155,12 @@ internal static SiPolicy CreateScannedPolicyFromPS(PolicyCustomRules customRule, // Add fallback levels, if applicable if (customRule.Scan.Levels.Count > 1) { - fallbacks = string.Join(",",customRule.Scan.Levels.Skip(1)); + fallbacks = string.Join(",", customRule.Scan.Levels.Skip(1)); + // Always ensure Hash is the final fallback for unsigned files + if (!fallbacks.Contains("Hash", StringComparison.OrdinalIgnoreCase)) + { + fallbacks += ",Hash"; + } } // Add paths to omit, if applicable From e02d47bfbad40b0562bc198a8cbb227bd208a11c Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Mon, 11 May 2026 15:11:32 -0400 Subject: [PATCH 6/6] Change omit line to > 0, not > 1. Breaking subfolders omit. -OmitPaths and -Fallback passed as single string instead of array. Fix: The script now splits the comma-separated values into proper arrays ($FallbackArray, $OmitArray) and uses splatting (@omitSplat) to conditionally pass -OmitPaths only when paths are specified. Applied to both app/Scripts/ and app/MSIX/ copies --- .../app/MSIX/CreateScannedPolicy.ps1 | 17 +++++++++++++---- .../app/Scripts/CreateScannedPolicy.ps1 | 16 ++++++++++++---- .../app/src/CustomRuleConditionsPanel.cs | 2 +- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/WDAC-Policy-Wizard/app/MSIX/CreateScannedPolicy.ps1 b/WDAC-Policy-Wizard/app/MSIX/CreateScannedPolicy.ps1 index d35fb73a..a8eb792f 100644 --- a/WDAC-Policy-Wizard/app/MSIX/CreateScannedPolicy.ps1 +++ b/WDAC-Policy-Wizard/app/MSIX/CreateScannedPolicy.ps1 @@ -15,29 +15,38 @@ param ( [string]$UserPEs ) +# Convert comma-separated strings to arrays for PowerShell cmdlet parameters +$FallbackArray = $Fallback -split ',' +$OmitArray = if ($PathsToOmit -ne '') { $PathsToOmit -split ',' } else { @() } + # Run New-CIPolicy -Scan to generate a policy from a directory # Use -WarningAction SilentlyContinue to suppress the "An item with the same key has already been added." warning # which previously required running the command twice as a workaround. + +# Build optional splat for OmitPaths +$omitSplat = @{} +if ($OmitArray.Count -gt 0) { $omitSplat['OmitPaths'] = $OmitArray } + if($Deny -eq "False") { if($UserPEs -eq "True") { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs -WarningAction SilentlyContinue + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $FallbackArray @omitSplat -UserPEs -WarningAction SilentlyContinue } else { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -WarningAction SilentlyContinue + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $FallbackArray @omitSplat -WarningAction SilentlyContinue } } else { if($UserPEs -eq "True") { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs -Deny -WarningAction SilentlyContinue + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $FallbackArray @omitSplat -UserPEs -Deny -WarningAction SilentlyContinue } else { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -Deny -WarningAction SilentlyContinue + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $FallbackArray @omitSplat -Deny -WarningAction SilentlyContinue } } # SIG # Begin signature block diff --git a/WDAC-Policy-Wizard/app/Scripts/CreateScannedPolicy.ps1 b/WDAC-Policy-Wizard/app/Scripts/CreateScannedPolicy.ps1 index d35fb73a..f27b2a50 100644 --- a/WDAC-Policy-Wizard/app/Scripts/CreateScannedPolicy.ps1 +++ b/WDAC-Policy-Wizard/app/Scripts/CreateScannedPolicy.ps1 @@ -15,29 +15,37 @@ param ( [string]$UserPEs ) +# Convert comma-separated strings to arrays for PowerShell cmdlet parameters +$FallbackArray = $Fallback -split ',' +$OmitArray = if ($PathsToOmit -ne '') { $PathsToOmit -split ',' } else { @() } + # Run New-CIPolicy -Scan to generate a policy from a directory # Use -WarningAction SilentlyContinue to suppress the "An item with the same key has already been added." warning # which previously required running the command twice as a workaround. +# Build optional splat for OmitPaths +$omitSplat = @{} +if ($OmitArray.Count -gt 0) { $omitSplat['OmitPaths'] = $OmitArray } + if($Deny -eq "False") { if($UserPEs -eq "True") { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs -WarningAction SilentlyContinue + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $FallbackArray @omitSplat -UserPEs -WarningAction SilentlyContinue } else { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -WarningAction SilentlyContinue + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $FallbackArray @omitSplat -WarningAction SilentlyContinue } } else { if($UserPEs -eq "True") { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -UserPEs -Deny -WarningAction SilentlyContinue + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $FallbackArray @omitSplat -UserPEs -Deny -WarningAction SilentlyContinue } else { - New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $Fallback -OmitPaths $PathsToOmit -Deny -WarningAction SilentlyContinue + New-CIPolicy -ScanPath $ScanPath -Level $Level -FilePath $PolicyPath -Fallback $FallbackArray @omitSplat -Deny -WarningAction SilentlyContinue } } # SIG # Begin signature block diff --git a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs index db02b92d..fc9d032a 100644 --- a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs +++ b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs @@ -303,7 +303,7 @@ private void Button_CreateRule_Click(object sender, EventArgs e) } // Check for Omit Scan Paths - if (this.checkedListBoxOmitPaths.CheckedItems.Count > 1) + if (this.checkedListBoxOmitPaths.CheckedItems.Count > 0) { // Using for loop to avoid System.InvalidOperationException despite list not changing for (int i = 0; i < this.checkedListBoxOmitPaths.CheckedItems.Count; i++)