From 162dd8803751610b1c8cedab6bf3abca2dc2e8d8 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Mon, 4 May 2026 17:44:43 -0400 Subject: [PATCH 01/16] =?UTF-8?q?New=20Files=20=E2=80=A2=20FolderHashScanF?= =?UTF-8?q?orm.cs=20=E2=80=94=20New=20dialog=20for=20folder-based=20hash?= =?UTF-8?q?=20file=20scanning=20with=20browse,=20subfolder=20toggle,=20fil?= =?UTF-8?q?e=20checklist,=20and=20hash=20type=20selection=20Modified=20Fil?= =?UTF-8?q?es=20CustomRuleConditionsPanel.cs=20=E2=80=A2=20Added=20hash=20?= =?UTF-8?q?mode=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 02/16] 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 03/16] 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 04/16] =?UTF-8?q?PSCmdlets.cs=20=E2=80=A2=20Added=20-NoLog?= =?UTF-8?q?o=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 d8107354b5ec3ad342c5e814e1d5235c3564bff8 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Thu, 7 May 2026 11:19:42 -0400 Subject: [PATCH 05/16] Revert "File Scanner UI updates, smoother controls" This reverts commit e7f6333fe256c277d9c3ea709325292e495d92e8. --- .../src/CustomRuleConditionsPanel.Designer.cs | 24 +++------ .../app/src/CustomRuleConditionsPanel.cs | 51 +------------------ 2 files changed, 7 insertions(+), 68 deletions(-) diff --git a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs index e65d9490..9da2f1a8 100644 --- a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs @@ -154,7 +154,6 @@ 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 @@ -372,22 +371,17 @@ 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(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.Size = new System.Drawing.Size(537, 359); panelFolderScanConditions.TabIndex = 126; panelFolderScanConditions.Visible = false; // // checkedListBoxOmitPaths // - checkedListBoxOmitPaths.Font = new System.Drawing.Font("Tahoma", 8.5F); + checkedListBoxOmitPaths.Font = new System.Drawing.Font("Tahoma", 8F); checkedListBoxOmitPaths.FormattingEnabled = true; - checkedListBoxOmitPaths.CheckOnClick = true; - checkedListBoxOmitPaths.HorizontalScrollbar = true; - checkedListBoxOmitPaths.IntegralHeight = false; - checkedListBoxOmitPaths.Location = new System.Drawing.Point(8, 250); + checkedListBoxOmitPaths.Location = new System.Drawing.Point(8, 260); checkedListBoxOmitPaths.Name = "checkedListBoxOmitPaths"; - 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.Size = new System.Drawing.Size(341, 80); checkedListBoxOmitPaths.TabIndex = 116; // // label12 @@ -407,7 +401,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, 228); + label11.Location = new System.Drawing.Point(5, 232); label11.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); label11.Name = "label11"; label11.Size = new System.Drawing.Size(231, 18); @@ -435,15 +429,13 @@ 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(500, 100); + checkedListBoxRuleLevels.Size = new System.Drawing.Size(341, 88); checkedListBoxRuleLevels.TabIndex = 111; checkedListBoxRuleLevels.DragDrop += RuleLevelsList_DragDropDone; checkedListBoxRuleLevels.DragOver += RuleLevelsList_DragInProgress; @@ -1079,7 +1071,6 @@ 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; @@ -1092,7 +1083,6 @@ 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; @@ -1205,7 +1195,6 @@ 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; @@ -1220,7 +1209,6 @@ 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 1a92ff92..579b9b3f 100644 --- a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs +++ b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs @@ -60,44 +60,6 @@ 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); } /// @@ -855,7 +817,6 @@ 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": @@ -2773,17 +2734,7 @@ private void LabelFolderScanLearnMore_Click(object sender, EventArgs e) /// private void RuleLevelsList_MouseDown(object sender, MouseEventArgs e) { - 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; - + 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 this.checkedListBoxRuleLevels.DoDragDrop(this.checkedListBoxRuleLevels.SelectedItem, DragDropEffects.Move); } From e904f1402bc6547170c373d7b73f1386096d1727 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Sun, 31 May 2026 10:13:43 -0400 Subject: [PATCH 06/16] =?UTF-8?q?=E2=80=A2=20Removed=20the=20forced=20rule?= =?UTF-8?q?TypeComboBox.SelectedIndex=20=3D=200=20from=20ResetCustomRulesP?= =?UTF-8?q?anel()=20so=20the=20dropdown=20no=20longer=20snaps=20back=20to?= =?UTF-8?q?=20Publisher=20on=20every=20row=20click.=20=E2=80=A2=20Updated?= =?UTF-8?q?=20RowSelectionChanged(object,=20EventArgs)=20and=20EventRowCli?= =?UTF-8?q?ck(object,=20DataGridViewCellEventArgs)=20to=20repopulate=20the?= =?UTF-8?q?=20rule=20panel=20based=20on=20the=20currently=20selected=20rul?= =?UTF-8?q?e=20type=20instead=20of=20hard-coding=20SetPublisherPanel(...).?= =?UTF-8?q?=20=E2=80=A2=20Extracted=20the=20panel-population=20logic=20int?= =?UTF-8?q?o=20a=20shared=20RefreshRulePanelForSelectedRow()=20method,=20r?= =?UTF-8?q?eused=20by=20both=20the=20row=20handlers=20and=20RuleTypeChange?= =?UTF-8?q?d(object,=20EventArgs).=20=E2=80=A2=20Set=20SelectedRow=20befor?= =?UTF-8?q?e=20refreshing=20the=20UI=20and=20added=20a=20bounds=20guard.?= =?UTF-8?q?=20=E2=80=A2=20Reordered=20the=20header-click=20(-1)=20sort=20c?= =?UTF-8?q?heck=20in=20EventRowClick(object,=20DataGridViewCellEventArgs)?= =?UTF-8?q?=20so=20sorting=20still=20works.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/src/EditWorkflow.Designer.cs | 6 +- .../src/EventLogRuleConfiguration.Designer.cs | 23 ++++--- .../app/src/EventLogRuleConfiguration.cs | 61 ++++++++++++------- .../app/src/Exceptions_Control.Designer.cs | 3 +- .../app/src/Exceptions_Control.cs | 4 ++ .../app/src/GridLayoutHelper.cs | 55 +++++++++++++++++ .../app/src/MainForm.Designer.cs | 8 ++- WDAC-Policy-Wizard/app/src/MainForm.cs | 39 ++++++++++-- .../app/src/PolicyMerge_Control.Designer.cs | 1 + .../app/src/PolicyMerge_Control.cs | 4 ++ .../app/src/SigningRules_Control.Designer.cs | 21 ++++--- .../app/src/SigningRules_Control.cs | 8 +++ 12 files changed, 187 insertions(+), 46 deletions(-) create mode 100644 WDAC-Policy-Wizard/app/src/GridLayoutHelper.cs diff --git a/WDAC-Policy-Wizard/app/src/EditWorkflow.Designer.cs b/WDAC-Policy-Wizard/app/src/EditWorkflow.Designer.cs index 17e2a9f1..14757d5f 100644 --- a/WDAC-Policy-Wizard/app/src/EditWorkflow.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/EditWorkflow.Designer.cs @@ -237,7 +237,7 @@ private void InitializeComponent() // buttonParseEventLog.Location = new System.Drawing.Point(343, 72); buttonParseEventLog.Name = "buttonParseEventLog"; - buttonParseEventLog.Size = new System.Drawing.Size(133, 27); + buttonParseEventLog.Size = new System.Drawing.Size(165, 32); buttonParseEventLog.TabIndex = 112; buttonParseEventLog.Text = "Parse Event Logs"; buttonParseEventLog.UseVisualStyleBackColor = true; @@ -248,7 +248,7 @@ private void InitializeComponent() buttonParseLogFile.Font = new System.Drawing.Font("Microsoft Sans Serif", 8F); buttonParseLogFile.Location = new System.Drawing.Point(343, 174); buttonParseLogFile.Name = "buttonParseLogFile"; - buttonParseLogFile.Size = new System.Drawing.Size(133, 27); + buttonParseLogFile.Size = new System.Drawing.Size(165, 32); buttonParseLogFile.TabIndex = 113; buttonParseLogFile.Text = "Parse Log File(s)"; buttonParseLogFile.UseVisualStyleBackColor = true; @@ -384,7 +384,7 @@ private void InitializeComponent() buttonParseMDELog.Font = new System.Drawing.Font("Microsoft Sans Serif", 8F); buttonParseMDELog.Location = new System.Drawing.Point(342, 273); buttonParseMDELog.Name = "buttonParseMDELog"; - buttonParseMDELog.Size = new System.Drawing.Size(133, 27); + buttonParseMDELog.Size = new System.Drawing.Size(165, 32); buttonParseMDELog.TabIndex = 124; buttonParseMDELog.Text = "Parse Log File(s)"; buttonParseMDELog.UseVisualStyleBackColor = true; diff --git a/WDAC-Policy-Wizard/app/src/EventLogRuleConfiguration.Designer.cs b/WDAC-Policy-Wizard/app/src/EventLogRuleConfiguration.Designer.cs index f8a0c3bb..093181ff 100644 --- a/WDAC-Policy-Wizard/app/src/EventLogRuleConfiguration.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/EventLogRuleConfiguration.Designer.cs @@ -86,6 +86,7 @@ private void InitializeComponent() addButton.Location = new System.Drawing.Point(442, 412); addButton.Name = "addButton"; addButton.Size = new System.Drawing.Size(130, 30); + addButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; addButton.TabIndex = 0; addButton.Text = "+ Add Allow Rule"; addButton.UseVisualStyleBackColor = true; @@ -106,6 +107,7 @@ private void InitializeComponent() publisherRulePanel.Location = new System.Drawing.Point(167, 447); publisherRulePanel.Name = "publisherRulePanel"; publisherRulePanel.Size = new System.Drawing.Size(716, 190); + publisherRulePanel.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; publisherRulePanel.TabIndex = 9; // // productTextBox @@ -227,6 +229,7 @@ private void InitializeComponent() label3.Location = new System.Drawing.Point(163, 415); label3.Name = "label3"; label3.Size = new System.Drawing.Size(90, 21); + label3.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; label3.TabIndex = 8; label3.Text = "Rule Type:"; // @@ -238,6 +241,7 @@ private void InitializeComponent() ruleTypeComboBox.Location = new System.Drawing.Point(259, 412); ruleTypeComboBox.Name = "ruleTypeComboBox"; ruleTypeComboBox.Size = new System.Drawing.Size(163, 29); + ruleTypeComboBox.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; ruleTypeComboBox.TabIndex = 3; ruleTypeComboBox.SelectedIndexChanged += RuleTypeChanged; // @@ -256,6 +260,7 @@ private void InitializeComponent() fileAttributeRulePanel.Location = new System.Drawing.Point(906, 447); fileAttributeRulePanel.Name = "fileAttributeRulePanel"; fileAttributeRulePanel.Size = new System.Drawing.Size(551, 190); + fileAttributeRulePanel.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; fileAttributeRulePanel.TabIndex = 21; fileAttributeRulePanel.Visible = false; // @@ -373,7 +378,7 @@ private void InitializeComponent() // eventsDataGridView.AllowUserToDeleteRows = false; eventsDataGridView.AllowUserToResizeRows = false; - eventsDataGridView.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.AllCells; + eventsDataGridView.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.None; eventsDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; eventsDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { addedColumn, eventIdColumn, filenameColumn, productColumn, policyColumn, publisherColumn, issuerTbsHashColumn }); eventsDataGridView.EnableHeadersVisualStyles = false; @@ -387,6 +392,7 @@ private void InitializeComponent() eventsDataGridView.RowTemplate.Height = 24; eventsDataGridView.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect; eventsDataGridView.Size = new System.Drawing.Size(896, 287); + eventsDataGridView.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; eventsDataGridView.TabIndex = 4; eventsDataGridView.VirtualMode = true; eventsDataGridView.CellClick += EventRowClick; @@ -395,7 +401,7 @@ private void InitializeComponent() // // addedColumn // - addedColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.AllCells; + addedColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None; addedColumn.HeaderText = "Added To Policy"; addedColumn.MinimumWidth = 100; addedColumn.Name = "addedColumn"; @@ -405,7 +411,7 @@ private void InitializeComponent() // // eventIdColumn // - eventIdColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.AllCells; + eventIdColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None; eventIdColumn.HeaderText = "Event Id"; eventIdColumn.MinimumWidth = 6; eventIdColumn.Name = "eventIdColumn"; @@ -415,7 +421,7 @@ private void InitializeComponent() // // filenameColumn // - filenameColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.AllCells; + filenameColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None; filenameColumn.HeaderText = "Filename"; filenameColumn.MinimumWidth = 6; filenameColumn.Name = "filenameColumn"; @@ -425,7 +431,7 @@ private void InitializeComponent() // // productColumn // - productColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.AllCells; + productColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None; productColumn.HeaderText = "Product"; productColumn.MinimumWidth = 6; productColumn.Name = "productColumn"; @@ -435,7 +441,7 @@ private void InitializeComponent() // // policyColumn // - policyColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.AllCells; + policyColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None; policyColumn.HeaderText = "Policy Name"; policyColumn.MinimumWidth = 6; policyColumn.Name = "policyColumn"; @@ -445,7 +451,7 @@ private void InitializeComponent() // // publisherColumn // - publisherColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.AllCells; + publisherColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None; publisherColumn.HeaderText = "Publisher"; publisherColumn.MinimumWidth = 6; publisherColumn.Name = "publisherColumn"; @@ -482,6 +488,7 @@ private void InitializeComponent() hashRulePanel.Location = new System.Drawing.Point(765, 664); hashRulePanel.Name = "hashRulePanel"; hashRulePanel.Size = new System.Drawing.Size(603, 93); + hashRulePanel.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; hashRulePanel.TabIndex = 22; hashRulePanel.Visible = false; // @@ -534,6 +541,7 @@ private void InitializeComponent() filePathRulePanel.Location = new System.Drawing.Point(16, 664); filePathRulePanel.Name = "filePathRulePanel"; filePathRulePanel.Size = new System.Drawing.Size(743, 93); + filePathRulePanel.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; filePathRulePanel.TabIndex = 22; filePathRulePanel.Visible = false; // @@ -595,6 +603,7 @@ private void InitializeComponent() // AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; + AutoScroll = true; BackColor = System.Drawing.Color.White; Controls.Add(publisherRulePanel); Controls.Add(label3); diff --git a/WDAC-Policy-Wizard/app/src/EventLogRuleConfiguration.cs b/WDAC-Policy-Wizard/app/src/EventLogRuleConfiguration.cs index 50d2995d..1f4d9f44 100644 --- a/WDAC-Policy-Wizard/app/src/EventLogRuleConfiguration.cs +++ b/WDAC-Policy-Wizard/app/src/EventLogRuleConfiguration.cs @@ -94,6 +94,10 @@ private void DisplayEvents() this.DisplayObjects.Add(dpObject); this.eventsDataGridView.RowCount += 1; } + + // Size each column to fit its text. Columns remain user-resizable afterwards + // since AutoSizeColumnsMode stays None. + GridLayoutHelper.AutoFitColumns(this.eventsDataGridView); } /// @@ -188,15 +192,14 @@ private void RowSelectionChanged(object sender, EventArgs e) return; } - // Set the UI - ResetCustomRulesPanel(); - SetPublisherPanel(this.CiEvents[selectedRow].SignerInfo.IssuerName, - this.CiEvents[selectedRow].SignerInfo.PublisherName, - this.CiEvents[selectedRow].OriginalFilename, - this.CiEvents[selectedRow].FileVersion, - this.CiEvents[selectedRow].ProductName); - + // Update the selected row before refreshing the UI so that the + // rule type panel reflects the newly selected event. this.SelectedRow = selectedRow; + + // Set the UI. Keep the user's current rule type selection and + // repopulate the matching panel for the newly selected row. + ResetCustomRulesPanel(); + RefreshRulePanelForSelectedRow(); } /// @@ -206,14 +209,7 @@ private void RowSelectionChanged(object sender, EventArgs e) /// private void EventRowClick(object sender, DataGridViewCellEventArgs e) { - // Set the UI - ResetCustomRulesPanel(); - int selectedRow = e.RowIndex; - if(selectedRow >= this.CiEvents.Count) - { - return; - } // Header selected, sort table if(selectedRow == -1) @@ -221,14 +217,19 @@ private void EventRowClick(object sender, DataGridViewCellEventArgs e) SortDataGrid(sender, e); return; } - - SetPublisherPanel(this.CiEvents[selectedRow].SignerInfo.IssuerName, - this.CiEvents[selectedRow].SignerInfo.PublisherName, - this.CiEvents[selectedRow].OriginalFilename, - this.CiEvents[selectedRow].FileVersion, - this.CiEvents[selectedRow].ProductName); + if(selectedRow >= this.CiEvents.Count) + { + return; + } + + // Update the selected row before refreshing the UI this.SelectedRow = selectedRow; + + // Set the UI. Keep the user's current rule type selection and + // repopulate the matching panel for the newly selected row. + ResetCustomRulesPanel(); + RefreshRulePanelForSelectedRow(); } /// @@ -410,8 +411,8 @@ private void ResetCustomRulesPanel() this.versionTextBox.Clear(); this.productTextBox.Clear(); - // Dropdown - this.ruleTypeComboBox.SelectedIndex = 0; + // NOTE: The rule type dropdown is intentionally left unchanged so the + // user's selection persists as they navigate between rows. } @@ -725,6 +726,20 @@ private void RuleTypeChanged(object sender, EventArgs e) // Path // File Attributes // File Hash + RefreshRulePanelForSelectedRow(); + } + + /// + /// Populates the rule details panel for the currently selected row based + /// on the rule type currently chosen in the dropdown. The dropdown + /// selection is intentionally preserved as the user navigates rows. + /// + private void RefreshRulePanelForSelectedRow() + { + if (this.SelectedRow < 0 || this.SelectedRow >= this.CiEvents.Count) + { + return; + } HideAllPanels(); diff --git a/WDAC-Policy-Wizard/app/src/Exceptions_Control.Designer.cs b/WDAC-Policy-Wizard/app/src/Exceptions_Control.Designer.cs index be107674..92bf979c 100644 --- a/WDAC-Policy-Wizard/app/src/Exceptions_Control.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/Exceptions_Control.Designer.cs @@ -290,7 +290,7 @@ private void InitializeComponent() // dataGridView_Exceptions // this.dataGridView_Exceptions.AllowUserToDeleteRows = false; - this.dataGridView_Exceptions.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.AllCells; + this.dataGridView_Exceptions.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.None; this.dataGridView_Exceptions.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.dataGridView_Exceptions.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { this.column_Action, @@ -420,6 +420,7 @@ private void InitializeComponent() // this.AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; + this.AutoScroll = true; this.BackColor = System.Drawing.Color.White; this.Controls.Add(this.panel_ExceptionRule); this.Name = "Exceptions_Control"; diff --git a/WDAC-Policy-Wizard/app/src/Exceptions_Control.cs b/WDAC-Policy-Wizard/app/src/Exceptions_Control.cs index 82872044..108b7630 100644 --- a/WDAC-Policy-Wizard/app/src/Exceptions_Control.cs +++ b/WDAC-Policy-Wizard/app/src/Exceptions_Control.cs @@ -469,6 +469,10 @@ public void AddException() this.displayObjects.Add(displayObject); this.dataGridView_Exceptions.RowCount += 1; + // Size each column to fit its text. Columns remain user-resizable afterwards + // since AutoSizeColumnsMode stays None. + GridLayoutHelper.AutoFitColumns(this.dataGridView_Exceptions); + // Scroll to bottom to see new rule added to list this.dataGridView_Exceptions.FirstDisplayedScrollingRowIndex = this.dataGridView_Exceptions.RowCount - 1; this.ExceptionRule = new PolicyCustomRules(); diff --git a/WDAC-Policy-Wizard/app/src/GridLayoutHelper.cs b/WDAC-Policy-Wizard/app/src/GridLayoutHelper.cs new file mode 100644 index 00000000..266d1b9f --- /dev/null +++ b/WDAC-Policy-Wizard/app/src/GridLayoutHelper.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.Windows.Forms; + +namespace WDAC_Wizard +{ + /// + /// Helper utilities for DataGridView layout behavior shared across the wizard pages. + /// + internal static class GridLayoutHelper + { + /// + /// Sizes each column to fit its content. The columns remain user-resizable afterwards + /// because the grid's AutoSizeColumnsMode is left as None. + /// The resize is deferred until the grid has a created handle so that text measurement + /// works correctly (calling it too early, e.g. during Load, is a silent no-op). + /// + /// The DataGridView to resize. + public static void AutoFitColumns(DataGridView grid) + { + if (grid == null) + { + return; + } + + void Resize() + { + if (grid.IsDisposed || grid.ColumnCount == 0) + { + return; + } + + grid.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells); + } + + if (grid.IsHandleCreated) + { + // Defer to the message loop so it runs after the current layout pass completes. + grid.BeginInvoke((MethodInvoker)Resize); + } + else + { + // Handle not created yet (e.g. populated during Load). Resize once it is. + void Handler(object sender, System.EventArgs e) + { + grid.HandleCreated -= Handler; + grid.BeginInvoke((MethodInvoker)Resize); + } + + grid.HandleCreated += Handler; + } + } + } +} diff --git a/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs b/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs index 9e654d47..c70f6748 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs @@ -101,6 +101,7 @@ private void InitializeComponent() label_Info.Tag = "IgnoreDarkMode"; label_Info.Text = "Info Text"; label_Info.Visible = false; + label_Info.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; // // backgroundWorker1 // @@ -164,6 +165,7 @@ private void InitializeComponent() control_Panel.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); control_Panel.Name = "control_Panel"; control_Panel.Size = new System.Drawing.Size(150, 700); + control_Panel.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; control_Panel.TabIndex = 30; // // workflow_Label @@ -334,6 +336,7 @@ private void InitializeComponent() settings_Button.Name = "settings_Button"; settings_Button.Size = new System.Drawing.Size(129, 45); settings_Button.TabIndex = 31; + settings_Button.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; settings_Button.Text = " Settings"; settings_Button.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; settings_Button.UseVisualStyleBackColor = false; @@ -346,6 +349,7 @@ private void InitializeComponent() button_Next.Name = "button_Next"; button_Next.Size = new System.Drawing.Size(93, 33); button_Next.TabIndex = 31; + button_Next.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; button_Next.Text = "Next"; button_Next.UseVisualStyleBackColor = true; button_Next.Visible = false; @@ -416,10 +420,12 @@ private void InitializeComponent() Controls.Add(button_Edit); Controls.Add(label_Info); Controls.Add(button_New); - FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable; HelpButton = true; Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon"); Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + MaximizeBox = true; + MinimumSize = new System.Drawing.Size(900, 600); Name = "MainWindow"; StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; Text = "App Control Policy Wizard"; diff --git a/WDAC-Policy-Wizard/app/src/MainForm.cs b/WDAC-Policy-Wizard/app/src/MainForm.cs index 6fe50737..81eb63a6 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.cs @@ -83,9 +83,37 @@ public MainWindow() Helper.LicenseCheck(); } - // ############### - // HEADER CONTROLS - // ############### + // ##################### + // DYNAMIC LAYOUT SUPPORT + // ##################### + + /// + /// Hosts a wizard page (UserControl) so it fills the MainWindow client area and tracks the + /// window as it is resized or maximized. Each page now manages its own responsive layout + /// internally (via WinForms layout containers and Dock/Anchor), so the host only needs to + /// dock the page to fill the available space. + /// + private void RegisterPage(Control page) + { + if (page == null) + { + return; + } + + page.Dock = DockStyle.Fill; + } + + /// + /// Automatically docks any wizard page (UserControl) added to the MainWindow so its content + /// expands and contracts with the host window. + /// + private void MainWindow_ControlAdded(object sender, ControlEventArgs e) + { + if (e.Control is UserControl) + { + RegisterPage(e.Control); + } + } /// /// New policy button selected: User can select either base or suppl policy, @@ -2239,7 +2267,10 @@ private void MainWindow_Load(object sender, EventArgs e) SetControlPanelUI(); // Set UI for the 'Next' Button - SetNextButtonUI(); + SetNextButtonUI(); + + // Dock wizard pages so their content fills and tracks the window as it is resized + this.ControlAdded += MainWindow_ControlAdded; } /// diff --git a/WDAC-Policy-Wizard/app/src/PolicyMerge_Control.Designer.cs b/WDAC-Policy-Wizard/app/src/PolicyMerge_Control.Designer.cs index 30066395..e008ae49 100644 --- a/WDAC-Policy-Wizard/app/src/PolicyMerge_Control.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/PolicyMerge_Control.Designer.cs @@ -170,6 +170,7 @@ private void InitializeComponent() // AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; + AutoScroll = true; Controls.Add(label_Error); Controls.Add(button_RemovePolicy); Controls.Add(button_AddPolicy); diff --git a/WDAC-Policy-Wizard/app/src/PolicyMerge_Control.cs b/WDAC-Policy-Wizard/app/src/PolicyMerge_Control.cs index 89651726..f9810a52 100644 --- a/WDAC-Policy-Wizard/app/src/PolicyMerge_Control.cs +++ b/WDAC-Policy-Wizard/app/src/PolicyMerge_Control.cs @@ -110,6 +110,10 @@ private void Button_AddPolicy_Click(object sender, EventArgs e) this.displayObjects.Add(new DisplayObject(this.nPolicies.ToString(), policyPath)); this.policiesDataGrid.RowCount += 1; + // Size each column to fit its text. Columns remain user-resizable + // afterwards since AutoSizeColumnsMode stays None. + GridLayoutHelper.AutoFitColumns(this.policiesDataGrid); + this._MainWindow.Policy.PoliciesToMerge = this.policiesToMerge; if (this.nPolicies >= 2 && !String.IsNullOrEmpty(this.mergePolicyPath)) diff --git a/WDAC-Policy-Wizard/app/src/SigningRules_Control.Designer.cs b/WDAC-Policy-Wizard/app/src/SigningRules_Control.Designer.cs index 1653d537..21e50982 100644 --- a/WDAC-Policy-Wizard/app/src/SigningRules_Control.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/SigningRules_Control.Designer.cs @@ -82,7 +82,7 @@ private void InitializeComponent() // rulesDataGrid // this.rulesDataGrid.AllowUserToDeleteRows = false; - this.rulesDataGrid.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.AllCells; + this.rulesDataGrid.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.None; dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; dataGridViewCellStyle1.Font = new System.Drawing.Font("Microsoft Sans Serif", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True; @@ -106,13 +106,14 @@ private void InitializeComponent() this.rulesDataGrid.RowTemplate.Height = 24; this.rulesDataGrid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect; this.rulesDataGrid.Size = new System.Drawing.Size(879, 440); + this.rulesDataGrid.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.rulesDataGrid.TabIndex = 92; this.rulesDataGrid.VirtualMode = true; this.rulesDataGrid.CellValueNeeded += new System.Windows.Forms.DataGridViewCellValueEventHandler(this.RulesDataGrid_CellValueNeeded); // // column_Action // - this.column_Action.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.AllCells; + this.column_Action.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None; this.column_Action.HeaderText = "Action"; this.column_Action.MinimumWidth = 6; this.column_Action.Name = "column_Action"; @@ -121,7 +122,7 @@ private void InitializeComponent() // // column_Level // - this.column_Level.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.AllCells; + this.column_Level.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None; this.column_Level.HeaderText = "Level"; this.column_Level.MinimumWidth = 6; this.column_Level.Name = "column_Level"; @@ -130,7 +131,7 @@ private void InitializeComponent() // // Column_Name // - this.Column_Name.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.AllCells; + this.Column_Name.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None; this.Column_Name.HeaderText = "Name"; this.Column_Name.MinimumWidth = 6; this.Column_Name.Name = "Column_Name"; @@ -139,7 +140,7 @@ private void InitializeComponent() // // Column_Files // - this.Column_Files.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.AllCells; + this.Column_Files.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None; this.Column_Files.HeaderText = "Associated Files"; this.Column_Files.MinimumWidth = 6; this.Column_Files.Name = "Column_Files"; @@ -148,7 +149,7 @@ private void InitializeComponent() // // Column_Exceptions // - this.Column_Exceptions.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.AllCells; + this.Column_Exceptions.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None; this.Column_Exceptions.HeaderText = "Exceptions"; this.Column_Exceptions.MinimumWidth = 6; this.Column_Exceptions.Name = "Column_Exceptions"; @@ -157,7 +158,7 @@ private void InitializeComponent() // // column_ID // - this.column_ID.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.AllCells; + this.column_ID.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None; this.column_ID.HeaderText = "Rule ID"; this.column_ID.MinimumWidth = 8; this.column_ID.Name = "column_ID"; @@ -195,6 +196,7 @@ private void InitializeComponent() this.deleteButton.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.deleteButton.Name = "deleteButton"; this.deleteButton.Size = new System.Drawing.Size(114, 26); + this.deleteButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.deleteButton.TabIndex = 93; this.deleteButton.Text = "- Remove Rule"; this.deleteButton.TextAlign = System.Drawing.ContentAlignment.MiddleRight; @@ -211,6 +213,7 @@ private void InitializeComponent() this.label_Error.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label_Error.Name = "label_Error"; this.label_Error.Size = new System.Drawing.Size(648, 18); + this.label_Error.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); this.label_Error.TabIndex = 96; this.label_Error.Tag = "IgnoreDarkMode"; this.label_Error.Text = "Label_Error: Lorem Ipsum text text text text. Lorum Ipsum text text text text tex" + @@ -224,6 +227,7 @@ private void InitializeComponent() this.addButton.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.addButton.Name = "addButton"; this.addButton.Size = new System.Drawing.Size(146, 26); + this.addButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); this.addButton.TabIndex = 97; this.addButton.Text = "+ Add Custom Rule"; this.addButton.TextAlign = System.Drawing.ContentAlignment.MiddleRight; @@ -236,6 +240,7 @@ private void InitializeComponent() this.checkBox_KernelList.Location = new System.Drawing.Point(163, 634); this.checkBox_KernelList.Name = "checkBox_KernelList"; this.checkBox_KernelList.Size = new System.Drawing.Size(320, 21); + this.checkBox_KernelList.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); this.checkBox_KernelList.TabIndex = 98; this.checkBox_KernelList.Text = "Merge with Recommended Kernel Block Rules"; this.checkBox_KernelList.UseVisualStyleBackColor = true; @@ -248,6 +253,7 @@ private void InitializeComponent() this.checkBox_UserModeList.Location = new System.Drawing.Point(163, 607); this.checkBox_UserModeList.Name = "checkBox_UserModeList"; this.checkBox_UserModeList.Size = new System.Drawing.Size(348, 21); + this.checkBox_UserModeList.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); this.checkBox_UserModeList.TabIndex = 99; this.checkBox_UserModeList.Text = "Merge with Recommended User Mode Block Rules"; this.checkBox_UserModeList.UseVisualStyleBackColor = false; @@ -295,6 +301,7 @@ private void InitializeComponent() // this.AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; + this.AutoScroll = true; this.BackColor = System.Drawing.Color.White; this.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center; this.Controls.Add(this.panel_Progress); diff --git a/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs b/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs index 91a0078b..835e8dfc 100644 --- a/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs +++ b/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs @@ -504,6 +504,10 @@ private void DisplayRules() } } } + + // Size each column to fit its text once all rules are loaded. Columns remain + // user-resizable afterwards since AutoSizeColumnsMode stays None. + GridLayoutHelper.AutoFitColumns(this.rulesDataGrid); } @@ -1213,6 +1217,10 @@ public void AddRuleToTable(string [] displayObjectArray, PolicyCustomRules custo this.displayObjects.Add(new DisplayObject(action, level, name, files, exceptions)); this.rulesDataGrid.RowCount += 1; + // Size each column to fit its text. Columns remain user-resizable afterwards + // since AutoSizeColumnsMode stays None. + GridLayoutHelper.AutoFitColumns(this.rulesDataGrid); + // Add custom list to RulesList this.Policy.CustomRules.Add(customRule); From a092ef091be573838c11c8f2aa3e866650ef5e74 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Sun, 31 May 2026 10:31:30 -0400 Subject: [PATCH 07/16] The host was force-docking every UserControl added to the form. I introduced a small marker interface so only intended full-page wizard content is docked --- WDAC-Policy-Wizard/app/src/BuildPage.cs | 2 +- WDAC-Policy-Wizard/app/src/ConfigTemplate_Control.cs | 2 +- WDAC-Policy-Wizard/app/src/EditWorkflow.cs | 2 +- .../app/src/EventLogRuleConfiguration.cs | 2 +- WDAC-Policy-Wizard/app/src/IWizardPage.cs | 12 ++++++++++++ WDAC-Policy-Wizard/app/src/MainForm.cs | 7 ++++--- WDAC-Policy-Wizard/app/src/PolicyMerge_Control.cs | 2 +- WDAC-Policy-Wizard/app/src/PolicyType.cs | 2 +- WDAC-Policy-Wizard/app/src/SettingsPage.cs | 2 +- WDAC-Policy-Wizard/app/src/SigningRules_Control.cs | 2 +- WDAC-Policy-Wizard/app/src/TemplatePage.cs | 2 +- 11 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 WDAC-Policy-Wizard/app/src/IWizardPage.cs diff --git a/WDAC-Policy-Wizard/app/src/BuildPage.cs b/WDAC-Policy-Wizard/app/src/BuildPage.cs index 0b41d04e..d2e9369d 100644 --- a/WDAC-Policy-Wizard/app/src/BuildPage.cs +++ b/WDAC-Policy-Wizard/app/src/BuildPage.cs @@ -11,7 +11,7 @@ namespace WDAC_Wizard { - public partial class BuildPage : UserControl + public partial class BuildPage : UserControl, IWizardPage { public string XmlFilePath { get; set; } // File path for the WDAC policy XML file public string BinFilePath { get; set; } // File path for the WDAC policy binary file diff --git a/WDAC-Policy-Wizard/app/src/ConfigTemplate_Control.cs b/WDAC-Policy-Wizard/app/src/ConfigTemplate_Control.cs index 1ed52f4c..e2b8f678 100644 --- a/WDAC-Policy-Wizard/app/src/ConfigTemplate_Control.cs +++ b/WDAC-Policy-Wizard/app/src/ConfigTemplate_Control.cs @@ -14,7 +14,7 @@ namespace WDAC_Wizard { - public partial class ConfigTemplate_Control : UserControl + public partial class ConfigTemplate_Control : UserControl, IWizardPage { public MainWindow _MainWindow; private WDAC_Policy Policy; diff --git a/WDAC-Policy-Wizard/app/src/EditWorkflow.cs b/WDAC-Policy-Wizard/app/src/EditWorkflow.cs index 38c6e9c9..d9fe6063 100644 --- a/WDAC-Policy-Wizard/app/src/EditWorkflow.cs +++ b/WDAC-Policy-Wizard/app/src/EditWorkflow.cs @@ -12,7 +12,7 @@ namespace WDAC_Wizard { - public partial class EditWorkflow : UserControl + public partial class EditWorkflow : UserControl, IWizardPage { public string EditPath { get; set; } private int NumberRules; diff --git a/WDAC-Policy-Wizard/app/src/EventLogRuleConfiguration.cs b/WDAC-Policy-Wizard/app/src/EventLogRuleConfiguration.cs index 1f4d9f44..c3e9831f 100644 --- a/WDAC-Policy-Wizard/app/src/EventLogRuleConfiguration.cs +++ b/WDAC-Policy-Wizard/app/src/EventLogRuleConfiguration.cs @@ -12,7 +12,7 @@ namespace WDAC_Wizard { - public partial class EventLogRuleConfiguration : UserControl + public partial class EventLogRuleConfiguration : UserControl, IWizardPage { private List CiEvents; // Declare an ArrayList to serve as the data store. diff --git a/WDAC-Policy-Wizard/app/src/IWizardPage.cs b/WDAC-Policy-Wizard/app/src/IWizardPage.cs new file mode 100644 index 00000000..062e0e9c --- /dev/null +++ b/WDAC-Policy-Wizard/app/src/IWizardPage.cs @@ -0,0 +1,12 @@ +namespace WDAC_Wizard +{ + /// + /// Marker interface implemented by full-page wizard content (UserControls) that the MainWindow + /// hosts and docks to fill its client area. The host uses this marker to dock only intended + /// wizard pages, rather than every UserControl that may be added to the form. Helper or + /// non-page UserControls should NOT implement this interface so they keep their own layout. + /// + public interface IWizardPage + { + } +} diff --git a/WDAC-Policy-Wizard/app/src/MainForm.cs b/WDAC-Policy-Wizard/app/src/MainForm.cs index 81eb63a6..bc1f97af 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.cs @@ -104,12 +104,13 @@ private void RegisterPage(Control page) } /// - /// Automatically docks any wizard page (UserControl) added to the MainWindow so its content - /// expands and contracts with the host window. + /// Automatically docks wizard pages added to the MainWindow so their content expands and + /// contracts with the host window. Only controls marked with are + /// docked, so helper/non-page UserControls keep their own layout and are not forcibly docked. /// private void MainWindow_ControlAdded(object sender, ControlEventArgs e) { - if (e.Control is UserControl) + if (e.Control is IWizardPage) { RegisterPage(e.Control); } diff --git a/WDAC-Policy-Wizard/app/src/PolicyMerge_Control.cs b/WDAC-Policy-Wizard/app/src/PolicyMerge_Control.cs index f9810a52..7e7c2167 100644 --- a/WDAC-Policy-Wizard/app/src/PolicyMerge_Control.cs +++ b/WDAC-Policy-Wizard/app/src/PolicyMerge_Control.cs @@ -12,7 +12,7 @@ namespace WDAC_Wizard.src { - public partial class PolicyMerge_Control : UserControl + public partial class PolicyMerge_Control : UserControl, IWizardPage { private int nPolicies; private string mergePolicyPath; diff --git a/WDAC-Policy-Wizard/app/src/PolicyType.cs b/WDAC-Policy-Wizard/app/src/PolicyType.cs index 9f8b4ca0..3e6f2add 100644 --- a/WDAC-Policy-Wizard/app/src/PolicyType.cs +++ b/WDAC-Policy-Wizard/app/src/PolicyType.cs @@ -11,7 +11,7 @@ namespace WDAC_Wizard { - public partial class PolicyType : UserControl + public partial class PolicyType : UserControl, IWizardPage { public string BaseToSupplementPath { get; set; } // Path to the supplemental policy on disk diff --git a/WDAC-Policy-Wizard/app/src/SettingsPage.cs b/WDAC-Policy-Wizard/app/src/SettingsPage.cs index d8678559..3a3cdc42 100644 --- a/WDAC-Policy-Wizard/app/src/SettingsPage.cs +++ b/WDAC-Policy-Wizard/app/src/SettingsPage.cs @@ -17,7 +17,7 @@ namespace WDAC_Wizard { - public partial class SettingsPage : UserControl + public partial class SettingsPage : UserControl, IWizardPage { private Dictionary SettingsDict; private MainWindow _MainWindow; diff --git a/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs b/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs index 835e8dfc..41b1072f 100644 --- a/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs +++ b/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs @@ -16,7 +16,7 @@ namespace WDAC_Wizard { - public partial class SigningRules_Control : UserControl + public partial class SigningRules_Control : UserControl, IWizardPage { // CI Policy objects public WDAC_Policy Policy; diff --git a/WDAC-Policy-Wizard/app/src/TemplatePage.cs b/WDAC-Policy-Wizard/app/src/TemplatePage.cs index 698d0e9b..9ee71f9c 100644 --- a/WDAC-Policy-Wizard/app/src/TemplatePage.cs +++ b/WDAC-Policy-Wizard/app/src/TemplatePage.cs @@ -11,7 +11,7 @@ namespace WDAC_Wizard { - public partial class TemplatePage : UserControl + public partial class TemplatePage : UserControl, IWizardPage { // Properties to maintain the policy mode selected private MainWindow _MainWindow; From a478c98a586b19e64234aa56b3f18bc4f8359d40 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Sun, 31 May 2026 10:57:07 -0400 Subject: [PATCH 08/16] =?UTF-8?q?=E2=80=A2=20Added=20RulesDataGrid=5FColum?= =?UTF-8?q?nHeaderMouseClick(object,=20DataGridViewCellMouseEventArgs),=20?= =?UTF-8?q?which=20sorts=20the=20displayObjects=20data=20store=20by=20the?= =?UTF-8?q?=20clicked=20column=20and=20toggles=20ascending/descending=20on?= =?UTF-8?q?=20repeated=20clicks=20(matching=20the=20event=20grid=20behavio?= =?UTF-8?q?r).=20=E2=80=A2=20Critically,=20because=20in-session=20custom?= =?UTF-8?q?=20rules=20are=20linked=20to=20grid=20rows=20via=20RowNumber,?= =?UTF-8?q?=20the=20handler=20remaps=20those=20row=20references=20after=20?= =?UTF-8?q?sorting=20so=20rule=20deletion/editing=20still=20targets=20the?= =?UTF-8?q?=20correct=20rows.=20=E2=80=A2=20Added=20GetDisplayObjectCompar?= =?UTF-8?q?ison(string)=20and=20a=20CompareDisplayStrings(string,=20string?= =?UTF-8?q?)=20helper=20that=20does=20case-insensitive=20comparison=20and?= =?UTF-8?q?=20pushes=20empty/null=20values=20to=20the=20bottom=20(same=20c?= =?UTF-8?q?onvention=20as=20the=20event=20grid's=20SortDisplayObjects(int,?= =?UTF-8?q?=20bool)).=20=E2=80=A2=20Added=20a=20sort=20glyph=20(=E2=96=B2/?= =?UTF-8?q?=E2=96=BC)=20on=20the=20active=20column=20header=20for=20visual?= =?UTF-8?q?=20feedback.=20The=20existing=20auto-fit=20sizing,=20virtual-mo?= =?UTF-8?q?de=20binding,=20and=20resize/docking=20behavior=20are=20all=20p?= =?UTF-8?q?reserved.=20You=20can=20now=20click=20any=20column=20header=20i?= =?UTF-8?q?n=20the=20File=20Rules=20grid=20to=20sort,=20and=20click=20agai?= =?UTF-8?q?n=20to=20reverse=20the=20order.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/src/SigningRules_Control.Designer.cs | 7 + .../app/src/SigningRules_Control.cs | 133 ++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/WDAC-Policy-Wizard/app/src/SigningRules_Control.Designer.cs b/WDAC-Policy-Wizard/app/src/SigningRules_Control.Designer.cs index 21e50982..bd985246 100644 --- a/WDAC-Policy-Wizard/app/src/SigningRules_Control.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/SigningRules_Control.Designer.cs @@ -110,6 +110,7 @@ private void InitializeComponent() this.rulesDataGrid.TabIndex = 92; this.rulesDataGrid.VirtualMode = true; this.rulesDataGrid.CellValueNeeded += new System.Windows.Forms.DataGridViewCellValueEventHandler(this.RulesDataGrid_CellValueNeeded); + this.rulesDataGrid.ColumnHeaderMouseClick += new System.Windows.Forms.DataGridViewCellMouseEventHandler(this.RulesDataGrid_ColumnHeaderMouseClick); // // column_Action // @@ -118,6 +119,7 @@ private void InitializeComponent() this.column_Action.MinimumWidth = 6; this.column_Action.Name = "column_Action"; this.column_Action.ReadOnly = true; + this.column_Action.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.column_Action.Width = 76; // // column_Level @@ -127,6 +129,7 @@ private void InitializeComponent() this.column_Level.MinimumWidth = 6; this.column_Level.Name = "column_Level"; this.column_Level.ReadOnly = true; + this.column_Level.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.column_Level.Width = 71; // // Column_Name @@ -136,6 +139,7 @@ private void InitializeComponent() this.Column_Name.MinimumWidth = 6; this.Column_Name.Name = "Column_Name"; this.Column_Name.ReadOnly = true; + this.Column_Name.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.Column_Name.Width = 74; // // Column_Files @@ -145,6 +149,7 @@ private void InitializeComponent() this.Column_Files.MinimumWidth = 6; this.Column_Files.Name = "Column_Files"; this.Column_Files.ReadOnly = true; + this.Column_Files.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.Column_Files.Width = 127; // // Column_Exceptions @@ -154,6 +159,7 @@ private void InitializeComponent() this.Column_Exceptions.MinimumWidth = 6; this.Column_Exceptions.Name = "Column_Exceptions"; this.Column_Exceptions.ReadOnly = true; + this.Column_Exceptions.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.Column_Exceptions.Width = 105; // // column_ID @@ -163,6 +169,7 @@ private void InitializeComponent() this.column_ID.MinimumWidth = 8; this.column_ID.Name = "column_ID"; this.column_ID.ReadOnly = true; + this.column_ID.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.column_ID.Width = 77; // // label8 diff --git a/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs b/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs index 41b1072f..955f9874 100644 --- a/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs +++ b/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs @@ -1141,6 +1141,139 @@ private void RemoveRuleIdFromFileAttribs(string ruleId) } } + /// + /// Sorts the rules grid by the clicked column header. Mirrors the event grid behavior: + /// the first click on a column sorts ascending and subsequent clicks toggle the direction. + /// The underlying displayObjects data store and the custom rule RowNumber references are + /// kept in sync so deletion and editing continue to map to the correct rows. + /// + /// + /// + private void RulesDataGrid_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e) + { + if (e.ColumnIndex < 0 || this.displayObjects.Count == 0) + { + return; + } + + string columnName = this.rulesDataGrid.Columns[e.ColumnIndex].Name; + + // Determine sort direction. If the column is already the sorted column, toggle the + // direction. Otherwise sort ascending and clear the sorted tag on the other columns. + bool ascending = true; + if (this.rulesDataGrid.Columns[e.ColumnIndex].Tag as string == "SortedAsc") + { + ascending = false; + } + + // Project the display objects onto a sortable list paired with their original row index + // so the custom rule RowNumber references can be remapped after sorting. + var indexedObjects = new List>(); + for (int i = 0; i < this.displayObjects.Count; i++) + { + indexedObjects.Add(new KeyValuePair(i, (DisplayObject)this.displayObjects[i])); + } + + Comparison comparison = GetDisplayObjectComparison(columnName); + indexedObjects.Sort((x, y) => + { + int result = comparison(x.Value, y.Value); + return ascending ? result : -result; + }); + + // Map each original row index to its new sorted position. + var oldToNewIndex = new Dictionary(); + for (int newIndex = 0; newIndex < indexedObjects.Count; newIndex++) + { + oldToNewIndex[indexedObjects[newIndex].Key] = newIndex; + } + + // Rebuild the display objects data store in the sorted order. + this.displayObjects.Clear(); + foreach (var pair in indexedObjects) + { + this.displayObjects.Add(pair.Value); + } + + // Remap the in-session custom rule row references so deletion still targets the right row. + if (this.Policy != null && this.Policy.CustomRules != null) + { + foreach (var customRule in this.Policy.CustomRules) + { + if (oldToNewIndex.TryGetValue(customRule.RowNumber, out int newRowNumber)) + { + customRule.RowNumber = newRowNumber; + } + } + } + + // Update the sorted tags so the next click toggles the direction. + foreach (DataGridViewColumn column in this.rulesDataGrid.Columns) + { + column.Tag = null; + column.HeaderCell.SortGlyphDirection = SortOrder.None; + } + + this.rulesDataGrid.Columns[e.ColumnIndex].Tag = ascending ? "SortedAsc" : "SortedDesc"; + this.rulesDataGrid.Columns[e.ColumnIndex].HeaderCell.SortGlyphDirection = + ascending ? SortOrder.Ascending : SortOrder.Descending; + + // Repaint the virtual-mode grid with the newly ordered data store. + this.rulesDataGrid.Refresh(); + } + + /// + /// Returns the comparison delegate used to sort the display objects for the given column. + /// Empty/null values are sorted to the bottom for ascending sorts. + /// + /// + /// + private Comparison GetDisplayObjectComparison(string columnName) + { + switch (columnName) + { + case "column_Action": + return (x, y) => CompareDisplayStrings(x.Action, y.Action); + + case "column_Level": + return (x, y) => CompareDisplayStrings(x.Level, y.Level); + + case "Column_Name": + return (x, y) => CompareDisplayStrings(x.Name, y.Name); + + case "Column_Files": + return (x, y) => CompareDisplayStrings(x.Files, y.Files); + + case "Column_Exceptions": + return (x, y) => CompareDisplayStrings(x.Exceptions, y.Exceptions); + + case "column_ID": + return (x, y) => CompareDisplayStrings(x.Id, y.Id); + + default: + return (x, y) => 0; + } + } + + /// + /// Case-insensitive string comparison that sorts empty/null values to the bottom. + /// + /// + /// + /// + private static int CompareDisplayStrings(string x, string y) + { + bool xEmpty = string.IsNullOrWhiteSpace(x); + bool yEmpty = string.IsNullOrWhiteSpace(y); + + // Sort empty/null strings to the bottom + if (xEmpty && yEmpty) return 0; + if (xEmpty) return 1; + if (yEmpty) return -1; + + return string.Compare(x, y, StringComparison.CurrentCultureIgnoreCase); + } + /// /// Sets the display object when the DataGridView needed to paint data /// From 071754777c7378efa39f7a83474c9a287a1cc58b Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Sun, 31 May 2026 11:33:59 -0400 Subject: [PATCH 09/16] 1. Added two layout constants (DefaultSliderBoxX = 139, FileAttributeSliderBoxX = 165) plus a fixed right edge so the boxes stay inside the panel. 2. Added a small helper, OffsetSliderValueBoxes(int xLocation), that shifts the value boxes' left edge and adjusts their width (keeping the right edge fixed). Auto expand when making window bigger. --- .../src/CustomRuleConditionsPanel.Designer.cs | 9 +++ .../app/src/CustomRuleConditionsPanel.cs | 69 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs index 9e3a9c24..47a0e1d4 100644 --- a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.Designer.cs @@ -693,16 +693,19 @@ private void InitializeComponent() panel_Publisher_Scroll.Controls.Add(textBoxSlider_4); panel_Publisher_Scroll.Controls.Add(textBoxSlider_1); panel_Publisher_Scroll.Controls.Add(textBoxSlider_0); + panel_Publisher_Scroll.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; panel_Publisher_Scroll.Location = new System.Drawing.Point(4, 389); panel_Publisher_Scroll.Margin = new System.Windows.Forms.Padding(2); panel_Publisher_Scroll.Name = "panel_Publisher_Scroll"; panel_Publisher_Scroll.Size = new System.Drawing.Size(510, 309); panel_Publisher_Scroll.TabIndex = 103; panel_Publisher_Scroll.Visible = false; + panel_Publisher_Scroll.Resize += Panel_Publisher_Scroll_Resize; // // textBoxSlider_2 // textBoxSlider_2.BackColor = System.Drawing.SystemColors.Control; + textBoxSlider_2.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; textBoxSlider_2.Enabled = false; textBoxSlider_2.Font = new System.Drawing.Font("Tahoma", 9F); textBoxSlider_2.Location = new System.Drawing.Point(139, 100); @@ -776,6 +779,7 @@ private void InitializeComponent() // textBoxEKU // textBoxEKU.BackColor = System.Drawing.SystemColors.Control; + textBoxEKU.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; textBoxEKU.Enabled = false; textBoxEKU.Font = new System.Drawing.Font("Tahoma", 9F); textBoxEKU.Location = new System.Drawing.Point(139, 239); @@ -825,6 +829,7 @@ private void InitializeComponent() // textBox_MaxVersion // textBox_MaxVersion.BackColor = System.Drawing.SystemColors.Control; + textBox_MaxVersion.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; textBox_MaxVersion.Enabled = false; textBox_MaxVersion.Font = new System.Drawing.Font("Tahoma", 9F); textBox_MaxVersion.ForeColor = System.Drawing.SystemColors.WindowText; @@ -841,6 +846,7 @@ private void InitializeComponent() // textBoxSlider_3 // textBoxSlider_3.BackColor = System.Drawing.SystemColors.Control; + textBoxSlider_3.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; textBoxSlider_3.Enabled = false; textBoxSlider_3.Font = new System.Drawing.Font("Tahoma", 9F); textBoxSlider_3.Location = new System.Drawing.Point(139, 142); @@ -854,6 +860,7 @@ private void InitializeComponent() // textBoxSlider_4 // textBoxSlider_4.BackColor = System.Drawing.SystemColors.Control; + textBoxSlider_4.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; textBoxSlider_4.Enabled = false; textBoxSlider_4.Font = new System.Drawing.Font("Tahoma", 9F); textBoxSlider_4.Location = new System.Drawing.Point(139, 184); @@ -867,6 +874,7 @@ private void InitializeComponent() // textBoxSlider_1 // textBoxSlider_1.BackColor = System.Drawing.SystemColors.Control; + textBoxSlider_1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; textBoxSlider_1.Enabled = false; textBoxSlider_1.Font = new System.Drawing.Font("Tahoma", 9F); textBoxSlider_1.Location = new System.Drawing.Point(139, 58); @@ -880,6 +888,7 @@ private void InitializeComponent() // textBoxSlider_0 // textBoxSlider_0.BackColor = System.Drawing.SystemColors.Control; + textBoxSlider_0.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; textBoxSlider_0.Enabled = false; textBoxSlider_0.Font = new System.Drawing.Font("Tahoma", 9F); textBoxSlider_0.Location = new System.Drawing.Point(139, 16); diff --git a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs index 1d44c930..9403b259 100644 --- a/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs +++ b/WDAC-Policy-Wizard/app/src/CustomRuleConditionsPanel.cs @@ -36,6 +36,24 @@ public partial class CustomRuleConditionsPanel : Form private string PrevComText = String.Empty; private bool IgnoreInput = false; + // X-coordinate of the slider value boxes (textBoxSlider_*) for the different rule types. + // File Attribute labels (e.g. "Original filename:") are wider than the Publisher labels, + // so the value boxes need to be shifted right to avoid overlapping the labels. + private const int DefaultSliderBoxX = 139; + private const int FileAttributeSliderBoxX = 165; + + // Padding kept between the right edge of the value boxes and the panel edge so the + // boxes never run off the right side of the window. + private const int SliderBoxRightPadding = 14; + + // Upper bound on the value box width so the boxes don't become unreasonably long on + // very wide windows. + private const int SliderBoxMaxWidth = 700; + + // Tracks the current left edge of the value boxes so the layout can be recomputed when + // the panel (and therefore the window) is resized. + private int currentSliderBoxX = DefaultSliderBoxX; + private enum UIState { RuleConditions = 0, @@ -1066,6 +1084,50 @@ private void SetFileSignerInfo(string refPath) this.PolicyCustomRule.SupportedCrypto = true; } + /// + /// Moves the slider value boxes (textBoxSlider_*) to the supplied X-coordinate so that wider + /// labels (e.g. File Attribute "Original filename:") do not overlap the value boxes. The right + /// edge is kept fixed so the boxes remain within the panel. + /// + /// The new X-coordinate for the left edge of the value boxes. + private void OffsetSliderValueBoxes(int xLocation) + { + this.currentSliderBoxX = xLocation; + + TextBox[] fullWidthBoxes = { this.textBoxSlider_0, this.textBoxSlider_1, this.textBoxSlider_2, this.textBoxSlider_3 }; + + // Expand the boxes to fill the panel, leaving padding on the right so they never run + // off the edge of the window. Cap the width so the boxes don't get too long. + int availableWidth = this.panel_Publisher_Scroll.ClientSize.Width - xLocation - SliderBoxRightPadding; + int newWidth = Math.Min(availableWidth, SliderBoxMaxWidth); + if (newWidth < 100) + { + newWidth = 100; // sanity floor + } + + foreach (TextBox box in fullWidthBoxes) + { + box.Location = new Point(xLocation, box.Location.Y); + box.Size = new Size(newWidth, box.Size.Height); + } + + // textBoxEKU spans the full width like the attribute boxes + this.textBoxEKU.Location = new Point(xLocation, this.textBoxEKU.Location.Y); + this.textBoxEKU.Size = new Size(newWidth, this.textBoxEKU.Size.Height); + + // textBoxSlider_4 (version box) keeps its own width but follows the same left edge + this.textBoxSlider_4.Location = new Point(xLocation, this.textBoxSlider_4.Location.Y); + } + + /// + /// Recomputes the slider value box layout when the panel is resized so the boxes grow + /// and shrink with the window width. + /// + private void Panel_Publisher_Scroll_Resize(object sender, EventArgs e) + { + OffsetSliderValueBoxes(this.currentSliderBoxX); + } + /// /// Sets the default state of the textboxes and checkboxes based on the rule type /// @@ -1137,6 +1199,9 @@ private void SetDefaultUIState(PolicyCustomRules.RuleType ruleType) this.checkBoxAttribute3.Text = "File name:"; this.checkBoxAttribute4.Text = "Min. Version:"; + // Publisher labels are narrow; restore the value boxes to their default position + OffsetSliderValueBoxes(DefaultSliderBoxX); + // Version textbox should be set to normal size this.textBoxSlider_4.Size = this.textBoxSlider_3.Size; @@ -1218,6 +1283,10 @@ private void SetDefaultUIState(PolicyCustomRules.RuleType ruleType) this.checkBoxAttribute2.Text = "Product name:"; this.checkBoxAttribute3.Text = "Internal name:"; + // The File Attribute labels (e.g. "Original filename:") are wider than the + // Publisher labels, so shift the value boxes right to avoid overlapping the labels + OffsetSliderValueBoxes(FileAttributeSliderBoxX); + // Set checkbox states to all disabled -- allow user to select the ones desired this.checkBoxAttribute0.Checked = false; this.checkBoxAttribute1.Checked = false; From 05a240a0a25fb18f0d17cf0150da8d9a66fd6d86 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Sun, 31 May 2026 20:25:14 -0400 Subject: [PATCH 10/16] Updated both templates to latest version Added a checker to see if the edit policy has merge rules, if so have the check box(es) checked Fixed Duplicating rules on re-check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • MergeFileRules(Object[], Object[]) now skips incoming Allow/Deny/FileRule entries whose ID already exists in the destination policy (case-insensitive). Added a GetFileRuleId(Object) helper. • MergeProductSigners(ProductSigners, ProductSigners) now dedupes FileRuleRef entries by RuleID the same way. • Re-merging the Recommended Driver/User-Mode Blocklist is now idempotent. --- .../Recommended_Driver_Blocklist.xml | 3085 +++++++++-------- .../Recommended_UserMode_Blocklist.xml | 124 +- WDAC-Policy-Wizard/app/src/PolicyHelper.cs | 245 +- .../app/src/SigningRules_Control.cs | 142 +- 4 files changed, 2043 insertions(+), 1553 deletions(-) diff --git a/WDAC-Policy-Wizard/app/Resources/policyTemplates/Recommended_Driver_Blocklist.xml b/WDAC-Policy-Wizard/app/Resources/policyTemplates/Recommended_Driver_Blocklist.xml index ffc052c4..254a52e9 100644 --- a/WDAC-Policy-Wizard/app/Resources/policyTemplates/Recommended_Driver_Blocklist.xml +++ b/WDAC-Policy-Wizard/app/Resources/policyTemplates/Recommended_Driver_Blocklist.xml @@ -1,16 +1,21 @@  - - 10.0.27770.0 + + 10.0.29545.0 {2E07F7E4-194C-4D20-B7C9-6F44A6C5A234} + + + - + Ok, + + @@ -117,10 +122,6 @@ - - - - @@ -269,40 +270,40 @@ - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -477,32 +478,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -619,10 +614,10 @@ - - - - + + + + @@ -631,28 +626,42 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -684,7 +693,7 @@ - + @@ -694,16 +703,16 @@ - - - + + + - - - - - - + + + + + + @@ -788,90 +797,92 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -962,38 +973,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1130,11 +1141,11 @@ - - - - - + + + + + @@ -1143,34 +1154,46 @@ - - - - + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1187,266 +1210,254 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1476,14 +1487,14 @@ - - - - - - - - + + + + + + + + @@ -1504,86 +1515,86 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1668,10 +1679,10 @@ - - - - + + + + @@ -1701,6 +1712,12 @@ + + + + + + @@ -1708,18 +1725,20 @@ - - + + + + - - - + + + @@ -1728,19 +1747,20 @@ - + - - - + + - - + + - + + + @@ -1749,116 +1769,128 @@ - - + + + - - - - - - - + + + + + + + - - - + + + - - + + - - + + - + + - - - - + + + + - - + + - - + + - + - + + - - + + + + - + - + - + + - - - - + + + - - - - - - + + + + + + + + - - - + + + - + - - + + - + - - - - - - + + + + + + + + + + + @@ -1868,27 +1900,35 @@ - + + + + + + + + + @@ -1901,6 +1941,7 @@ + @@ -1912,7 +1953,7 @@ - + @@ -1950,13 +1991,14 @@ + + - @@ -2013,35 +2055,47 @@ + + - + + + + + + + + - + + + + @@ -2053,36 +2107,45 @@ - + + - + - + + + + + + + + + - + @@ -2093,16 +2156,19 @@ + + + + - @@ -2149,7 +2215,6 @@ - @@ -2170,17 +2235,17 @@ - - - - - - + + + + + + @@ -2246,26 +2311,22 @@ - - - - @@ -2304,7 +2365,7 @@ - + @@ -2444,27 +2505,8 @@ - - - - - - - - - - - - - - - - - - - @@ -2476,7 +2518,6 @@ - @@ -2487,12 +2528,11 @@ - - - - + + + @@ -2651,200 +2691,231 @@ - - - - - - - - - - - - - + + + - - + + - - - + + + - - - + + + - - - - + + + + - - - - + + + + - - + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - + + + + + + + + + + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - + + + - - + + - - + + - - - - - + + + + + - - - + + + - - - + + + - - - - + + + + - - - + + + - - - + + + - - - + + + - - + + + - - + + + - - - - - - + + + - - - + + + - - - + + + - - - + + + @@ -2852,21 +2923,155 @@ - - - + + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + @@ -2914,92 +3119,52 @@ - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - + - - + + - + - - + + - - + + - - + + - - + + - + + + + + + + + @@ -3007,8 +3172,11 @@ + + + @@ -3030,19 +3198,18 @@ - - - - + + + @@ -3050,12 +3217,12 @@ - - - + + + @@ -3102,10 +3269,12 @@ - + + + @@ -3134,24 +3303,37 @@ + + + - + + + + + + + + - - - - + + + + + + + @@ -3162,17 +3344,21 @@ + - - - + + + + + + @@ -3197,20 +3383,15 @@ - - - - - - - - - + + + + @@ -3317,60 +3498,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -3399,6 +3526,7 @@ + @@ -3477,12 +3605,12 @@ - - - - - - + + + + + + @@ -3492,17 +3620,17 @@ - - - - - - - - - - - + + + + + + + + + + + @@ -3677,12 +3805,6 @@ - - - - - - @@ -3692,17 +3814,17 @@ - - - - - - - - - - - + + + + + + + + + + + @@ -3711,7 +3833,7 @@ - + @@ -3831,6 +3953,14 @@ + + + + + + + + @@ -3842,17 +3972,23 @@ - - - - - - - + + + + + + + + + + + + + @@ -3891,19 +4027,19 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -3988,6 +4124,8 @@ + + @@ -3998,80 +4136,80 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -4152,9 +4290,9 @@ - - - + + + @@ -4172,29 +4310,29 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -4359,19 +4497,31 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + @@ -4388,10 +4538,6 @@ - - - - @@ -4401,81 +4547,81 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -4485,117 +4631,105 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -4609,37 +4743,37 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -4649,9 +4783,9 @@ - - - + + + @@ -4681,14 +4815,14 @@ - - - - - - - - + + + + + + + + @@ -4709,14 +4843,14 @@ - - - - - - - - + + + + + + + + @@ -4726,61 +4860,61 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -4873,10 +5007,10 @@ - - - - + + + + @@ -4890,37 +5024,46 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + @@ -4934,14 +5077,10 @@ - 10.0.27770.0 - - - - - true + 10.0.29545.0 - {A244370E-44C9-4C06-B551-F6016E563076} + {C017F6D6-2021-4204-AA68-F5065969B65E} + {C017F6D6-2021-4204-AA68-F5065969B65E} \ No newline at end of file diff --git a/WDAC-Policy-Wizard/app/Resources/policyTemplates/Recommended_UserMode_Blocklist.xml b/WDAC-Policy-Wizard/app/Resources/policyTemplates/Recommended_UserMode_Blocklist.xml index 6e8a1031..d92be85a 100644 --- a/WDAC-Policy-Wizard/app/Resources/policyTemplates/Recommended_UserMode_Blocklist.xml +++ b/WDAC-Policy-Wizard/app/Resources/policyTemplates/Recommended_UserMode_Blocklist.xml @@ -1,36 +1,49 @@  - 10.1.0.2 + 10.2.0.0 {A244370E-44C9-4C06-B551-F6016E563076} {2E07F7E4-194C-4D20-B7C9-6F44A6C5A234} + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -39,46 +52,46 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + @@ -329,7 +342,7 @@ - + @@ -717,6 +730,7 @@ + @@ -724,6 +738,7 @@ + @@ -738,6 +753,7 @@ + @@ -1400,7 +1416,7 @@ - 10.1.0.2 + 10.2.0.0 diff --git a/WDAC-Policy-Wizard/app/src/PolicyHelper.cs b/WDAC-Policy-Wizard/app/src/PolicyHelper.cs index 06c078b3..34a7094b 100644 --- a/WDAC-Policy-Wizard/app/src/PolicyHelper.cs +++ b/WDAC-Policy-Wizard/app/src/PolicyHelper.cs @@ -2045,6 +2045,18 @@ public static SiPolicy MergePolicies(SiPolicy tempPolicy, SiPolicy resultantPoli return resultantPolicy; } + // Dedupe template FileRules against the existing policy by content fingerprint (Hash, or + // FileName + version range, etc.) and capture a map of duplicate-template-IDs -> existing-IDs. + // This handles policies that were merged before the dedup fix existed: their recommended rules + // live under remapped IDs (e.g. ID_DENY_D_0) so an ID-only dedup would otherwise re-insert + // every template rule. Remap the template's FileRuleRefs to the existing IDs so the signing + // scenario merge can also dedup correctly. + var duplicateIdMap = BuildFileRuleDuplicateIdMap(tempPolicy.FileRules, resultantPolicy.FileRules); + if (duplicateIdMap.Count > 0) + { + RemapFileRuleRefIds(tempPolicy.SigningScenarios, duplicateIdMap); + } + // Handle Signing Scenario (AllowedSigners, DeniedSigners and FileRuleRefs) resultantPolicy.SigningScenarios = MergeSigningScenario(tempPolicy.SigningScenarios, resultantPolicy.SigningScenarios); @@ -2052,7 +2064,7 @@ public static SiPolicy MergePolicies(SiPolicy tempPolicy, SiPolicy resultantPoli resultantPolicy.Signers = MergeSigners(tempPolicy.Signers, resultantPolicy.Signers); // Handle File Rules - resultantPolicy.FileRules = MergeFileRules(tempPolicy.FileRules, resultantPolicy.FileRules); + resultantPolicy.FileRules = MergeFileRules(tempPolicy.FileRules, resultantPolicy.FileRules, duplicateIdMap); // Handle CiSigners if (tempPolicy.CiSigners != null && tempPolicy.CiSigners.Length > 0) @@ -2196,27 +2208,45 @@ static ProductSigners MergeProductSigners(ProductSigners newProductSigners, Prod } else // new and existing DeniedSigners { - int copySize = newProductSigners.FileRulesRef.FileRuleRef.Length - + resultProductSigners.FileRulesRef.FileRuleRef.Length; - FileRuleRef[] fileRuleRefCopy = new FileRuleRef[copySize]; + // Build a set of existing rule ref IDs to avoid duplicates when the same + // template policy is merged multiple times. + var existingRefIds = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var refEntry in resultProductSigners.FileRulesRef.FileRuleRef) + { + if (refEntry != null && !string.IsNullOrEmpty(refEntry.RuleID)) + { + existingRefIds.Add(refEntry.RuleID); + } + } - int newFileRuleRefLen = newProductSigners.FileRulesRef.FileRuleRef.Length; + var mergedRefs = new List( + newProductSigners.FileRulesRef.FileRuleRef.Length + + resultProductSigners.FileRulesRef.FileRuleRef.Length); - // New DeniedSigners - for (int i = 0; i < newFileRuleRefLen; i++) + // New FileRuleRefs first - skip any already present + for (int i = 0; i < newProductSigners.FileRulesRef.FileRuleRef.Length; i++) { - fileRuleRefCopy[i] = newProductSigners.FileRulesRef.FileRuleRef[i]; + var refEntry = newProductSigners.FileRulesRef.FileRuleRef[i]; + if (refEntry != null && !string.IsNullOrEmpty(refEntry.RuleID) + && existingRefIds.Contains(refEntry.RuleID)) + { + continue; + } + + mergedRefs.Add(refEntry); + if (refEntry != null && !string.IsNullOrEmpty(refEntry.RuleID)) + { + existingRefIds.Add(refEntry.RuleID); + } } - // Existing AllowedSigners + // Existing FileRuleRefs for (int i = 0; i < resultProductSigners.FileRulesRef.FileRuleRef.Length; i++) { - // Offset the index to length of new Prod signers to not overwrite entries - fileRuleRefCopy[i + newFileRuleRefLen] = resultProductSigners.FileRulesRef.FileRuleRef[i]; + mergedRefs.Add(resultProductSigners.FileRulesRef.FileRuleRef[i]); } - - resultProductSigners.FileRulesRef.FileRuleRef = fileRuleRefCopy; + resultProductSigners.FileRulesRef.FileRuleRef = mergedRefs.ToArray(); } } @@ -2270,7 +2300,8 @@ static Signer[] MergeSigners(Signer[] newSigners, Signer[] resultSigners) /// /// /// - static Object[] MergeFileRules(Object[] newFileRules, Object[] resultFileRules) + static Object[] MergeFileRules(Object[] newFileRules, Object[] resultFileRules, + Dictionary duplicateIdMap = null) { // Short circuit if nothing from the new sipolicy if (newFileRules == null || newFileRules.Length == 0) @@ -2284,23 +2315,193 @@ static Object[] MergeFileRules(Object[] newFileRules, Object[] resultFileRules) return newFileRules; } - int copySize = newFileRules.Length + resultFileRules.Length; - Object[] fileRulesCopy = new Object[copySize]; + // Build a set of existing rule IDs to prevent duplicate entries when the same + // template (e.g. Recommended Driver/User Mode Blocklist) is merged multiple times. + var existingIds = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var rule in resultFileRules) + { + string id = GetFileRuleId(rule); + if (!string.IsNullOrEmpty(id)) + { + existingIds.Add(id); + } + } + + var merged = new List(newFileRules.Length + resultFileRules.Length); - // New DeniedSigners + // New rules first - skip any whose ID is already present or that was matched to an + // existing rule by content fingerprint (legacy/remapped-ID case). for (int i = 0; i < newFileRules.Length; i++) { - fileRulesCopy[i] = newFileRules[i]; + string id = GetFileRuleId(newFileRules[i]); + if (!string.IsNullOrEmpty(id) && existingIds.Contains(id)) + { + continue; + } + + if (duplicateIdMap != null && !string.IsNullOrEmpty(id) && duplicateIdMap.ContainsKey(id)) + { + continue; + } + + merged.Add(newFileRules[i]); + if (!string.IsNullOrEmpty(id)) + { + existingIds.Add(id); + } } - // Existing AllowedSigners + // Existing rules for (int i = 0; i < resultFileRules.Length; i++) { - // Offset the index to length of new Prod signers to not overwrite entries - fileRulesCopy[i + newFileRules.Length] = resultFileRules[i]; + merged.Add(resultFileRules[i]); + } + + return merged.ToArray(); + } + + /// + /// Returns a map of template-rule IDs to existing-policy-rule IDs for any template FileRules whose + /// content matches a rule already in the resultant policy. Used to dedup recommended-blocklist + /// merges against policies whose rule IDs were remapped on a previous merge (legacy data). + /// + private static Dictionary BuildFileRuleDuplicateIdMap(Object[] newFileRules, Object[] resultFileRules) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (newFileRules == null || newFileRules.Length == 0 + || resultFileRules == null || resultFileRules.Length == 0) + { + return map; + } + + // Index existing rules by content fingerprint -> ID + var existingByFingerprint = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var rule in resultFileRules) + { + string fp = GetFileRuleFingerprint(rule); + string id = GetFileRuleId(rule); + if (!string.IsNullOrEmpty(fp) && !string.IsNullOrEmpty(id) && !existingByFingerprint.ContainsKey(fp)) + { + existingByFingerprint[fp] = id; + } + } + + if (existingByFingerprint.Count == 0) + { + return map; } - return fileRulesCopy; + foreach (var rule in newFileRules) + { + string fp = GetFileRuleFingerprint(rule); + string id = GetFileRuleId(rule); + if (string.IsNullOrEmpty(fp) || string.IsNullOrEmpty(id)) + { + continue; + } + + if (existingByFingerprint.TryGetValue(fp, out string existingId) + && !string.Equals(id, existingId, StringComparison.OrdinalIgnoreCase) + && !map.ContainsKey(id)) + { + map[id] = existingId; + } + } + + return map; + } + + /// + /// Rewrites FileRuleRef RuleIDs inside the supplied SigningScenarios using the provided template->existing + /// ID map so that the signing-scenario merge dedupes references that point at the same logical rule. + /// + private static void RemapFileRuleRefIds(SigningScenario[] scenarios, Dictionary idMap) + { + if (scenarios == null || idMap == null || idMap.Count == 0) + { + return; + } + + foreach (var scenario in scenarios) + { + var ps = scenario?.ProductSigners; + if (ps?.FileRulesRef?.FileRuleRef == null) + { + continue; + } + + foreach (var refEntry in ps.FileRulesRef.FileRuleRef) + { + if (refEntry != null && !string.IsNullOrEmpty(refEntry.RuleID) + && idMap.TryGetValue(refEntry.RuleID, out string remapped)) + { + refEntry.RuleID = remapped; + } + } + } + } + + /// + /// Builds an ID-independent fingerprint for a FileRule entry. Prefers Hash (most specific); otherwise + /// falls back to a composite of identifying attributes. Mirrors SigningRules_Control's fingerprint logic. + /// + private static string GetFileRuleFingerprint(Object rule) + { + switch (rule) + { + case Allow a: + return BuildFileRuleFingerprint("A", a.Hash, a.FileName, a.MinimumFileVersion, a.MaximumFileVersion, + a.InternalName, a.FileDescription, a.ProductName, a.FilePath, + a.PackageFamilyName); + case Deny d: + return BuildFileRuleFingerprint("D", d.Hash, d.FileName, d.MinimumFileVersion, d.MaximumFileVersion, + d.InternalName, d.FileDescription, d.ProductName, d.FilePath, + d.PackageFamilyName); + case FileRule f: + return BuildFileRuleFingerprint("F", f.Hash, f.FileName, f.MinimumFileVersion, f.MaximumFileVersion, + f.InternalName, f.FileDescription, f.ProductName, f.FilePath, + f.PackageFamilyName); + default: + return null; + } + } + + private static string BuildFileRuleFingerprint(string kind, byte[] hash, string fileName, string minVer, + string maxVer, string internalName, string fileDescription, + string productName, string filePath, string packageFamilyName) + { + if (hash != null && hash.Length > 0) + { + return kind + "|H|" + BitConverter.ToString(hash); + } + + return string.Join("|", new[] + { + kind, "M", + fileName ?? string.Empty, + minVer ?? string.Empty, + maxVer ?? string.Empty, + internalName ?? string.Empty, + fileDescription ?? string.Empty, + productName ?? string.Empty, + filePath ?? string.Empty, + packageFamilyName ?? string.Empty + }); + } + + /// + /// Returns the ID attribute of a FileRules entry regardless of its concrete type + /// (Allow, Deny, FileRule). + /// + private static string GetFileRuleId(Object rule) + { + switch (rule) + { + case Allow a: return a.ID; + case Deny d: return d.ID; + case FileRule f: return f.ID; + default: return null; + } } /// diff --git a/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs b/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs index 91a0078b..9c655b19 100644 --- a/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs +++ b/WDAC-Policy-Wizard/app/src/SigningRules_Control.cs @@ -587,18 +587,40 @@ private void SetBlocklistStates() this.checkBox_UserModeList.Visible = true; } + // When editing an existing policy, auto-check a "Merge with Recommended" box only when + // every rule from the bundled blocklist template is already present in the policy (1:1 match). + // The policy may include additional custom rules - we only verify the recommended set is intact. + bool isEdit = this._MainWindow.Policy.PolicyWorkflow == WDAC_Policy.Workflow.Edit; + // Recommended Kernel Driver Blocklist - if(Properties.Settings.Default.useDriverBlockRules) + bool kmFullyPresent = isEdit && IsRecommendedBlocklistFullyPresent("Recommended_Driver_Blocklist.xml"); + if (kmFullyPresent) + { + // Already in policy - reflect that and prevent the merge from running again on save + this.checkBox_KernelList.CheckedChanged -= CheckBox_KernelList_CheckedChanged; + this.checkBox_KernelList.Checked = true; + this.Policy.UseKernelModeBlocks = false; + this.checkBox_KernelList.CheckedChanged += CheckBox_KernelList_CheckedChanged; + } + else if (Properties.Settings.Default.useDriverBlockRules) { - this.checkBox_KernelList.Checked = true; + this.checkBox_KernelList.Checked = true; } else { - this.checkBox_KernelList.Checked = false; + this.checkBox_KernelList.Checked = false; } // Recommended User Mode Blocklist - if (Properties.Settings.Default.useUsermodeBlockRules) + bool umFullyPresent = isEdit && IsRecommendedBlocklistFullyPresent("Recommended_UserMode_Blocklist.xml"); + if (umFullyPresent) + { + this.checkBox_UserModeList.CheckedChanged -= CheckBox_UserModeList_CheckedChanged; + this.checkBox_UserModeList.Checked = true; + this.Policy.UseUserModeBlocks = false; + this.checkBox_UserModeList.CheckedChanged += CheckBox_UserModeList_CheckedChanged; + } + else if (Properties.Settings.Default.useUsermodeBlockRules) { this.checkBox_UserModeList.Checked = true; } @@ -608,6 +630,118 @@ private void SetBlocklistStates() } } + /// + /// Returns true only when every FileRule from the bundled recommended blocklist template is also + /// present in the policy currently loaded, compared by content fingerprint (Hash or FileName + + /// version range) rather than by ID. This is required because the merge pipeline remaps IDs + /// (e.g. ID_DENY_AGENT64_SHA1 -> ID_DENY_D_0) when collisions exist, so an ID-based comparison + /// would incorrectly report the recommended set as missing on re-edit. Additional non-template + /// rules in the policy are ignored. + /// + private bool IsRecommendedBlocklistFullyPresent(string templateFileName) + { + try + { + string templatePath = Path.Combine(this._MainWindow.ExeFolderPath, "Templates", templateFileName); + if (!File.Exists(templatePath)) + { + return false; + } + + SiPolicy templatePolicy = Helper.DeserializeXMLtoPolicy(templatePath); + if (templatePolicy?.FileRules == null || templatePolicy.FileRules.Length == 0) + { + return false; + } + + // Build set of FileRule content fingerprints currently in the policy + var policyFingerprints = new HashSet(StringComparer.OrdinalIgnoreCase); + if (this.Policy.siPolicy?.FileRules != null) + { + foreach (var rule in this.Policy.siPolicy.FileRules) + { + string fp = GetFileRuleFingerprint(rule); + if (!string.IsNullOrEmpty(fp)) + { + policyFingerprints.Add(fp); + } + } + } + + // Every template rule must have a content match in the policy for a 1:1 match + foreach (var rule in templatePolicy.FileRules) + { + string fp = GetFileRuleFingerprint(rule); + if (string.IsNullOrEmpty(fp)) + { + continue; + } + + if (!policyFingerprints.Contains(fp)) + { + return false; + } + } + + return true; + } + catch (Exception exp) + { + Logger.Log.AddErrorMsg("IsRecommendedBlocklistFullyPresent() encountered an exception for " + templateFileName, exp); + return false; + } + } + + /// + /// Builds an ID-independent fingerprint for a FileRule entry so rules can be matched after merge-time + /// ID remapping. Prefers Hash (most specific); otherwise falls back to a composite of the rule type + /// and identifying attributes (FileName, version range, InternalName, FileDescription, ProductName, + /// FilePath, PackageFamilyName). + /// + private static string GetFileRuleFingerprint(object rule) + { + switch (rule) + { + case Allow a: + return BuildFingerprint("A", a.Hash, a.FileName, a.MinimumFileVersion, a.MaximumFileVersion, + a.InternalName, a.FileDescription, a.ProductName, a.FilePath, + a.PackageFamilyName); + case Deny d: + return BuildFingerprint("D", d.Hash, d.FileName, d.MinimumFileVersion, d.MaximumFileVersion, + d.InternalName, d.FileDescription, d.ProductName, d.FilePath, + d.PackageFamilyName); + case FileRule f: + return BuildFingerprint("F", f.Hash, f.FileName, f.MinimumFileVersion, f.MaximumFileVersion, + f.InternalName, f.FileDescription, f.ProductName, f.FilePath, + f.PackageFamilyName); + default: + return null; + } + } + + private static string BuildFingerprint(string kind, byte[] hash, string fileName, string minVer, + string maxVer, string internalName, string fileDescription, + string productName, string filePath, string packageFamilyName) + { + if (hash != null && hash.Length > 0) + { + return kind + "|H|" + BitConverter.ToString(hash); + } + + return string.Join("|", new[] + { + kind, "M", + fileName ?? string.Empty, + minVer ?? string.Empty, + maxVer ?? string.Empty, + internalName ?? string.Empty, + fileDescription ?? string.Empty, + productName ?? string.Empty, + filePath ?? string.Empty, + packageFamilyName ?? string.Empty + }); + } + /// /// Method to set all of the MainWindow objects to the local instances of the Policy helper class objects. /// From 6941ab4cc0e220643af4903bda2f7c13fe78d2d0 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Thu, 4 Jun 2026 09:14:58 -0400 Subject: [PATCH 11/16] =?UTF-8?q?=E2=80=A2=20Added=20Button=5FBack=5FClick?= =?UTF-8?q?=20handler=20that:=20=E2=80=A2=20No-ops=20on=20the=20home=20pag?= =?UTF-8?q?e=20or=20first=20workflow=20page.=20=E2=80=A2=20Confirms=20aban?= =?UTF-8?q?doning=20an=20in-progress=20custom=20rule.=20=E2=80=A2=20Clears?= =?UTF-8?q?=20ErrorOnPage/ErrorMsg,=20decrements=20CurrentPage,=20re-shows?= =?UTF-8?q?=20the=20Next=20button=20(in=20case=20the=20previous=20page=20h?= =?UTF-8?q?id=20it,=20e.g.=20build=20page),=20and=20re-runs=20PageControll?= =?UTF-8?q?er=20so=20the=20existing=20user=20control=20is=20brought=20back?= =?UTF-8?q?=20to=20front=20(existing=20pages=20are=20reused=20via=20PageLi?= =?UTF-8?q?st,=20so=20user=20state=20on=20those=20pages=20is=20preserved).?= =?UTF-8?q?=20=E2=80=A2=20Added=20UpdateBackButtonVisibility()=20that=20sh?= =?UTF-8?q?ows=20the=20Back=20button=20only=20when=20Next=20is=20visible?= =?UTF-8?q?=20and=20CurrentPage=20>=201.=20Called=20at=20the=20end=20of=20?= =?UTF-8?q?ShowControlPanel=20so=20visibility=20is=20updated=20on=20every?= =?UTF-8?q?=20navigation.=20=E2=80=A2=20Added=20button=5FBack=20to=20Contr?= =?UTF-8?q?ols=20and=20brought=20it=20to=20front=20inside=20ShowControlPan?= =?UTF-8?q?el.=20=E2=80=A2=20Hid=20button=5FBack=20when=20Home=5FButton=5F?= =?UTF-8?q?Click=20and=20Settings=5FButton=5FClick=20hide=20button=5FNext.?= =?UTF-8?q?=20=E2=80=A2=20Extended=20SetNextButtonUI()=20to=20apply=20matc?= =?UTF-8?q?hing=20dark/light=20styling=20to=20button=5FBack.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/src/MainForm.Designer.cs | 15 ++++ WDAC-Policy-Wizard/app/src/MainForm.cs | 80 ++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs b/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs index 9e654d47..32e4c730 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs @@ -50,6 +50,7 @@ private void InitializeComponent() home_Button = new System.Windows.Forms.Button(); settings_Button = new System.Windows.Forms.Button(); button_Next = new System.Windows.Forms.Button(); + button_Back = new System.Windows.Forms.Button(); label1 = new System.Windows.Forms.Label(); label2 = new System.Windows.Forms.Label(); label3 = new System.Windows.Forms.Label(); @@ -351,6 +352,18 @@ private void InitializeComponent() button_Next.Visible = false; button_Next.Click += Button_Next_Click; // + // button_Back + // + button_Back.Location = new System.Drawing.Point(1035, 663); + button_Back.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + button_Back.Name = "button_Back"; + button_Back.Size = new System.Drawing.Size(93, 33); + button_Back.TabIndex = 36; + button_Back.Text = "Back"; + button_Back.UseVisualStyleBackColor = true; + button_Back.Visible = false; + button_Back.Click += Button_Back_Click; + // // label1 // label1.AutoSize = true; @@ -410,6 +423,7 @@ private void InitializeComponent() Controls.Add(label2); Controls.Add(label1); Controls.Add(button_Next); + Controls.Add(button_Back); Controls.Add(control_Panel); Controls.Add(label_Welcome); Controls.Add(button_Merge); @@ -443,6 +457,7 @@ private void InitializeComponent() private System.Windows.Forms.Button settings_Button; private System.Windows.Forms.Button home_Button; private System.Windows.Forms.Button button_Next; + private System.Windows.Forms.Button button_Back; private System.Windows.Forms.Label label1; private System.Windows.Forms.Panel controlHighlight_Panel; private System.Windows.Forms.Button page3_Button; diff --git a/WDAC-Policy-Wizard/app/src/MainForm.cs b/WDAC-Policy-Wizard/app/src/MainForm.cs index 6fe50737..a3e59ce5 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.cs @@ -196,6 +196,7 @@ private void Button_Merge_Click(object sender, EventArgs e) private void Home_Button_Click(object sender, EventArgs e) { this.button_Next.Visible = false; + this.button_Back.Visible = false; // If the CustomRules Panel is open, close it if (this.CustomRuleinProgress && this._SigningRulesControl != null) @@ -237,6 +238,7 @@ private void Settings_Button_Click(object sender, EventArgs e) { Logger.Log.AddInfoMsg("Workflow -- Settings Button Selected"); this.button_Next.Visible = false; + this.button_Back.Visible = false; this.CurrentPage = 99; var _SettingsPage = new SettingsPage(this); @@ -272,11 +274,67 @@ private void Button_Next_Click(object sender, EventArgs e) this.CurrentPage++; PageController(sender, e); } - + else DisplayInfoText(99); } - + + /// + /// Controls the PageController method when the user presses the Back button. + /// Navigates to the previous page in the workflow without rebuilding it. + /// + private void Button_Back_Click(object sender, EventArgs e) + { + // Cannot go back from home page or first workflow page + if (this.CurrentPage <= 1) + { + return; + } + + // If a custom rule is in progress, confirm abandoning it + if (this.CustomRuleinProgress) + { + DialogResult res = MessageBox.Show("Do you want to abandon this custom rule and go back?", + "Confirmation", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question); + + if (res == DialogResult.No) + { + return; + } + + this.CustomRuleinProgress = false; + if (this._SigningRulesControl != null) + { + this._SigningRulesControl.CloseCustomRulesPanel(); + } + } + + // Clear any error state from the page we are leaving + this.ErrorOnPage = false; + this.ErrorMsg = string.Empty; + + // Reuse existing pages instead of recreating them + this.RedoFlowRequired = false; + + this.CurrentPage--; + + // Re-show the Next button in case the previous page hid it (e.g. build page) + this.button_Next.Visible = true; + + PageController(sender, e); + UpdateBackButtonVisibility(); + } + + /// + /// Updates the visibility of the Back button based on the current page in the workflow. + /// + private void UpdateBackButtonVisibility() + { + this.button_Back.Visible = this.button_Next.Visible && this.CurrentPage > 1; + } + /// /// /// @@ -1787,6 +1845,8 @@ private void ShowControlPanel(object sender, EventArgs e) button_Next.BringToFront(); button_Next.Focus(); + this.Controls.Add(button_Back); + button_Back.BringToFront(); // Set highlight panel location int X_OFFSET = 15; int Y_OFFSET = 5; @@ -2008,6 +2068,8 @@ private void ShowControlPanel(object sender, EventArgs e) controlHighlight_Panel.Location = new System.Drawing.Point(this.page5_Button.Location.X - X_OFFSET, this.page5_Button.Location.Y + Y_OFFSET); break; } + + UpdateBackButtonVisibility(); } /// @@ -2142,6 +2204,13 @@ public void SetNextButtonUI() button_Next.FlatStyle = System.Windows.Forms.FlatStyle.Flat; button_Next.ForeColor = System.Drawing.Color.DodgerBlue; button_Next.BackColor = System.Drawing.Color.Transparent; + + button_Back.FlatAppearance.BorderColor = System.Drawing.Color.DodgerBlue; + button_Back.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(50, 30, 144, 255); + button_Back.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(50, 30, 144, 255); + button_Back.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + button_Back.ForeColor = System.Drawing.Color.DodgerBlue; + button_Back.BackColor = System.Drawing.Color.Transparent; } // Light Mode @@ -2153,6 +2222,13 @@ public void SetNextButtonUI() button_Next.FlatStyle = System.Windows.Forms.FlatStyle.Flat; button_Next.ForeColor = System.Drawing.Color.Black; button_Next.BackColor = System.Drawing.Color.WhiteSmoke; + + button_Back.FlatAppearance.BorderColor = System.Drawing.Color.Black; + button_Back.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(50, 30, 144, 255); + button_Back.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(50, 30, 144, 255); + button_Back.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + button_Back.ForeColor = System.Drawing.Color.Black; + button_Back.BackColor = System.Drawing.Color.WhiteSmoke; } } From ec6bf7ee69894c046d5706b384761d5c05a96115 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Thu, 4 Jun 2026 13:22:18 -0400 Subject: [PATCH 12/16] 1. After the PowerShell merge, deserialize the result, reset the policy GUID, and deduplicate rules (signers and file rules with identical content but different IDs). 2. Re-serialize. --- WDAC-Policy-Wizard/app/src/MainForm.cs | 23 ++ WDAC-Policy-Wizard/app/src/PolicyHelper.cs | 314 +++++++++++++++++++++ 2 files changed, 337 insertions(+) diff --git a/WDAC-Policy-Wizard/app/src/MainForm.cs b/WDAC-Policy-Wizard/app/src/MainForm.cs index 6fe50737..6814b27b 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.cs @@ -1699,6 +1699,29 @@ public void MergePolicies_MergeControl(BackgroundWorker worker) } PSCmdlets.MergePolicies(this.Policy.PoliciesToMerge, this.Policy.SchemaPath); + + // Post-merge processing: + // 1. Generate a new Policy GUID (best practice when policy contents change) + // 2. Remove duplicate rules that exist across the input policies + try + { + if (File.Exists(this.Policy.SchemaPath)) + { + SiPolicy mergedPolicy = Helper.DeserializeXMLtoPolicy(this.Policy.SchemaPath); + if (mergedPolicy != null) + { + mergedPolicy = PolicyHelper.DeduplicateRules(mergedPolicy); + PolicyHelper.ResetPolicyGuid(mergedPolicy); + Helper.SerializePolicytoXML(mergedPolicy, this.Policy.SchemaPath); + Logger.Log.AddInfoMsg("Merged policy post-processed: deduplicated rules and reset Policy GUID."); + } + } + } + catch (Exception ex) + { + Logger.Log.AddErrorMsg("Exception during merged policy post-processing", ex); + } + worker.ReportProgress(90); } diff --git a/WDAC-Policy-Wizard/app/src/PolicyHelper.cs b/WDAC-Policy-Wizard/app/src/PolicyHelper.cs index 06c078b3..3c0680bb 100644 --- a/WDAC-Policy-Wizard/app/src/PolicyHelper.cs +++ b/WDAC-Policy-Wizard/app/src/PolicyHelper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data; +using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; using System.Runtime.InteropServices.Marshalling; @@ -2382,6 +2383,319 @@ static EKU[] MergeEKUs(EKU[] newEKU, EKU[] resultEKU) return ekuCopy; } + /// + /// Removes duplicate Signers, FileRules (Allow/Deny/FileAttrib) and EKUs from a merged policy. + /// Two rules are considered duplicates if their content (excluding ID) is identical. + /// References (FileRuleRef, AllowedSigner, DeniedSigner, CiSigner, FileAttribRef, etc.) are + /// remapped to point to the surviving rule, and duplicate references are themselves dropped. + /// + /// + /// + public static SiPolicy DeduplicateRules(SiPolicy siPolicy) + { + if (siPolicy == null) + { + return siPolicy; + } + + // 1) Dedupe FileRules and build idMapping (oldId -> survivingId) + Dictionary fileRuleIdMap = new Dictionary(); + if (siPolicy.FileRules != null && siPolicy.FileRules.Length > 0) + { + Dictionary seen = new Dictionary(); + List deduped = new List(); + + foreach (var rule in siPolicy.FileRules) + { + string key = GetFileRuleKey(rule); + string id = GetFileRuleId(rule); + if (id == null) + { + deduped.Add(rule); + continue; + } + + if (key != null && seen.TryGetValue(key, out string survivorId)) + { + // Duplicate; map to survivor + fileRuleIdMap[id] = survivorId; + Logger.Log.AddInfoMsg($"Dedupe: dropping duplicate FileRule {id}, mapping to {survivorId}"); + } + else + { + if (key != null) + { + seen[key] = id; + } + deduped.Add(rule); + } + } + + siPolicy.FileRules = deduped.ToArray(); + siPolicy = UpdateSiPolicyFileRuleIDs(siPolicy, fileRuleIdMap); + } + + // 2) Dedupe Signers and build idMapping + Dictionary signerIdMap = new Dictionary(); + if (siPolicy.Signers != null && siPolicy.Signers.Length > 0) + { + Dictionary seen = new Dictionary(); + List deduped = new List(); + + foreach (var signer in siPolicy.Signers) + { + string key = GetSignerKey(signer); + if (signer.ID == null) + { + deduped.Add(signer); + continue; + } + + if (key != null && seen.TryGetValue(key, out string survivorId)) + { + signerIdMap[signer.ID] = survivorId; + Logger.Log.AddInfoMsg($"Dedupe: dropping duplicate Signer {signer.ID}, mapping to {survivorId}"); + } + else + { + if (key != null) + { + seen[key] = signer.ID; + } + deduped.Add(signer); + } + } + + siPolicy.Signers = deduped.ToArray(); + siPolicy = UpdateSiPolicySignerIDs(siPolicy, signerIdMap); + } + + // 3) Dedupe references (now-redundant) inside SigningScenarios and CiSigners/UpdatePolicySigners/SupplementalPolicySigners + DedupeReferences(siPolicy); + + // 4) Dedupe EKUs by Value+FriendlyName + if (siPolicy.EKUs != null && siPolicy.EKUs.Length > 0) + { + Dictionary seen = new Dictionary(); + List deduped = new List(); + Dictionary ekuMap = new Dictionary(); + + foreach (var eku in siPolicy.EKUs) + { + string key = GetEkuKey(eku); + if (eku.ID == null) + { + deduped.Add(eku); + continue; + } + + if (key != null && seen.TryGetValue(key, out EKU survivor)) + { + ekuMap[eku.ID] = survivor.ID; + } + else + { + if (key != null) + { + seen[key] = eku; + } + deduped.Add(eku); + } + } + + siPolicy.EKUs = deduped.ToArray(); + + // Update Signer.CertEKU references (CertEKU.ID points to EKU.ID) + if (ekuMap.Count > 0 && siPolicy.Signers != null) + { + foreach (var signer in siPolicy.Signers) + { + if (signer.CertEKU == null) continue; + foreach (var certEku in signer.CertEKU) + { + if (certEku != null && certEku.ID != null && ekuMap.TryGetValue(certEku.ID, out string newId)) + { + certEku.ID = newId; + } + } + } + } + } + + return siPolicy; + } + + /// + /// Removes duplicate FileRuleRef, AllowedSigner, DeniedSigner, CiSigner, SupplementalPolicySigner, + /// UpdatePolicySigner, FileAttribRef entries that may exist after merging two policies. + /// + private static void DedupeReferences(SiPolicy siPolicy) + { + // Signing Scenarios + if (siPolicy.SigningScenarios != null) + { + foreach (var scn in siPolicy.SigningScenarios) + { + if (scn.ProductSigners == null) continue; + + if (scn.ProductSigners.FileRulesRef?.FileRuleRef != null) + { + scn.ProductSigners.FileRulesRef.FileRuleRef = scn.ProductSigners.FileRulesRef.FileRuleRef + .GroupBy(r => r.RuleID ?? string.Empty) + .Select(g => g.First()) + .ToArray(); + } + + if (scn.ProductSigners.AllowedSigners?.AllowedSigner != null) + { + scn.ProductSigners.AllowedSigners.AllowedSigner = scn.ProductSigners.AllowedSigners.AllowedSigner + .GroupBy(s => s.SignerId ?? string.Empty) + .Select(g => g.First()) + .ToArray(); + } + + if (scn.ProductSigners.DeniedSigners?.DeniedSigner != null) + { + scn.ProductSigners.DeniedSigners.DeniedSigner = scn.ProductSigners.DeniedSigners.DeniedSigner + .GroupBy(s => s.SignerId ?? string.Empty) + .Select(g => g.First()) + .ToArray(); + } + } + } + + // CiSigners + if (siPolicy.CiSigners != null) + { + siPolicy.CiSigners = siPolicy.CiSigners + .GroupBy(s => s.SignerId ?? string.Empty) + .Select(g => g.First()) + .ToArray(); + } + + // UpdatePolicySigners + if (siPolicy.UpdatePolicySigners != null) + { + siPolicy.UpdatePolicySigners = siPolicy.UpdatePolicySigners + .GroupBy(s => s.SignerId ?? string.Empty) + .Select(g => g.First()) + .ToArray(); + } + + // SupplementalPolicySigners + if (siPolicy.SupplementalPolicySigners != null) + { + siPolicy.SupplementalPolicySigners = siPolicy.SupplementalPolicySigners + .GroupBy(s => s.SignerId ?? string.Empty) + .Select(g => g.First()) + .ToArray(); + } + + // Signer.FileAttribRef + if (siPolicy.Signers != null) + { + foreach (var signer in siPolicy.Signers) + { + if (signer.FileAttribRef != null) + { + signer.FileAttribRef = signer.FileAttribRef + .GroupBy(r => r.RuleID ?? string.Empty) + .Select(g => g.First()) + .ToArray(); + } + } + } + } + + private static string GetFileRuleId(object rule) + { + if (rule is Allow a) return a.ID; + if (rule is Deny d) return d.ID; + if (rule is FileAttrib f) return f.ID; + if (rule is FileRule fr) return fr.ID; + return null; + } + + /// + /// Builds a content-based key for a FileRule (Allow/Deny/FileAttrib/FileRule) excluding ID + /// so duplicates with different IDs collapse into one entry. + /// + private static string GetFileRuleKey(object rule) + { + if (rule is Allow a) + { + return string.Join("|", + "Allow", + a.FileName, a.InternalName, a.FileDescription, a.ProductName, + a.PackageFamilyName, a.PackageVersion, + a.MinimumFileVersion, a.MaximumFileVersion, + HashToString(a.Hash), a.AppIDs, a.FilePath); + } + if (rule is Deny d) + { + return string.Join("|", + "Deny", + d.FileName, d.InternalName, d.FileDescription, d.ProductName, + d.PackageFamilyName, d.PackageVersion, + d.MinimumFileVersion, d.MaximumFileVersion, + HashToString(d.Hash), d.AppIDs, d.FilePath); + } + if (rule is FileAttrib f) + { + return string.Join("|", + "FileAttrib", + f.FileName, f.InternalName, f.FileDescription, f.ProductName, + f.PackageFamilyName, f.PackageVersion, + f.MinimumFileVersion, f.MaximumFileVersion, + HashToString(f.Hash), f.AppIDs, f.FilePath); + } + return null; + } + + /// + /// Builds a content-based key for a Signer excluding ID. + /// + private static string GetSignerKey(Signer signer) + { + if (signer == null) return null; + + string certRoot = signer.CertRoot != null + ? $"{signer.CertRoot.Type}:{HashToString(signer.CertRoot.Value)}" + : string.Empty; + string certPub = signer.CertPublisher?.Value ?? string.Empty; + string certIss = signer.CertIssuer?.Value ?? string.Empty; + string certOem = signer.CertOemID?.Value ?? string.Empty; + + string certEkus = string.Empty; + if (signer.CertEKU != null) + { + certEkus = string.Join(",", signer.CertEKU.Select(e => e?.ID ?? string.Empty).OrderBy(s => s)); + } + + string fileAttribs = string.Empty; + if (signer.FileAttribRef != null) + { + fileAttribs = string.Join(",", signer.FileAttribRef.Select(r => r?.RuleID ?? string.Empty).OrderBy(s => s)); + } + + string signTime = signer.SignTimeAfterSpecified ? signer.SignTimeAfter.ToString("o") : string.Empty; + + return string.Join("|", "Signer", signer.Name ?? string.Empty, + certRoot, certPub, certIss, certOem, certEkus, fileAttribs, signTime); + } + + private static string GetEkuKey(EKU eku) + { + if (eku == null) return null; + return $"EKU|{HashToString(eku.Value)}|{eku.FriendlyName ?? string.Empty}"; + } + + private static string HashToString(byte[] data) + { + if (data == null || data.Length == 0) return string.Empty; + return BitConverter.ToString(data); + } + /// /// Resets the Policy ID for base and supplemental policies and BasePolicy ID, for base policies /// From a463c01a8341d0493207be9922d37582f9c15482 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Thu, 4 Jun 2026 17:33:27 -0400 Subject: [PATCH 13/16] add anchor for back button --- WDAC-Policy-Wizard/app/src/MainForm.Designer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs b/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs index c3918360..108c994a 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs @@ -363,6 +363,7 @@ private void InitializeComponent() button_Back.Name = "button_Back"; button_Back.Size = new System.Drawing.Size(93, 33); button_Back.TabIndex = 36; + button_Back.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; button_Back.Text = "Back"; button_Back.UseVisualStyleBackColor = true; button_Back.Visible = false; From 93c39d5561838454d65ba2942060dd2a640f6881 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Thu, 4 Jun 2026 17:45:51 -0400 Subject: [PATCH 14/16] update merge --- WDAC-Policy-Wizard/app/src/PolicyHelper.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/WDAC-Policy-Wizard/app/src/PolicyHelper.cs b/WDAC-Policy-Wizard/app/src/PolicyHelper.cs index cd15b666..6de372f8 100644 --- a/WDAC-Policy-Wizard/app/src/PolicyHelper.cs +++ b/WDAC-Policy-Wizard/app/src/PolicyHelper.cs @@ -2490,21 +2490,6 @@ private static string BuildFileRuleFingerprint(string kind, byte[] hash, string }); } - /// - /// Returns the ID attribute of a FileRules entry regardless of its concrete type - /// (Allow, Deny, FileRule). - /// - private static string GetFileRuleId(Object rule) - { - switch (rule) - { - case Allow a: return a.ID; - case Deny d: return d.ID; - case FileRule f: return f.ID; - default: return null; - } - } - /// /// Handles the merge operation between a new Signer[] and an existing Signer[] struct /// From 431783bdedd728001167b888a73785a927acba69 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Fri, 12 Jun 2026 10:19:57 -0400 Subject: [PATCH 15/16] Comparer Policy feature --- .../app/Properties/Resources.Designer.cs | 10 + .../app/Properties/Resources.resx | 3 + .../app/Resources/imgs/compare.png | Bin 0 -> 642 bytes WDAC-Policy-Wizard/app/WDAC Wizard.csproj | 1 + .../app/src/MainForm.Designer.cs | 73 +- WDAC-Policy-Wizard/app/src/MainForm.cs | 27 + .../app/src/PolicyCompareReport.cs | 323 ++++++++ .../app/src/PolicyCompare_Form.Designer.cs | 391 +++++++++ .../app/src/PolicyCompare_Form.cs | 784 ++++++++++++++++++ WDAC-Policy-Wizard/app/src/PolicyComparer.cs | 617 ++++++++++++++ 10 files changed, 2211 insertions(+), 18 deletions(-) create mode 100644 WDAC-Policy-Wizard/app/Resources/imgs/compare.png create mode 100644 WDAC-Policy-Wizard/app/src/PolicyCompareReport.cs create mode 100644 WDAC-Policy-Wizard/app/src/PolicyCompare_Form.Designer.cs create mode 100644 WDAC-Policy-Wizard/app/src/PolicyCompare_Form.cs create mode 100644 WDAC-Policy-Wizard/app/src/PolicyComparer.cs diff --git a/WDAC-Policy-Wizard/app/Properties/Resources.Designer.cs b/WDAC-Policy-Wizard/app/Properties/Resources.Designer.cs index b3000e73..01f904b0 100644 --- a/WDAC-Policy-Wizard/app/Properties/Resources.Designer.cs +++ b/WDAC-Policy-Wizard/app/Properties/Resources.Designer.cs @@ -865,6 +865,16 @@ internal static System.Drawing.Bitmap merge { return ((System.Drawing.Bitmap)(obj)); } } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap compare { + get { + object obj = ResourceManager.GetObject("compare", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } /// /// Looks up a localized string similar to AllowMicrosoft - SingleFormat.xml. diff --git a/WDAC-Policy-Wizard/app/Properties/Resources.resx b/WDAC-Policy-Wizard/app/Properties/Resources.resx index 47072e86..4012751b 100644 --- a/WDAC-Policy-Wizard/app/Properties/Resources.resx +++ b/WDAC-Policy-Wizard/app/Properties/Resources.resx @@ -121,6 +121,9 @@ ..\Resources\imgs\merge.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\imgs\compare.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\imgs\next.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/WDAC-Policy-Wizard/app/Resources/imgs/compare.png b/WDAC-Policy-Wizard/app/Resources/imgs/compare.png new file mode 100644 index 0000000000000000000000000000000000000000..cc635d2dbad881fd9db697fb47b523f549d8056a GIT binary patch literal 642 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3{2vlE{-7;ac^fW^wV|}X>(Va=po10>GDa! zL+(Latigi{5f!1%9f~KqZofNW$0XF)a7J&!v-fg0=KX!P>a(i)+4+9^>u+U-=gbNc zU~z;XgGT$)%~witvv#}xe{%cElf(ZXxoXbN`D|@|`tqu{IcoQ7rkljxvuo6xEG|8J z@9I}aCA->u&pkXHEN5=~_UJ;cD~oS(KQ%Blq5@9+wkbUI%dkJ~RQF=mE1n?1 zkjAQ8SA#|M`2sII*?gt!wg35D_6N=OFFideyuS3OpTO_m6L+%yoqhku+r0D9yQ1V? zo>nt1uitO9{Xx}6`#C#4@@DL6-It>N?A`ltkUd*h*N1{_+P?qTH!8c+qHDldBj?}Zyqlq`@Avb$t7gWo>kH3V|4jYxq#YRg-S2*UUNS?oyd$e>wZWZhyYBz{ qQa<-;<*vB*q2Jk{5h&2}fpMz99=rZo^>M&-!{F)a=d#Wzp$Pz`LMvJT literal 0 HcmV?d00001 diff --git a/WDAC-Policy-Wizard/app/WDAC Wizard.csproj b/WDAC-Policy-Wizard/app/WDAC Wizard.csproj index 2b7d294f..13c9aec7 100644 --- a/WDAC-Policy-Wizard/app/WDAC Wizard.csproj +++ b/WDAC-Policy-Wizard/app/WDAC Wizard.csproj @@ -195,6 +195,7 @@ + diff --git a/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs b/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs index 9e654d47..3418107e 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs @@ -39,6 +39,7 @@ private void InitializeComponent() backgroundWorker1 = new System.ComponentModel.BackgroundWorker(); button_Edit = new System.Windows.Forms.Button(); button_Merge = new System.Windows.Forms.Button(); + button_Compare = new System.Windows.Forms.Button(); control_Panel = new System.Windows.Forms.Panel(); workflow_Label = new System.Windows.Forms.Label(); page5_Button = new System.Windows.Forms.Button(); @@ -54,6 +55,7 @@ private void InitializeComponent() label2 = new System.Windows.Forms.Label(); label3 = new System.Windows.Forms.Label(); label4 = new System.Windows.Forms.Label(); + label5 = new System.Windows.Forms.Label(); control_Panel.SuspendLayout(); SuspendLayout(); // @@ -78,10 +80,10 @@ private void InitializeComponent() button_New.FlatStyle = System.Windows.Forms.FlatStyle.Flat; button_New.Font = new System.Drawing.Font("Tahoma", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0); button_New.Image = Properties.Resources.newPolicy; - button_New.Location = new System.Drawing.Point(328, 225); + button_New.Location = new System.Drawing.Point(215, 225); button_New.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); button_New.Name = "button_New"; - button_New.Size = new System.Drawing.Size(195, 217); + button_New.Size = new System.Drawing.Size(165, 217); button_New.TabIndex = 10; button_New.Text = "Policy Creator"; button_New.TextAlign = System.Drawing.ContentAlignment.BottomCenter; @@ -118,10 +120,10 @@ private void InitializeComponent() button_Edit.FlatStyle = System.Windows.Forms.FlatStyle.Flat; button_Edit.Font = new System.Drawing.Font("Tahoma", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0); button_Edit.Image = Properties.Resources.tools; - button_Edit.Location = new System.Drawing.Point(567, 225); + button_Edit.Location = new System.Drawing.Point(425, 225); button_Edit.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); button_Edit.Name = "button_Edit"; - button_Edit.Size = new System.Drawing.Size(195, 217); + button_Edit.Size = new System.Drawing.Size(165, 217); button_Edit.TabIndex = 25; button_Edit.Text = "Policy Editor"; button_Edit.TextAlign = System.Drawing.ContentAlignment.BottomCenter; @@ -137,16 +139,35 @@ private void InitializeComponent() button_Merge.FlatStyle = System.Windows.Forms.FlatStyle.Flat; button_Merge.Font = new System.Drawing.Font("Tahoma", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0); button_Merge.Image = Properties.Resources.merge; - button_Merge.Location = new System.Drawing.Point(813, 225); + button_Merge.Location = new System.Drawing.Point(635, 225); button_Merge.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); button_Merge.Name = "button_Merge"; - button_Merge.Size = new System.Drawing.Size(195, 217); + button_Merge.Size = new System.Drawing.Size(165, 217); button_Merge.TabIndex = 26; button_Merge.Text = "Policy Merger"; button_Merge.TextAlign = System.Drawing.ContentAlignment.BottomCenter; button_Merge.UseVisualStyleBackColor = false; button_Merge.Click += Button_Merge_Click; // + // button_Compare + // + button_Compare.BackColor = System.Drawing.Color.Transparent; + button_Compare.FlatAppearance.BorderSize = 0; + button_Compare.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(50, 30, 144, 255); + button_Compare.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(50, 30, 144, 255); + button_Compare.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + button_Compare.Font = new System.Drawing.Font("Tahoma", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0); + button_Compare.Image = Properties.Resources.compare; + button_Compare.Location = new System.Drawing.Point(845, 225); + button_Compare.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + button_Compare.Name = "button_Compare"; + button_Compare.Size = new System.Drawing.Size(165, 217); + button_Compare.TabIndex = 27; + button_Compare.Text = "Policy Comparer"; + button_Compare.TextAlign = System.Drawing.ContentAlignment.BottomCenter; + button_Compare.UseVisualStyleBackColor = false; + button_Compare.Click += Button_Compare_Click; + // // control_Panel // control_Panel.BackColor = System.Drawing.Color.FromArgb(230, 230, 230); @@ -365,39 +386,51 @@ private void InitializeComponent() // // label2 // - label2.AutoSize = true; label2.BackColor = System.Drawing.Color.Transparent; label2.Font = new System.Drawing.Font("Tahoma", 9F); - label2.Location = new System.Drawing.Point(296, 453); + label2.Location = new System.Drawing.Point(198, 453); label2.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); label2.Name = "label2"; - label2.Size = new System.Drawing.Size(244, 36); + label2.Size = new System.Drawing.Size(200, 50); label2.TabIndex = 33; - label2.Text = "Create a new base or supplemental \r\npolicy"; + label2.Text = "Create a new base or\r\nsupplemental policy"; + label2.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; // // label3 // - label3.AutoSize = true; label3.BackColor = System.Drawing.Color.Transparent; label3.Font = new System.Drawing.Font("Tahoma", 9F); - label3.Location = new System.Drawing.Point(563, 453); + label3.Location = new System.Drawing.Point(408, 453); label3.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); label3.Name = "label3"; - label3.Size = new System.Drawing.Size(218, 36); + label3.Size = new System.Drawing.Size(200, 50); label3.TabIndex = 34; - label3.Text = "Edit an existing policy on disk or \r\nconvert event logs to a policy"; + label3.Text = "Edit an existing policy or\r\nconvert event logs to a policy"; + label3.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; // // label4 // - label4.AutoSize = true; label4.BackColor = System.Drawing.Color.Transparent; label4.Font = new System.Drawing.Font("Tahoma", 9F); - label4.Location = new System.Drawing.Point(802, 453); + label4.Location = new System.Drawing.Point(618, 453); label4.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); label4.Name = "label4"; - label4.Size = new System.Drawing.Size(236, 18); + label4.Size = new System.Drawing.Size(200, 50); label4.TabIndex = 35; - label4.Text = "Merge two existing policies into one\r\n"; + label4.Text = "Merge two existing\r\npolicies into one"; + label4.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // label5 + // + label5.BackColor = System.Drawing.Color.Transparent; + label5.Font = new System.Drawing.Font("Tahoma", 9F); + label5.Location = new System.Drawing.Point(828, 453); + label5.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + label5.Name = "label5"; + label5.Size = new System.Drawing.Size(200, 50); + label5.TabIndex = 36; + label5.Text = "Compare two or more policies\r\nand view their differences"; + label5.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; // // MainWindow // @@ -405,6 +438,7 @@ private void InitializeComponent() AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; BackColor = System.Drawing.Color.White; ClientSize = new System.Drawing.Size(1232, 703); + Controls.Add(label5); Controls.Add(label4); Controls.Add(label3); Controls.Add(label2); @@ -412,6 +446,7 @@ private void InitializeComponent() Controls.Add(button_Next); Controls.Add(control_Panel); Controls.Add(label_Welcome); + Controls.Add(button_Compare); Controls.Add(button_Merge); Controls.Add(button_Edit); Controls.Add(label_Info); @@ -439,6 +474,7 @@ private void InitializeComponent() private System.Windows.Forms.Button button_New; private System.Windows.Forms.Button button_Edit; private System.Windows.Forms.Button button_Merge; + private System.Windows.Forms.Button button_Compare; private System.Windows.Forms.Panel control_Panel; private System.Windows.Forms.Button settings_Button; private System.Windows.Forms.Button home_Button; @@ -453,6 +489,7 @@ private void InitializeComponent() private System.Windows.Forms.Label label2; private System.Windows.Forms.Label label3; private System.Windows.Forms.Label label4; + private System.Windows.Forms.Label label5; private System.Windows.Forms.Label workflow_Label; } } diff --git a/WDAC-Policy-Wizard/app/src/MainForm.cs b/WDAC-Policy-Wizard/app/src/MainForm.cs index 6fe50737..83b2fde1 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.cs @@ -186,6 +186,33 @@ private void Button_Merge_Click(object sender, EventArgs e) } } + /// + /// Compare policy button selected: opens a separate window where the user can load 2 or + /// more policies (XML or binary) and view their differences side-by-side. The comparison + /// flow is non-destructive and does not affect the active wizard workflow state. + /// + private void Button_Compare_Click(object sender, EventArgs e) + { + Logger.Log.AddNewSeparationLine("Workflow -- Compare Policies Selected"); + + try + { + using (var compareForm = new PolicyCompare_Form()) + { + compareForm.ShowDialog(this); + } + } + catch (Exception ex) + { + Logger.Log.AddErrorMsg("Button_Compare_Click caught the following exception", ex); + MessageBox.Show(this, + "Unable to open the Policy Compare window: " + ex.Message, + "Policy Compare Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + } + // ##################### // CONTROL PANEL CONTROLS // ##################### diff --git a/WDAC-Policy-Wizard/app/src/PolicyCompareReport.cs b/WDAC-Policy-Wizard/app/src/PolicyCompareReport.cs new file mode 100644 index 00000000..a2d6459b --- /dev/null +++ b/WDAC-Policy-Wizard/app/src/PolicyCompareReport.cs @@ -0,0 +1,323 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace WDAC_Wizard +{ + /// + /// Renders a to text-based report formats + /// (CSV, HTML, Markdown). All output is UTF-8 and self-contained. + /// + internal static class PolicyCompareReport + { + public enum ReportFormat + { + Csv, + Html, + Markdown, + } + + /// + /// Writes to in the chosen format. + /// + /// Comparison result to render. Must not be null. + /// Destination file path. + /// CSV, HTML, or Markdown. + /// When true, only entries marked IsDifferent are included. + public static void Write(PolicyComparer.ComparisonResult result, + string path, + ReportFormat format, + bool differencesOnly) + { + if (result == null) throw new ArgumentNullException(nameof(result)); + if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); + + string content; + switch (format) + { + case ReportFormat.Csv: + content = BuildCsv(result, differencesOnly); + break; + case ReportFormat.Html: + content = BuildHtml(result, differencesOnly); + break; + case ReportFormat.Markdown: + content = BuildMarkdown(result, differencesOnly); + break; + default: + throw new ArgumentOutOfRangeException(nameof(format)); + } + + File.WriteAllText(path, content, new UTF8Encoding(false)); + } + + private static IEnumerable ValidPolicies(PolicyComparer.ComparisonResult r) + { + return r.Policies.Where(p => p.Policy != null).ToList(); + } + + private static IEnumerable> OrderedGroups( + PolicyComparer.ComparisonResult result, bool differencesOnly) + { + return result.Entries + .Where(en => !differencesOnly || en.IsDifferent) + .GroupBy(en => en.Section) + .OrderBy(g => PolicyComparer.OrderOf(g.Key)) + .ThenBy(g => g.Key, StringComparer.OrdinalIgnoreCase); + } + + // ----------------- CSV ----------------- + + private static string BuildCsv(PolicyComparer.ComparisonResult result, bool differencesOnly) + { + var sb = new StringBuilder(); + var policies = ValidPolicies(result).ToList(); + + // Header + sb.Append("Section,Item,Different"); + foreach (var p in policies) + { + sb.Append(',').Append(CsvEscape(p.DisplayName)); + } + sb.AppendLine(); + + foreach (var grp in OrderedGroups(result, differencesOnly)) + { + foreach (var entry in grp.OrderBy(e => e.DisplayName, StringComparer.OrdinalIgnoreCase)) + { + sb.Append(CsvEscape(entry.Section)).Append(','); + sb.Append(CsvEscape(entry.DisplayName)).Append(','); + sb.Append(entry.IsDifferent ? "Yes" : "No"); + foreach (var p in policies) + { + entry.Values.TryGetValue(p.DisplayName, out string v); + sb.Append(',').Append(CsvEscape(v ?? "")); + } + sb.AppendLine(); + } + } + + return sb.ToString(); + } + + private static string CsvEscape(string value) + { + if (value == null) return string.Empty; + bool needsQuotes = value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) >= 0; + if (!needsQuotes) return value; + return "\"" + value.Replace("\"", "\"\"") + "\""; + } + + // ----------------- HTML ----------------- + + private static string BuildHtml(PolicyComparer.ComparisonResult result, bool differencesOnly) + { + var sb = new StringBuilder(); + var policies = ValidPolicies(result).ToList(); + + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine("App Control Policy Comparison"); + sb.AppendLine(""); + + sb.Append("

App Control Policy Comparison

"); + sb.Append("
Generated ").Append(HtmlEscape(DateTime.Now.ToString("u"))).Append("
"); + + // Source policies metadata block + sb.Append("

Policies

"); + sb.AppendLine(""); + sb.AppendLine(""); + for (int i = 0; i < policies.Count; i++) + { + var p = policies[i]; + sb.Append("") + .Append("") + .Append("") + .Append("") + .Append("") + .Append("") + .AppendLine(""); + } + sb.AppendLine("
#Display NamePathSizeModified (UTC)SHA-256
").Append(i + 1).Append("").Append(HtmlEscape(p.DisplayName)).Append("").Append(HtmlEscape(p.SourcePath)).Append("").Append(FormatBytes(p.FileSizeBytes)).Append("").Append(HtmlEscape(p.LastWriteUtc.ToString("u"))).Append("").Append(HtmlEscape(p.Sha256 ?? string.Empty)).Append("
"); + + // Summary strip + var counts = SummaryCounts(result); + sb.Append("
"); + foreach (var kv in counts.OrderBy(kv => PolicyComparer.OrderOf(kv.Key)).ThenBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase)) + { + string cls = kv.Value == 0 ? "zero" : string.Empty; + sb.Append("") + .Append(HtmlEscape(kv.Key)).Append(": ").Append(kv.Value) + .Append(""); + } + sb.AppendLine("
"); + + // Each section + foreach (var grp in OrderedGroups(result, differencesOnly)) + { + sb.Append("

").Append(HtmlEscape(grp.Key)).Append("

"); + sb.AppendLine(""); + sb.Append(""); + foreach (var p in policies) sb.Append(""); + sb.AppendLine(""); + + foreach (var entry in grp.OrderBy(e => e.DisplayName, StringComparer.OrdinalIgnoreCase)) + { + sb.Append(entry.IsDifferent ? "" : ""); + sb.Append(""); + foreach (var p in policies) + { + entry.Values.TryGetValue(p.DisplayName, out string v); + if (v == null) + { + sb.Append(""); + } + else + { + sb.Append(""); + } + } + sb.AppendLine(""); + } + sb.AppendLine("
Item").Append(HtmlEscape(p.DisplayName)).Append("
").Append(HtmlEscape(entry.DisplayName)).Append("<not present>").Append(HtmlEscape(v)).Append("
"); + } + + sb.AppendLine(""); + return sb.ToString(); + } + + private static string HtmlEscape(string value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + return value.Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace("\"", """); + } + + // ----------------- Markdown ----------------- + + private static string BuildMarkdown(PolicyComparer.ComparisonResult result, bool differencesOnly) + { + var sb = new StringBuilder(); + var policies = ValidPolicies(result).ToList(); + + sb.AppendLine("# App Control Policy Comparison"); + sb.AppendLine(); + sb.Append("_Generated ").Append(DateTime.Now.ToString("u")).AppendLine("_"); + sb.AppendLine(); + + sb.AppendLine("## Policies"); + sb.AppendLine(); + sb.AppendLine("| # | Display Name | Path | Size | Modified (UTC) | SHA-256 |"); + sb.AppendLine("|---|---|---|---|---|---|"); + for (int i = 0; i < policies.Count; i++) + { + var p = policies[i]; + sb.Append("| ").Append(i + 1) + .Append(" | ").Append(MdEscape(p.DisplayName)) + .Append(" | ").Append(MdEscape(p.SourcePath)) + .Append(" | ").Append(FormatBytes(p.FileSizeBytes)) + .Append(" | ").Append(p.LastWriteUtc.ToString("u")) + .Append(" | `").Append(p.Sha256 ?? string.Empty).Append("`") + .AppendLine(" |"); + } + sb.AppendLine(); + + // Summary + var counts = SummaryCounts(result); + sb.Append("**Differences:** "); + sb.AppendLine(string.Join(" · ", + counts.OrderBy(kv => PolicyComparer.OrderOf(kv.Key)) + .ThenBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase) + .Select(kv => kv.Key + ": " + kv.Value))); + sb.AppendLine(); + + foreach (var grp in OrderedGroups(result, differencesOnly)) + { + sb.Append("## ").AppendLine(grp.Key); + sb.AppendLine(); + + sb.Append("| Item"); + foreach (var p in policies) sb.Append(" | ").Append(MdEscape(p.DisplayName)); + sb.AppendLine(" |"); + + sb.Append("|---"); + foreach (var _ in policies) sb.Append("|---"); + sb.AppendLine("|"); + + foreach (var entry in grp.OrderBy(e => e.DisplayName, StringComparer.OrdinalIgnoreCase)) + { + sb.Append("| ").Append(MdEscape(entry.DisplayName)); + foreach (var p in policies) + { + entry.Values.TryGetValue(p.DisplayName, out string v); + sb.Append(" | ").Append(MdEscape(v ?? "")); + } + sb.AppendLine(" |"); + } + sb.AppendLine(); + } + + return sb.ToString(); + } + + private static string MdEscape(string value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + return value.Replace("|", "\\|").Replace("\r", string.Empty).Replace("\n", " "); + } + + // ----------------- Summary helpers ----------------- + + /// + /// Returns a dictionary of section -> number of entries flagged as different. + /// Sections with zero differences are still included. + /// + public static Dictionary SummaryCounts(PolicyComparer.ComparisonResult result) + { + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (result == null) return dict; + + foreach (var entry in result.Entries) + { + if (entry == null || string.IsNullOrEmpty(entry.Section)) continue; + if (!dict.ContainsKey(entry.Section)) dict[entry.Section] = 0; + if (entry.IsDifferent) dict[entry.Section]++; + } + return dict; + } + + private static string FormatBytes(long bytes) + { + if (bytes <= 0) return "0 B"; + string[] units = { "B", "KB", "MB", "GB" }; + double size = bytes; + int unit = 0; + while (size >= 1024 && unit < units.Length - 1) + { + size /= 1024; + unit++; + } + return string.Format("{0:0.##} {1}", size, units[unit]); + } + } +} diff --git a/WDAC-Policy-Wizard/app/src/PolicyCompare_Form.Designer.cs b/WDAC-Policy-Wizard/app/src/PolicyCompare_Form.Designer.cs new file mode 100644 index 00000000..63c27997 --- /dev/null +++ b/WDAC-Policy-Wizard/app/src/PolicyCompare_Form.Designer.cs @@ -0,0 +1,391 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace WDAC_Wizard +{ + partial class PolicyCompare_Form + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + label_Title = new System.Windows.Forms.Label(); + label_Subtitle = new System.Windows.Forms.Label(); + policiesDataGrid = new System.Windows.Forms.DataGridView(); + Column_Index = new System.Windows.Forms.DataGridViewTextBoxColumn(); + Column_Path = new System.Windows.Forms.DataGridViewTextBoxColumn(); + Column_Status = new System.Windows.Forms.DataGridViewTextBoxColumn(); + button_AddPolicy = new System.Windows.Forms.Button(); + button_RemovePolicy = new System.Windows.Forms.Button(); + button_Compare = new System.Windows.Forms.Button(); + button_Export = new System.Windows.Forms.Button(); + button_Close = new System.Windows.Forms.Button(); + checkBox_DifferencesOnly = new System.Windows.Forms.CheckBox(); + label_Section = new System.Windows.Forms.Label(); + comboBox_Section = new System.Windows.Forms.ComboBox(); + label_Filter = new System.Windows.Forms.Label(); + textBox_Filter = new System.Windows.Forms.TextBox(); + label_Status = new System.Windows.Forms.Label(); + progressBar = new System.Windows.Forms.ProgressBar(); + summaryFlow = new System.Windows.Forms.FlowLayoutPanel(); + resultsListView = new System.Windows.Forms.ListView(); + resultsContextMenu = new System.Windows.Forms.ContextMenuStrip(components); + menuItem_CopyCell = new System.Windows.Forms.ToolStripMenuItem(); + menuItem_CopyRow = new System.Windows.Forms.ToolStripMenuItem(); + menuItem_CopyRowTsv = new System.Windows.Forms.ToolStripMenuItem(); + ((System.ComponentModel.ISupportInitialize)policiesDataGrid).BeginInit(); + resultsContextMenu.SuspendLayout(); + SuspendLayout(); + // + // label_Title + // + label_Title.AutoSize = true; + label_Title.Font = new System.Drawing.Font("Tahoma", 14F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0); + label_Title.Location = new System.Drawing.Point(20, 18); + label_Title.Name = "label_Title"; + label_Title.Size = new System.Drawing.Size(389, 29); + label_Title.TabIndex = 0; + label_Title.Text = "Compare App Control Policy Files"; + // + // label_Subtitle + // + label_Subtitle.AutoSize = true; + label_Subtitle.Font = new System.Drawing.Font("Tahoma", 10F); + label_Subtitle.Location = new System.Drawing.Point(22, 55); + label_Subtitle.Name = "label_Subtitle"; + label_Subtitle.Size = new System.Drawing.Size(636, 21); + label_Subtitle.TabIndex = 1; + label_Subtitle.Text = "Add 2 or more policy files (XML, CIP, or P7B) — drag-and-drop is supported. Click Compare to view differences."; + // + // policiesDataGrid + // + policiesDataGrid.AllowUserToAddRows = false; + policiesDataGrid.AllowUserToDeleteRows = false; + policiesDataGrid.AllowDrop = true; + policiesDataGrid.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + policiesDataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + policiesDataGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { Column_Index, Column_Path, Column_Status }); + policiesDataGrid.Location = new System.Drawing.Point(22, 95); + policiesDataGrid.MultiSelect = true; + policiesDataGrid.Name = "policiesDataGrid"; + policiesDataGrid.ReadOnly = true; + policiesDataGrid.RowHeadersWidth = 30; + policiesDataGrid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect; + policiesDataGrid.Size = new System.Drawing.Size(1100, 170); + policiesDataGrid.TabIndex = 2; + // + // Column_Index + // + Column_Index.HeaderText = "#"; + Column_Index.MinimumWidth = 40; + Column_Index.Name = "Column_Index"; + Column_Index.ReadOnly = true; + Column_Index.Width = 50; + // + // Column_Path + // + Column_Path.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + Column_Path.HeaderText = "Policy Path"; + Column_Path.MinimumWidth = 200; + Column_Path.Name = "Column_Path"; + Column_Path.ReadOnly = true; + // + // Column_Status + // + Column_Status.HeaderText = "Status"; + Column_Status.MinimumWidth = 100; + Column_Status.Name = "Column_Status"; + Column_Status.ReadOnly = true; + Column_Status.Width = 220; + // + // button_AddPolicy + // + button_AddPolicy.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); + button_AddPolicy.Font = new System.Drawing.Font("Tahoma", 9F); + button_AddPolicy.Location = new System.Drawing.Point(22, 280); + button_AddPolicy.Name = "button_AddPolicy"; + button_AddPolicy.Size = new System.Drawing.Size(140, 32); + button_AddPolicy.TabIndex = 3; + button_AddPolicy.Text = "+ Add Policy"; + button_AddPolicy.UseVisualStyleBackColor = true; + button_AddPolicy.Click += Button_AddPolicy_Click; + // + // button_RemovePolicy + // + button_RemovePolicy.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); + button_RemovePolicy.Font = new System.Drawing.Font("Tahoma", 9F); + button_RemovePolicy.Location = new System.Drawing.Point(170, 280); + button_RemovePolicy.Name = "button_RemovePolicy"; + button_RemovePolicy.Size = new System.Drawing.Size(140, 32); + button_RemovePolicy.TabIndex = 4; + button_RemovePolicy.Text = "- Remove Policy"; + button_RemovePolicy.UseVisualStyleBackColor = true; + button_RemovePolicy.Click += Button_RemovePolicy_Click; + // + // button_Export + // + button_Export.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + button_Export.Enabled = false; + button_Export.Font = new System.Drawing.Font("Tahoma", 9F); + button_Export.Location = new System.Drawing.Point(835, 280); + button_Export.Name = "button_Export"; + button_Export.Size = new System.Drawing.Size(140, 32); + button_Export.TabIndex = 5; + button_Export.Text = "Export Report…"; + button_Export.UseVisualStyleBackColor = true; + button_Export.Click += Button_Export_Click; + // + // button_Compare + // + button_Compare.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + button_Compare.Font = new System.Drawing.Font("Tahoma", 9F, System.Drawing.FontStyle.Bold); + button_Compare.Location = new System.Drawing.Point(982, 280); + button_Compare.Name = "button_Compare"; + button_Compare.Size = new System.Drawing.Size(140, 32); + button_Compare.TabIndex = 6; + button_Compare.Text = "Compare"; + button_Compare.UseVisualStyleBackColor = true; + button_Compare.Click += Button_Compare_Click; + // + // button_Close + // + button_Close.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + button_Close.DialogResult = System.Windows.Forms.DialogResult.Cancel; + button_Close.Font = new System.Drawing.Font("Tahoma", 9F); + button_Close.Location = new System.Drawing.Point(982, 670); + button_Close.Name = "button_Close"; + button_Close.Size = new System.Drawing.Size(140, 32); + button_Close.TabIndex = 99; + button_Close.Text = "Close"; + button_Close.UseVisualStyleBackColor = true; + button_Close.Click += Button_Close_Click; + // + // checkBox_DifferencesOnly + // + checkBox_DifferencesOnly.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); + checkBox_DifferencesOnly.AutoSize = true; + checkBox_DifferencesOnly.Checked = true; + checkBox_DifferencesOnly.CheckState = System.Windows.Forms.CheckState.Checked; + checkBox_DifferencesOnly.Font = new System.Drawing.Font("Tahoma", 9F); + checkBox_DifferencesOnly.Location = new System.Drawing.Point(22, 333); + checkBox_DifferencesOnly.Name = "checkBox_DifferencesOnly"; + checkBox_DifferencesOnly.Size = new System.Drawing.Size(159, 22); + checkBox_DifferencesOnly.TabIndex = 7; + checkBox_DifferencesOnly.Text = "Show differences only"; + checkBox_DifferencesOnly.UseVisualStyleBackColor = true; + checkBox_DifferencesOnly.CheckedChanged += CheckBox_DifferencesOnly_CheckedChanged; + // + // label_Section + // + label_Section.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); + label_Section.AutoSize = true; + label_Section.Font = new System.Drawing.Font("Tahoma", 9F); + label_Section.Location = new System.Drawing.Point(195, 335); + label_Section.Name = "label_Section"; + label_Section.Size = new System.Drawing.Size(58, 18); + label_Section.TabIndex = 8; + label_Section.Text = "Section:"; + // + // comboBox_Section + // + comboBox_Section.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); + comboBox_Section.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + comboBox_Section.Font = new System.Drawing.Font("Tahoma", 9F); + comboBox_Section.Location = new System.Drawing.Point(258, 332); + comboBox_Section.Name = "comboBox_Section"; + comboBox_Section.Size = new System.Drawing.Size(200, 26); + comboBox_Section.TabIndex = 9; + comboBox_Section.SelectedIndexChanged += ComboBox_Section_SelectedIndexChanged; + // + // label_Filter + // + label_Filter.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); + label_Filter.AutoSize = true; + label_Filter.Font = new System.Drawing.Font("Tahoma", 9F); + label_Filter.Location = new System.Drawing.Point(475, 335); + label_Filter.Name = "label_Filter"; + label_Filter.Size = new System.Drawing.Size(46, 18); + label_Filter.TabIndex = 10; + label_Filter.Text = "Filter:"; + // + // textBox_Filter + // + textBox_Filter.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + textBox_Filter.Font = new System.Drawing.Font("Tahoma", 9F); + textBox_Filter.Location = new System.Drawing.Point(525, 332); + textBox_Filter.Name = "textBox_Filter"; + textBox_Filter.PlaceholderText = "Type to filter visible rows…"; + textBox_Filter.Size = new System.Drawing.Size(597, 26); + textBox_Filter.TabIndex = 11; + textBox_Filter.TextChanged += TextBox_Filter_TextChanged; + // + // summaryFlow + // + summaryFlow.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + summaryFlow.AutoScroll = true; + summaryFlow.BackColor = System.Drawing.Color.Transparent; + summaryFlow.FlowDirection = System.Windows.Forms.FlowDirection.LeftToRight; + summaryFlow.Location = new System.Drawing.Point(22, 365); + summaryFlow.Name = "summaryFlow"; + summaryFlow.Size = new System.Drawing.Size(1100, 38); + summaryFlow.TabIndex = 12; + summaryFlow.WrapContents = false; + // + // label_Status + // + label_Status.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + label_Status.AutoEllipsis = true; + label_Status.Font = new System.Drawing.Font("Tahoma", 9F); + label_Status.ForeColor = System.Drawing.Color.DodgerBlue; + label_Status.Location = new System.Drawing.Point(22, 678); + label_Status.Name = "label_Status"; + label_Status.Size = new System.Drawing.Size(640, 18); + label_Status.TabIndex = 13; + // + // progressBar + // + progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + progressBar.Location = new System.Drawing.Point(670, 675); + progressBar.Name = "progressBar"; + progressBar.Size = new System.Drawing.Size(160, 20); + progressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + progressBar.MarqueeAnimationSpeed = 30; + progressBar.TabIndex = 14; + progressBar.Visible = false; + // + // resultsListView + // + resultsListView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + resultsListView.ContextMenuStrip = resultsContextMenu; + resultsListView.FullRowSelect = true; + resultsListView.GridLines = true; + resultsListView.Location = new System.Drawing.Point(22, 410); + resultsListView.MultiSelect = false; + resultsListView.Name = "resultsListView"; + resultsListView.Size = new System.Drawing.Size(1100, 250); + resultsListView.TabIndex = 15; + resultsListView.UseCompatibleStateImageBehavior = false; + resultsListView.View = System.Windows.Forms.View.Details; + resultsListView.OwnerDraw = false; + // + // resultsContextMenu + // + resultsContextMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + menuItem_CopyCell, + menuItem_CopyRow, + menuItem_CopyRowTsv }); + resultsContextMenu.Name = "resultsContextMenu"; + resultsContextMenu.Size = new System.Drawing.Size(180, 70); + // + // menuItem_CopyCell + // + menuItem_CopyCell.Name = "menuItem_CopyCell"; + menuItem_CopyCell.Size = new System.Drawing.Size(180, 22); + menuItem_CopyCell.Text = "Copy cell"; + menuItem_CopyCell.Click += MenuItem_CopyCell_Click; + // + // menuItem_CopyRow + // + menuItem_CopyRow.Name = "menuItem_CopyRow"; + menuItem_CopyRow.Size = new System.Drawing.Size(180, 22); + menuItem_CopyRow.Text = "Copy row"; + menuItem_CopyRow.Click += MenuItem_CopyRow_Click; + // + // menuItem_CopyRowTsv + // + menuItem_CopyRowTsv.Name = "menuItem_CopyRowTsv"; + menuItem_CopyRowTsv.Size = new System.Drawing.Size(180, 22); + menuItem_CopyRowTsv.Text = "Copy row as TSV"; + menuItem_CopyRowTsv.Click += MenuItem_CopyRowTsv_Click; + // + // PolicyCompare_Form + // + AllowDrop = true; + AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; + ClientSize = new System.Drawing.Size(1144, 710); + Controls.Add(progressBar); + Controls.Add(resultsListView); + Controls.Add(summaryFlow); + Controls.Add(textBox_Filter); + Controls.Add(label_Filter); + Controls.Add(comboBox_Section); + Controls.Add(label_Section); + Controls.Add(label_Status); + Controls.Add(checkBox_DifferencesOnly); + Controls.Add(button_Close); + Controls.Add(button_Compare); + Controls.Add(button_Export); + Controls.Add(button_RemovePolicy); + Controls.Add(button_AddPolicy); + Controls.Add(policiesDataGrid); + Controls.Add(label_Subtitle); + Controls.Add(label_Title); + FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable; + MinimumSize = new System.Drawing.Size(960, 640); + Name = "PolicyCompare_Form"; + StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + Text = "Compare App Control Policies"; + Load += PolicyCompare_Form_Load; + ((System.ComponentModel.ISupportInitialize)policiesDataGrid).EndInit(); + resultsContextMenu.ResumeLayout(false); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private System.Windows.Forms.Label label_Title; + private System.Windows.Forms.Label label_Subtitle; + private System.Windows.Forms.DataGridView policiesDataGrid; + private System.Windows.Forms.DataGridViewTextBoxColumn Column_Index; + private System.Windows.Forms.DataGridViewTextBoxColumn Column_Path; + private System.Windows.Forms.DataGridViewTextBoxColumn Column_Status; + private System.Windows.Forms.Button button_AddPolicy; + private System.Windows.Forms.Button button_RemovePolicy; + private System.Windows.Forms.Button button_Compare; + private System.Windows.Forms.Button button_Export; + private System.Windows.Forms.Button button_Close; + private System.Windows.Forms.CheckBox checkBox_DifferencesOnly; + private System.Windows.Forms.Label label_Section; + private System.Windows.Forms.ComboBox comboBox_Section; + private System.Windows.Forms.Label label_Filter; + private System.Windows.Forms.TextBox textBox_Filter; + private System.Windows.Forms.Label label_Status; + private System.Windows.Forms.ProgressBar progressBar; + private System.Windows.Forms.FlowLayoutPanel summaryFlow; + private System.Windows.Forms.ListView resultsListView; + private System.Windows.Forms.ContextMenuStrip resultsContextMenu; + private System.Windows.Forms.ToolStripMenuItem menuItem_CopyCell; + private System.Windows.Forms.ToolStripMenuItem menuItem_CopyRow; + private System.Windows.Forms.ToolStripMenuItem menuItem_CopyRowTsv; + } +} diff --git a/WDAC-Policy-Wizard/app/src/PolicyCompare_Form.cs b/WDAC-Policy-Wizard/app/src/PolicyCompare_Form.cs new file mode 100644 index 00000000..4bc2e7e5 --- /dev/null +++ b/WDAC-Policy-Wizard/app/src/PolicyCompare_Form.cs @@ -0,0 +1,784 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using System.Windows.Forms; + +namespace WDAC_Wizard +{ + /// + /// Standalone window that lets the user select 2 or more policy files (XML or binary) and view a + /// side-by-side comparison of their settings, rule options, file rules, signers, etc. + /// + public partial class PolicyCompare_Form : Form + { + private const string AllSectionsLabel = "All sections"; + + private readonly List _policyPaths; + private PolicyComparer.ComparisonResult _lastResult; + private BackgroundWorker _compareWorker; + + public PolicyCompare_Form() + { + InitializeComponent(); + _policyPaths = new List(); + } + + // ----------------------------------------------------------------------- + // Lifecycle + // ----------------------------------------------------------------------- + + private void PolicyCompare_Form_Load(object sender, EventArgs e) + { + ApplyTheme(); + + resultsListView.SizeChanged += ResultsListView_SizeChanged; + + // Drag-and-drop on the form and on the policies grid + DragEnter += PolicyCompare_Form_DragEnter; + DragDrop += PolicyCompare_Form_DragDrop; + policiesDataGrid.DragEnter += PolicyCompare_Form_DragEnter; + policiesDataGrid.DragDrop += PolicyCompare_Form_DragDrop; + + // Section combo: start with "All sections"; populated more after a comparison runs + ResetSectionCombo(); + + UpdateButtonStates(); + } + + // ----------------------------------------------------------------------- + // Drag-and-drop + // ----------------------------------------------------------------------- + + private void PolicyCompare_Form_DragEnter(object sender, DragEventArgs e) + { + if (e.Data != null && e.Data.GetDataPresent(DataFormats.FileDrop)) + { + e.Effect = DragDropEffects.Copy; + } + else + { + e.Effect = DragDropEffects.None; + } + } + + private void PolicyCompare_Form_DragDrop(object sender, DragEventArgs e) + { + if (e.Data == null || !e.Data.GetDataPresent(DataFormats.FileDrop)) + { + return; + } + + string[] paths = e.Data.GetData(DataFormats.FileDrop) as string[]; + if (paths == null) return; + + int added = 0; + int skipped = 0; + foreach (var p in paths) + { + if (TryAddPolicyPath(p)) added++; else skipped++; + } + + if (added > 0 || skipped > 0) + { + SetStatus(string.Format("Drag-and-drop: {0} added, {1} skipped.", added, skipped), + added > 0 ? Color.SeaGreen : Color.OrangeRed); + } + UpdateButtonStates(); + } + + // ----------------------------------------------------------------------- + // Add / Remove policies + // ----------------------------------------------------------------------- + + private void Button_AddPolicy_Click(object sender, EventArgs e) + { + using (var openFileDialog = new OpenFileDialog()) + { + openFileDialog.Title = "Choose App Control policies to compare"; + openFileDialog.Filter = "App Control Policy Files (*.xml; *.cip; *.p7b)|*.xml;*.cip;*.p7b|" + + "XML Policy Files (*.xml)|*.xml|" + + "Binary Policy Files (*.cip; *.p7b)|*.cip;*.p7b|" + + "All Files (*.*)|*.*"; + openFileDialog.Multiselect = true; + openFileDialog.RestoreDirectory = true; + openFileDialog.CheckPathExists = true; + + if (openFileDialog.ShowDialog() != DialogResult.OK) + { + return; + } + + foreach (string path in openFileDialog.FileNames) + { + TryAddPolicyPath(path); + } + } + + UpdateButtonStates(); + } + + /// + /// Adds a single policy path to the list (if valid and not already present). Returns true + /// if the path was added, false if it was rejected (duplicate, missing, unsupported). + /// + private bool TryAddPolicyPath(string path) + { + if (string.IsNullOrEmpty(path) || !File.Exists(path)) + { + return false; + } + + string ext = Path.GetExtension(path).ToLowerInvariant(); + if (ext != ".xml" && ext != ".cip" && ext != ".p7b") + { + SetStatus(string.Format("Skipped {0}: unsupported extension.", Path.GetFileName(path)), + Color.OrangeRed); + return false; + } + + if (_policyPaths.Any(p => string.Equals(p, path, StringComparison.OrdinalIgnoreCase))) + { + SetStatus(string.Format("{0} is already added.", Path.GetFileName(path)), Color.OrangeRed); + return false; + } + + _policyPaths.Add(path); + policiesDataGrid.Rows.Add(_policyPaths.Count.ToString(), path, "Pending compare"); + Logger.Log?.AddInfoMsg("PolicyCompare: added policy to compare list: " + path); + return true; + } + + private void Button_RemovePolicy_Click(object sender, EventArgs e) + { + if (policiesDataGrid.SelectedRows.Count == 0) + { + SetStatus("Select a policy row to remove.", Color.OrangeRed); + return; + } + + var rowsToRemove = policiesDataGrid.SelectedRows + .Cast() + .OrderByDescending(r => r.Index) + .ToList(); + + foreach (var row in rowsToRemove) + { + if (row.Index >= 0 && row.Index < _policyPaths.Count) + { + _policyPaths.RemoveAt(row.Index); + } + policiesDataGrid.Rows.RemoveAt(row.Index); + } + + for (int i = 0; i < policiesDataGrid.Rows.Count; i++) + { + policiesDataGrid.Rows[i].Cells[Column_Index.Index].Value = (i + 1).ToString(); + } + + UpdateButtonStates(); + } + + // ----------------------------------------------------------------------- + // Compare (background) + // ----------------------------------------------------------------------- + + private void Button_Compare_Click(object sender, EventArgs e) + { + if (_policyPaths.Count < 2) + { + SetStatus("Add at least two policies to compare.", Color.OrangeRed); + return; + } + + if (_compareWorker != null && _compareWorker.IsBusy) + { + SetStatus("Comparison already in progress…", Color.DodgerBlue); + return; + } + + // Reset prior status text on the grid + for (int i = 0; i < policiesDataGrid.Rows.Count; i++) + { + policiesDataGrid.Rows[i].Cells[Column_Status.Index].Value = "Loading…"; + policiesDataGrid.Rows[i].Cells[Column_Status.Index].Style.ForeColor = Color.DodgerBlue; + } + + // Disable interactive elements during the load + SetCompareControlsEnabled(false); + progressBar.Visible = true; + SetStatus("Loading policies…", Color.DodgerBlue); + Logger.Log?.AddNewSeparationLine("Workflow -- Compare Policies"); + + // Snapshot paths to avoid concurrent modification + var pathsSnapshot = new List(_policyPaths); + + _compareWorker = new BackgroundWorker { WorkerReportsProgress = true }; + _compareWorker.DoWork += CompareWorker_DoWork; + _compareWorker.ProgressChanged += CompareWorker_ProgressChanged; + _compareWorker.RunWorkerCompleted += CompareWorker_RunWorkerCompleted; + _compareWorker.RunWorkerAsync(pathsSnapshot); + } + + private void CompareWorker_DoWork(object sender, DoWorkEventArgs e) + { + var paths = (List)e.Argument; + var loadedPolicies = new List(); + var worker = (BackgroundWorker)sender; + + for (int i = 0; i < paths.Count; i++) + { + var loaded = PolicyComparer.LoadPolicy(paths[i]); + loadedPolicies.Add(loaded); + + int percent = (int)((i + 1) / (double)paths.Count * 100.0); + worker.ReportProgress(percent, new LoadProgress { Index = i, Loaded = loaded }); + } + + var result = PolicyComparer.Compare(loadedPolicies); + e.Result = result; + } + + private class LoadProgress + { + public int Index; + public PolicyComparer.LoadedPolicy Loaded; + } + + private void CompareWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) + { + var p = e.UserState as LoadProgress; + if (p == null) return; + + string statusText = p.Loaded.Policy != null + ? "Loaded" + : ("Error: " + (p.Loaded.LoadError ?? "Unknown")); + + if (p.Index < policiesDataGrid.Rows.Count) + { + policiesDataGrid.Rows[p.Index].Cells[Column_Status.Index].Value = statusText; + policiesDataGrid.Rows[p.Index].Cells[Column_Status.Index].Style.ForeColor = + p.Loaded.Policy != null ? Color.SeaGreen : Color.Firebrick; + } + } + + private void CompareWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) + { + progressBar.Visible = false; + + try + { + if (e.Error != null) + { + Logger.Log?.AddErrorMsg("PolicyCompare: comparison failed", e.Error); + SetStatus("Comparison failed: " + e.Error.Message, Color.Firebrick); + _lastResult = null; + RenderComparison(null); + return; + } + + var result = e.Result as PolicyComparer.ComparisonResult; + int successfullyLoaded = result?.Policies.Count(lp => lp.Policy != null) ?? 0; + if (result == null || successfullyLoaded < 2) + { + SetStatus("Need at least two policies that load successfully to compare.", Color.Firebrick); + _lastResult = null; + RenderComparison(null); + return; + } + + _lastResult = result; + RebuildSectionCombo(); + RenderComparison(_lastResult); + BuildSummaryStrip(_lastResult); + + int diffCount = _lastResult.Entries.Count(en => en.IsDifferent); + SetStatus(string.Format("Compared {0} policies. {1} differences found.", + successfullyLoaded, diffCount), + diffCount == 0 ? Color.SeaGreen : Color.DodgerBlue); + } + finally + { + _compareWorker = null; + // Re-enable controls AFTER _lastResult has been assigned, so the Export button + // correctly reflects whether a comparison result is available. + SetCompareControlsEnabled(true); + } + } + + private void SetCompareControlsEnabled(bool enabled) + { + button_Compare.Enabled = enabled && _policyPaths.Count >= 2; + button_AddPolicy.Enabled = enabled; + button_RemovePolicy.Enabled = enabled && _policyPaths.Count > 0; + button_Export.Enabled = enabled && _lastResult != null + && _lastResult.Entries != null + && _lastResult.Entries.Count > 0; + } + + // ----------------------------------------------------------------------- + // Section combo / Filter / Differences-only + // ----------------------------------------------------------------------- + + private void ResetSectionCombo() + { + comboBox_Section.BeginUpdate(); + try + { + comboBox_Section.Items.Clear(); + comboBox_Section.Items.Add(AllSectionsLabel); + comboBox_Section.SelectedIndex = 0; + } + finally + { + comboBox_Section.EndUpdate(); + } + } + + private void RebuildSectionCombo() + { + if (_lastResult == null) + { + ResetSectionCombo(); + return; + } + + string previous = comboBox_Section.SelectedItem as string; + + comboBox_Section.BeginUpdate(); + try + { + comboBox_Section.Items.Clear(); + comboBox_Section.Items.Add(AllSectionsLabel); + + var sections = _lastResult.Entries + .Select(en => en.Section) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(PolicyComparer.OrderOf) + .ThenBy(s => s, StringComparer.OrdinalIgnoreCase) + .ToArray(); + foreach (var s in sections) comboBox_Section.Items.Add(s); + + int idx = 0; + if (!string.IsNullOrEmpty(previous)) + { + int found = comboBox_Section.Items.IndexOf(previous); + if (found >= 0) idx = found; + } + comboBox_Section.SelectedIndex = idx; + } + finally + { + comboBox_Section.EndUpdate(); + } + } + + private void ComboBox_Section_SelectedIndexChanged(object sender, EventArgs e) + { + if (_lastResult != null) RenderComparison(_lastResult); + } + + private void TextBox_Filter_TextChanged(object sender, EventArgs e) + { + if (_lastResult != null) RenderComparison(_lastResult); + } + + private void CheckBox_DifferencesOnly_CheckedChanged(object sender, EventArgs e) + { + if (_lastResult != null) RenderComparison(_lastResult); + } + + // ----------------------------------------------------------------------- + // Summary header strip + // ----------------------------------------------------------------------- + + private void BuildSummaryStrip(PolicyComparer.ComparisonResult result) + { + summaryFlow.Controls.Clear(); + if (result == null) return; + + var counts = PolicyCompareReport.SummaryCounts(result); + var ordered = counts.OrderBy(kv => PolicyComparer.OrderOf(kv.Key)) + .ThenBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase); + + foreach (var kv in ordered) + { + var btn = new Button + { + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + FlatStyle = FlatStyle.Flat, + Margin = new Padding(0, 4, 6, 0), + Padding = new Padding(8, 2, 8, 2), + Tag = kv.Key, + Text = string.Format("{0}: {1}", kv.Key, kv.Value), + UseVisualStyleBackColor = false, + Cursor = Cursors.Hand, + }; + + btn.FlatAppearance.BorderColor = Color.LightGray; + if (kv.Value > 0) + { + btn.BackColor = Color.FromArgb(231, 240, 250); + btn.ForeColor = Color.Black; + } + else + { + btn.BackColor = Color.FromArgb(240, 240, 240); + btn.ForeColor = Color.DimGray; + } + + btn.Click += SummaryButton_Click; + summaryFlow.Controls.Add(btn); + } + } + + private void SummaryButton_Click(object sender, EventArgs e) + { + if (_lastResult == null) return; + var btn = sender as Button; + if (btn == null) return; + + string section = btn.Tag as string; + if (string.IsNullOrEmpty(section)) return; + + // Set the section combo to that section. If not present (because filter was hiding it), + // fall back to "All sections". + int idx = comboBox_Section.Items.IndexOf(section); + comboBox_Section.SelectedIndex = idx >= 0 ? idx : 0; + + // Scroll the listview to the first item in that section + ScrollToSection(section); + } + + private void ScrollToSection(string section) + { + foreach (ListViewItem item in resultsListView.Items) + { + if (item.Group != null && string.Equals(item.Group.Header, section, StringComparison.OrdinalIgnoreCase)) + { + item.EnsureVisible(); + item.Selected = true; + item.Focused = true; + resultsListView.Focus(); + break; + } + } + } + + // ----------------------------------------------------------------------- + // Render comparison + // ----------------------------------------------------------------------- + + private void RenderComparison(PolicyComparer.ComparisonResult result) + { + resultsListView.BeginUpdate(); + try + { + resultsListView.Items.Clear(); + resultsListView.Groups.Clear(); + resultsListView.Columns.Clear(); + + if (result == null || result.Policies.Count == 0) + { + return; + } + + resultsListView.Columns.Add("Section", 160); + resultsListView.Columns.Add("Item", 260); + + var validPolicies = result.Policies.Where(p => p.Policy != null).ToList(); + foreach (var policy in validPolicies) + { + resultsListView.Columns.Add(policy.DisplayName, 200); + } + + ResizePolicyColumns(); + + bool diffOnly = checkBox_DifferencesOnly.Checked; + string sectionFilter = comboBox_Section.SelectedItem as string; + string textFilter = textBox_Filter.Text?.Trim(); + + IEnumerable source = result.Entries; + + if (diffOnly) + { + source = source.Where(en => en.IsDifferent); + } + + if (!string.IsNullOrEmpty(sectionFilter) + && !string.Equals(sectionFilter, AllSectionsLabel, StringComparison.OrdinalIgnoreCase)) + { + source = source.Where(en => string.Equals(en.Section, sectionFilter, StringComparison.OrdinalIgnoreCase)); + } + + if (!string.IsNullOrEmpty(textFilter)) + { + source = source.Where(en => MatchesTextFilter(en, validPolicies, textFilter)); + } + + var grouped = source.GroupBy(en => en.Section) + .OrderBy(g => PolicyComparer.OrderOf(g.Key)) + .ThenBy(g => g.Key, StringComparer.OrdinalIgnoreCase); + + foreach (var sectionGroup in grouped) + { + var lvGroup = new ListViewGroup(sectionGroup.Key, sectionGroup.Key); + resultsListView.Groups.Add(lvGroup); + + foreach (var entry in sectionGroup.OrderBy(en => en.DisplayName, StringComparer.OrdinalIgnoreCase)) + { + var item = new ListViewItem(entry.Section) { Group = lvGroup }; + item.SubItems.Add(entry.DisplayName); + + foreach (var policy in validPolicies) + { + entry.Values.TryGetValue(policy.DisplayName, out string value); + item.SubItems.Add(value ?? ""); + } + + if (entry.IsDifferent) + { + item.BackColor = Color.FromArgb(255, 245, 220); + item.ForeColor = Color.Black; + } + + resultsListView.Items.Add(item); + } + } + } + finally + { + resultsListView.EndUpdate(); + } + } + + private static bool MatchesTextFilter(PolicyComparer.ComparisonEntry entry, + List validPolicies, + string filter) + { + if (entry.Section != null && entry.Section.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0) return true; + if (entry.DisplayName != null && entry.DisplayName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0) return true; + + foreach (var p in validPolicies) + { + entry.Values.TryGetValue(p.DisplayName, out string v); + if (!string.IsNullOrEmpty(v) && v.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0) + { + return true; + } + } + return false; + } + + // ----------------------------------------------------------------------- + // Context menu (copy) + // ----------------------------------------------------------------------- + + private void MenuItem_CopyCell_Click(object sender, EventArgs e) + { + var hit = resultsListView.PointToClient(MousePosition); + var info = resultsListView.HitTest(hit); + if (info?.SubItem != null && !string.IsNullOrEmpty(info.SubItem.Text)) + { + SafeSetClipboard(info.SubItem.Text); + return; + } + + // Fall back: copy first selected row first cell + if (resultsListView.SelectedItems.Count > 0) + { + SafeSetClipboard(resultsListView.SelectedItems[0].Text); + } + } + + private void MenuItem_CopyRow_Click(object sender, EventArgs e) + { + CopySelectedRow(separator: " | "); + } + + private void MenuItem_CopyRowTsv_Click(object sender, EventArgs e) + { + CopySelectedRow(separator: "\t"); + } + + private void CopySelectedRow(string separator) + { + if (resultsListView.SelectedItems.Count == 0) return; + var item = resultsListView.SelectedItems[0]; + var sb = new StringBuilder(); + for (int i = 0; i < item.SubItems.Count; i++) + { + if (i > 0) sb.Append(separator); + sb.Append(item.SubItems[i].Text); + } + SafeSetClipboard(sb.ToString()); + } + + private static void SafeSetClipboard(string text) + { + try { Clipboard.SetText(text ?? string.Empty); } + catch (Exception ex) { Logger.Log?.AddWarningMsg("PolicyCompare: clipboard set failed: " + ex.Message); } + } + + // ----------------------------------------------------------------------- + // Export + // ----------------------------------------------------------------------- + + private void Button_Export_Click(object sender, EventArgs e) + { + if (_lastResult == null || _lastResult.Entries.Count == 0) + { + SetStatus("Run a comparison first, then export.", Color.OrangeRed); + return; + } + + using (var sfd = new SaveFileDialog()) + { + sfd.Title = "Export comparison report"; + sfd.Filter = "HTML Report (*.html)|*.html|" + + "CSV File (*.csv)|*.csv|" + + "Markdown File (*.md)|*.md"; + sfd.FilterIndex = 1; + sfd.AddExtension = true; + sfd.RestoreDirectory = true; + sfd.FileName = "policy-comparison"; + + if (sfd.ShowDialog(this) != DialogResult.OK) return; + + PolicyCompareReport.ReportFormat format; + string ext = Path.GetExtension(sfd.FileName).ToLowerInvariant(); + switch (ext) + { + case ".csv": format = PolicyCompareReport.ReportFormat.Csv; break; + case ".md": format = PolicyCompareReport.ReportFormat.Markdown; break; + case ".html": + case ".htm": + default: format = PolicyCompareReport.ReportFormat.Html; break; + } + + try + { + PolicyCompareReport.Write(_lastResult, sfd.FileName, format, + checkBox_DifferencesOnly.Checked); + SetStatus("Exported report to " + sfd.FileName, Color.SeaGreen); + Logger.Log?.AddInfoMsg("PolicyCompare: exported " + format + " report to " + sfd.FileName); + } + catch (Exception ex) + { + Logger.Log?.AddErrorMsg("PolicyCompare: export failed", ex); + SetStatus("Export failed: " + ex.Message, Color.Firebrick); + } + } + } + + // ----------------------------------------------------------------------- + // Misc + // ----------------------------------------------------------------------- + + private void Button_Close_Click(object sender, EventArgs e) + { + Close(); + } + + private void ResultsListView_SizeChanged(object sender, EventArgs e) + { + ResizePolicyColumns(); + } + + private void ResizePolicyColumns() + { + const int fixedColumnsWidth = 160 + 260; + const int verticalScrollAllowance = 24; + + if (resultsListView.Columns.Count <= 2) + { + return; + } + + int policyColumnCount = resultsListView.Columns.Count - 2; + int available = Math.Max(0, resultsListView.ClientSize.Width - fixedColumnsWidth - verticalScrollAllowance); + int width = Math.Max(140, available / policyColumnCount); + + for (int i = 2; i < resultsListView.Columns.Count; i++) + { + resultsListView.Columns[i].Width = width; + } + } + + private void UpdateButtonStates() + { + button_Compare.Enabled = _policyPaths.Count >= 2 && (_compareWorker == null || !_compareWorker.IsBusy); + button_RemovePolicy.Enabled = _policyPaths.Count > 0; + button_Export.Enabled = _lastResult != null + && _lastResult.Entries != null + && _lastResult.Entries.Count > 0; + } + + private void SetStatus(string text, Color color) + { + label_Status.Text = text; + label_Status.ForeColor = color; + } + + /// + /// Apply Light/Dark theming consistent with other forms in the Wizard. + /// + private void ApplyTheme() + { + bool dark = Properties.Settings.Default.useDarkMode; + + BackColor = dark ? Color.FromArgb(15, 15, 15) : Color.White; + ForeColor = dark ? Color.White : Color.Black; + + foreach (Control ctrl in Controls) + { + if (ctrl is Label || ctrl is CheckBox) + { + ctrl.BackColor = BackColor; + if (ctrl != label_Status) // keep status label highlight color + { + ctrl.ForeColor = ForeColor; + } + } + else if (ctrl is TextBox tb) + { + tb.BackColor = dark ? Color.FromArgb(32, 32, 32) : Color.White; + tb.ForeColor = dark ? Color.White : Color.Black; + } + else if (ctrl is ComboBox cb) + { + cb.BackColor = dark ? Color.FromArgb(32, 32, 32) : Color.White; + cb.ForeColor = dark ? Color.White : Color.Black; + } + else if (ctrl is Button btn) + { + btn.FlatStyle = FlatStyle.Flat; + btn.FlatAppearance.BorderColor = dark ? Color.DodgerBlue : Color.Black; + btn.BackColor = dark ? Color.Transparent : Color.WhiteSmoke; + btn.ForeColor = dark ? Color.DodgerBlue : Color.Black; + } + } + + policiesDataGrid.BackgroundColor = dark ? Color.FromArgb(15, 15, 15) : Color.White; + policiesDataGrid.DefaultCellStyle.BackColor = dark ? Color.FromArgb(32, 32, 32) : Color.White; + policiesDataGrid.DefaultCellStyle.ForeColor = dark ? Color.White : Color.Black; + policiesDataGrid.AlternatingRowsDefaultCellStyle.BackColor = dark ? Color.FromArgb(24, 24, 24) : Color.WhiteSmoke; + policiesDataGrid.ColumnHeadersDefaultCellStyle.BackColor = dark ? Color.Black : Color.WhiteSmoke; + policiesDataGrid.ColumnHeadersDefaultCellStyle.ForeColor = dark ? Color.White : Color.Black; + policiesDataGrid.EnableHeadersVisualStyles = false; + policiesDataGrid.GridColor = dark ? Color.LightSlateGray : Color.LightGray; + + resultsListView.BackColor = dark ? Color.FromArgb(15, 15, 15) : Color.White; + resultsListView.ForeColor = dark ? Color.White : Color.Black; + + summaryFlow.BackColor = BackColor; + } + } +} diff --git a/WDAC-Policy-Wizard/app/src/PolicyComparer.cs b/WDAC-Policy-Wizard/app/src/PolicyComparer.cs new file mode 100644 index 00000000..510f6324 --- /dev/null +++ b/WDAC-Policy-Wizard/app/src/PolicyComparer.cs @@ -0,0 +1,617 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Xml.Serialization; + +namespace WDAC_Wizard +{ + /// + /// Loads one or more App Control policies from disk and compares them, producing a hierarchical + /// list of differences grouped by section (metadata, rule options, file rules, signers, etc.). + /// + internal static class PolicyComparer + { + /// + /// Canonical ordering of comparison sections. Sections not in this list are treated as + /// having a sort key larger than any listed section and are sorted alphabetically among + /// themselves at the end. Centralized here so the renderer, exporter, and summary header + /// all agree on order. + /// + public static readonly string[] SectionOrder = new[] + { + "Rule Options", + "Policy Metadata", + "Settings", + "Signing Scenarios", + "Signers", + "Update Policy Signers", + "Supplemental Policy Signers", + "CI Signers", + "EKUs", + "File Rules", + }; + + /// + /// Returns the canonical sort key for a section. Lower numbers sort first; sections not in + /// the canonical list return int.MaxValue so they fall to the end (still alphabetical + /// among themselves when ThenBy is applied). + /// + public static int OrderOf(string section) + { + if (section == null) return int.MaxValue; + int idx = Array.IndexOf(SectionOrder, section); + return idx < 0 ? int.MaxValue : idx; + } + + /// + /// Represents a single policy that has been loaded from disk and prepared for comparison. + /// + public class LoadedPolicy + { + public string SourcePath { get; set; } + public string DisplayName { get; set; } + public SiPolicy Policy { get; set; } + public string LoadError { get; set; } + + // File metadata for traceability and reports + public long FileSizeBytes { get; set; } + public DateTime LastWriteUtc { get; set; } + public string Sha256 { get; set; } + } + + /// + /// A single comparison entry: identifies an item (e.g. a rule option, file rule, signer) and which + /// policies contain it. The Values dictionary stores per-policy display strings. If a policy does + /// not contain the item, its value is null. + /// + public class ComparisonEntry + { + public string Section { get; set; } // e.g. "Rule Options", "File Rules" + public string Key { get; set; } // unique within section + public string DisplayName { get; set; } // human-readable name shown in UI + public Dictionary Values { get; set; } // policyDisplayName -> value (null if missing) + public bool IsDifferent { get; set; } // true if not all policies have the same value + + public ComparisonEntry() + { + Values = new Dictionary(); + } + } + + /// + /// Result of a multi-policy comparison: list of policies that were compared and a list of + /// every entry seen in any of the policies for each section. + /// + public class ComparisonResult + { + public List Policies { get; set; } = new List(); + public List Entries { get; set; } = new List(); + } + + /// + /// Loads a policy file from disk. Supports both XML policies and binary (.cip/.p7b) policies. + /// + /// Path to the policy file + /// LoadedPolicy with either a populated Policy or a LoadError + public static LoadedPolicy LoadPolicy(string policyPath) + { + var loaded = new LoadedPolicy + { + SourcePath = policyPath, + DisplayName = string.IsNullOrEmpty(policyPath) ? "" : Path.GetFileName(policyPath) + }; + + if (string.IsNullOrEmpty(policyPath) || !File.Exists(policyPath)) + { + loaded.LoadError = "File not found."; + return loaded; + } + + try + { + // Capture file metadata up front (best-effort). These are useful in the report + // even if parsing later fails. + try + { + var fi = new FileInfo(policyPath); + loaded.FileSizeBytes = fi.Length; + loaded.LastWriteUtc = fi.LastWriteTimeUtc; + loaded.Sha256 = ComputeSha256(policyPath); + } + catch (Exception metaEx) + { + Logger.Log?.AddWarningMsg("PolicyComparer.LoadPolicy: failed to read file metadata: " + metaEx.Message); + } + + string xmlPath = policyPath; + + // Convert binary policies to XML on disk first, then deserialize + if (Helper.IsBinaryPolicyFile(policyPath)) + { + xmlPath = BinaryPolicyConverter.ConvertToXml(policyPath); + if (string.IsNullOrEmpty(xmlPath)) + { + loaded.LoadError = "Unable to convert binary policy to XML."; + return loaded; + } + } + + SiPolicy siPolicy = Helper.DeserializeXMLtoPolicy(xmlPath); + if (siPolicy == null) + { + loaded.LoadError = "Unable to parse policy XML."; + return loaded; + } + + loaded.Policy = siPolicy; + } + catch (Exception ex) + { + Logger.Log?.AddErrorMsg("PolicyComparer.LoadPolicy caught the following exception", ex); + loaded.LoadError = ex.Message; + } + + return loaded; + } + + private static string ComputeSha256(string path) + { + using (var stream = File.OpenRead(path)) + using (var sha = SHA256.Create()) + { + byte[] hash = sha.ComputeHash(stream); + var sb = new StringBuilder(hash.Length * 2); + for (int i = 0; i < hash.Length; i++) + { + sb.Append(hash[i].ToString("X2")); + } + return sb.ToString(); + } + } + + /// + /// Compares the provided list of policies and returns a ComparisonResult containing per-section + /// entries with the value from each policy. + /// + public static ComparisonResult Compare(List policies) + { + var result = new ComparisonResult(); + if (policies == null || policies.Count == 0) + { + return result; + } + + // Ensure unique display names so duplicate file names do not collide as dictionary keys + EnsureUniqueDisplayNames(policies); + result.Policies.AddRange(policies); + + // Only operate on policies that loaded successfully + var validPolicies = policies.Where(p => p.Policy != null).ToList(); + + CompareMetadata(validPolicies, result); + CompareRuleOptions(validPolicies, result); + CompareFileRules(validPolicies, result); + CompareSigners(validPolicies, result); + CompareSigningScenarios(validPolicies, result); + CompareUpdateSigners(validPolicies, result); + CompareSupplementalSigners(validPolicies, result); + CompareCiSigners(validPolicies, result); + CompareSettings(validPolicies, result); + CompareEKUs(validPolicies, result); + + // Mark which entries are different across policies + foreach (var entry in result.Entries) + { + entry.IsDifferent = ComputeIsDifferent(entry, validPolicies); + } + + return result; + } + + private static void EnsureUniqueDisplayNames(List policies) + { + var seen = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var p in policies) + { + string baseName = string.IsNullOrEmpty(p.DisplayName) ? "policy" : p.DisplayName; + if (!seen.ContainsKey(baseName)) + { + seen[baseName] = 1; + p.DisplayName = baseName; + } + else + { + seen[baseName]++; + p.DisplayName = string.Format("{0} ({1})", baseName, seen[baseName]); + } + } + } + + private static bool ComputeIsDifferent(ComparisonEntry entry, List validPolicies) + { + // An entry is "different" if any valid policy has a value that differs from any other, + // including the case where some policies are missing the item entirely. + string firstValue = null; + bool firstSet = false; + foreach (var p in validPolicies) + { + entry.Values.TryGetValue(p.DisplayName, out string v); + if (!firstSet) + { + firstValue = v; + firstSet = true; + } + else if (!string.Equals(firstValue, v, StringComparison.Ordinal)) + { + return true; + } + } + return false; + } + + private static void AddOrUpdateEntry(ComparisonResult result, + string section, + string key, + string displayName, + string policyName, + string value) + { + var entry = result.Entries.FirstOrDefault(e => e.Section == section && e.Key == key); + if (entry == null) + { + entry = new ComparisonEntry + { + Section = section, + Key = key, + DisplayName = displayName, + }; + result.Entries.Add(entry); + } + + // If the same key was already added for this policy, prefer the latest non-null value + entry.Values[policyName] = value; + } + + private static void CompareMetadata(List policies, ComparisonResult result) + { + const string section = "Policy Metadata"; + foreach (var p in policies) + { + AddOrUpdateEntry(result, section, "FriendlyName", "Friendly Name", + p.DisplayName, p.Policy.FriendlyName); + AddOrUpdateEntry(result, section, "PolicyType", "Policy Type", + p.DisplayName, + p.Policy.PolicyTypeSpecified ? p.Policy.PolicyType.ToString() : null); + AddOrUpdateEntry(result, section, "PolicyID", "Policy ID", + p.DisplayName, p.Policy.PolicyID); + AddOrUpdateEntry(result, section, "BasePolicyID", "Base Policy ID", + p.DisplayName, p.Policy.BasePolicyID); + AddOrUpdateEntry(result, section, "PolicyTypeID", "Policy Type ID", + p.DisplayName, p.Policy.PolicyTypeID); + AddOrUpdateEntry(result, section, "VersionEx", "Version", + p.DisplayName, p.Policy.VersionEx); + AddOrUpdateEntry(result, section, "PlatformID", "Platform ID", + p.DisplayName, p.Policy.PlatformID); + AddOrUpdateEntry(result, section, "HvciOptions", "HVCI Options", + p.DisplayName, + p.Policy.HvciOptionsSpecified ? p.Policy.HvciOptions.ToString() : null); + } + } + + private static void CompareRuleOptions(List policies, ComparisonResult result) + { + const string section = "Rule Options"; + + // Pre-compute friendly name for every known OptionType so the user always sees + // every option, even if no policy has it enabled. + var optionTypeToFriendly = GetOptionTypeFriendlyNames(); + + // First, ensure every option appears as a row, defaulting to "No" for each policy. + foreach (var kvp in optionTypeToFriendly.OrderBy(k => k.Value, StringComparer.OrdinalIgnoreCase)) + { + foreach (var p in policies) + { + AddOrUpdateEntry(result, section, kvp.Key.ToString(), kvp.Value, p.DisplayName, "No"); + } + } + + // Now mark each option present on a policy as "Yes". + foreach (var p in policies) + { + if (p.Policy.Rules == null) continue; + foreach (var rule in p.Policy.Rules) + { + if (rule == null) continue; + string key = rule.Item.ToString(); + string display = optionTypeToFriendly.TryGetValue(rule.Item, out string friendly) + ? friendly + : key; + AddOrUpdateEntry(result, section, key, display, p.DisplayName, "Yes"); + } + } + } + + /// + /// Builds a map from each OptionType enum value to its XML friendly name (e.g. + /// OptionType.EnabledUMCI -> "Enabled:UMCI"). Falls back to the enum name when no + /// XmlEnumAttribute is present. + /// + private static Dictionary GetOptionTypeFriendlyNames() + { + var map = new Dictionary(); + Type t = typeof(OptionType); + foreach (OptionType value in Enum.GetValues(t)) + { + FieldInfo field = t.GetField(value.ToString()); + string friendly = value.ToString(); + if (field != null) + { + var attr = field.GetCustomAttribute(); + if (attr != null && !string.IsNullOrEmpty(attr.Name)) + { + friendly = attr.Name; + } + } + map[value] = friendly; + } + return map; + } + + private static void CompareFileRules(List policies, ComparisonResult result) + { + const string section = "File Rules"; + foreach (var p in policies) + { + if (p.Policy.FileRules == null) continue; + foreach (var rule in p.Policy.FileRules) + { + if (rule == null) continue; + + string id = null; + string friendly = null; + string typeName = rule.GetType().Name; + string description = null; + + switch (rule) + { + case Allow a: + id = a.ID; + friendly = a.FriendlyName; + description = BuildFileRuleDescription("Allow", a.FilePath, a.FileName, + a.PackageFamilyName, a.MinimumFileVersion, + a.MaximumFileVersion, a.Hash); + break; + case Deny d: + id = d.ID; + friendly = d.FriendlyName; + description = BuildFileRuleDescription("Deny", d.FilePath, d.FileName, + d.PackageFamilyName, d.MinimumFileVersion, + d.MaximumFileVersion, d.Hash); + break; + case FileAttrib fa: + id = fa.ID; + friendly = fa.FriendlyName; + description = BuildFileRuleDescription("FileAttrib", fa.FilePath, fa.FileName, + fa.PackageFamilyName, fa.MinimumFileVersion, + fa.MaximumFileVersion, fa.Hash); + break; + case FileRule fr: + id = fr.ID; + friendly = fr.FriendlyName; + description = BuildFileRuleDescription(fr.Type.ToString(), fr.FilePath, + fr.FileName, fr.PackageFamilyName, + fr.MinimumFileVersion, fr.MaximumFileVersion, + fr.Hash); + break; + } + + string key = string.IsNullOrEmpty(id) + ? string.Format("{0}|{1}", typeName, friendly ?? description ?? Guid.NewGuid().ToString()) + : id; + string display = string.IsNullOrEmpty(friendly) ? (id ?? typeName) : friendly; + + AddOrUpdateEntry(result, section, key, display, p.DisplayName, description); + } + } + } + + private static string BuildFileRuleDescription(string ruleType, string filePath, string fileName, + string packageFamilyName, string minimumVersion, + string maximumVersion, byte[] hash) + { + var parts = new List { ruleType }; + if (!string.IsNullOrEmpty(filePath)) parts.Add("Path=" + filePath); + if (!string.IsNullOrEmpty(fileName)) parts.Add("FileName=" + fileName); + if (!string.IsNullOrEmpty(packageFamilyName)) parts.Add("PFN=" + packageFamilyName); + if (!string.IsNullOrEmpty(minimumVersion)) parts.Add("MinVer=" + minimumVersion); + if (!string.IsNullOrEmpty(maximumVersion)) parts.Add("MaxVer=" + maximumVersion); + if (hash != null && hash.Length > 0) + { + parts.Add("Hash=" + BitConverter.ToString(hash).Replace("-", string.Empty)); + } + return string.Join("; ", parts); + } + + private static void CompareSigners(List policies, ComparisonResult result) + { + const string section = "Signers"; + foreach (var p in policies) + { + if (p.Policy.Signers == null) continue; + foreach (var signer in p.Policy.Signers) + { + if (signer == null) continue; + string key = string.IsNullOrEmpty(signer.ID) + ? "Signer|" + (signer.Name ?? Guid.NewGuid().ToString()) + : signer.ID; + string display = string.IsNullOrEmpty(signer.Name) ? (signer.ID ?? "Signer") : signer.Name; + string description = BuildSignerDescription(signer); + AddOrUpdateEntry(result, section, key, display, p.DisplayName, description); + } + } + } + + private static string BuildSignerDescription(Signer signer) + { + var parts = new List(); + if (!string.IsNullOrEmpty(signer.Name)) parts.Add("Name=" + signer.Name); + if (signer.CertRoot != null && signer.CertRoot.Value != null && signer.CertRoot.Value.Length > 0) + { + parts.Add("CertRoot=" + BitConverter.ToString(signer.CertRoot.Value).Replace("-", string.Empty)); + } + if (signer.CertPublisher != null && !string.IsNullOrEmpty(signer.CertPublisher.Value)) + { + parts.Add("Publisher=" + signer.CertPublisher.Value); + } + if (signer.CertIssuer != null && !string.IsNullOrEmpty(signer.CertIssuer.Value)) + { + parts.Add("Issuer=" + signer.CertIssuer.Value); + } + if (signer.FileAttribRef != null && signer.FileAttribRef.Length > 0) + { + parts.Add("FileAttribRefs=" + signer.FileAttribRef.Length); + } + if (signer.SignTimeAfterSpecified) + { + parts.Add("SignTimeAfter=" + signer.SignTimeAfter.ToString("o")); + } + return parts.Count == 0 ? "Signer" : string.Join("; ", parts); + } + + private static void CompareSigningScenarios(List policies, ComparisonResult result) + { + const string section = "Signing Scenarios"; + foreach (var p in policies) + { + if (p.Policy.SigningScenarios == null) continue; + foreach (var scenario in p.Policy.SigningScenarios) + { + if (scenario == null) continue; + string scenarioLabel = ScenarioLabel(scenario.Value); + string key = string.IsNullOrEmpty(scenario.ID) + ? "Scenario|" + scenarioLabel + : scenario.ID; + string display = string.IsNullOrEmpty(scenario.FriendlyName) + ? scenarioLabel + : scenario.FriendlyName + " (" + scenarioLabel + ")"; + + int allowedSigners = scenario.ProductSigners?.AllowedSigners?.AllowedSigner?.Length ?? 0; + int deniedSigners = scenario.ProductSigners?.DeniedSigners?.DeniedSigner?.Length ?? 0; + int fileRulesRefs = scenario.ProductSigners?.FileRulesRef?.FileRuleRef?.Length ?? 0; + + string description = string.Format( + "Scenario={0}; AllowedSigners={1}; DeniedSigners={2}; FileRuleRefs={3}", + scenarioLabel, allowedSigners, deniedSigners, fileRulesRefs); + + AddOrUpdateEntry(result, section, key, display, p.DisplayName, description); + } + } + } + + private static string ScenarioLabel(byte value) + { + switch (value) + { + case 12: return "User Mode"; + case 131: return "Kernel Mode"; + default: return "Scenario " + value; + } + } + + private static void CompareUpdateSigners(List policies, ComparisonResult result) + { + const string section = "Update Policy Signers"; + foreach (var p in policies) + { + if (p.Policy.UpdatePolicySigners == null) continue; + foreach (var s in p.Policy.UpdatePolicySigners) + { + if (s == null) continue; + string key = string.IsNullOrEmpty(s.SignerId) ? "UpdateSigner|" + Guid.NewGuid() : s.SignerId; + AddOrUpdateEntry(result, section, key, s.SignerId ?? key, p.DisplayName, "Referenced"); + } + } + } + + private static void CompareSupplementalSigners(List policies, ComparisonResult result) + { + const string section = "Supplemental Policy Signers"; + foreach (var p in policies) + { + if (p.Policy.SupplementalPolicySigners == null) continue; + foreach (var s in p.Policy.SupplementalPolicySigners) + { + if (s == null) continue; + string key = string.IsNullOrEmpty(s.SignerId) ? "SuppSigner|" + Guid.NewGuid() : s.SignerId; + AddOrUpdateEntry(result, section, key, s.SignerId ?? key, p.DisplayName, "Referenced"); + } + } + } + + private static void CompareCiSigners(List policies, ComparisonResult result) + { + const string section = "CI Signers"; + foreach (var p in policies) + { + if (p.Policy.CiSigners == null) continue; + foreach (var s in p.Policy.CiSigners) + { + if (s == null) continue; + string key = string.IsNullOrEmpty(s.SignerId) ? "CiSigner|" + Guid.NewGuid() : s.SignerId; + AddOrUpdateEntry(result, section, key, s.SignerId ?? key, p.DisplayName, "Referenced"); + } + } + } + + private static void CompareSettings(List policies, ComparisonResult result) + { + const string section = "Settings"; + foreach (var p in policies) + { + if (p.Policy.Settings == null) continue; + foreach (var setting in p.Policy.Settings) + { + if (setting == null) continue; + string key = string.Format("{0}|{1}|{2}", + setting.Provider ?? string.Empty, + setting.Key ?? string.Empty, + setting.ValueName ?? string.Empty); + string display = string.Format("{0} / {1} / {2}", + setting.Provider ?? "", + setting.Key ?? "", + setting.ValueName ?? ""); + string value = setting.Value != null && setting.Value.Item != null + ? setting.Value.Item.ToString() + : ""; + AddOrUpdateEntry(result, section, key, display, p.DisplayName, value); + } + } + } + + private static void CompareEKUs(List policies, ComparisonResult result) + { + const string section = "EKUs"; + foreach (var p in policies) + { + if (p.Policy.EKUs == null) continue; + foreach (var eku in p.Policy.EKUs) + { + if (eku == null) continue; + string key = string.IsNullOrEmpty(eku.ID) ? "EKU|" + Guid.NewGuid() : eku.ID; + string display = string.IsNullOrEmpty(eku.FriendlyName) ? (eku.ID ?? "EKU") : eku.FriendlyName; + string value = eku.Value != null && eku.Value.Length > 0 + ? BitConverter.ToString(eku.Value).Replace("-", string.Empty) + : "Present"; + AddOrUpdateEntry(result, section, key, display, p.DisplayName, value); + } + } + } + } +} From 8402c400ed63387853579b0851dc0fe083617ce2 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Mon, 15 Jun 2026 15:26:48 -0400 Subject: [PATCH 16/16] Fix summary for Folder Scan output. Bug with "Policy-relevant binaries" numbers --- WDAC-Policy-Wizard/app/src/BuildPage.cs | 2 +- WDAC-Policy-Wizard/app/src/MainForm.cs | 271 +++++++++++++++++++++++- 2 files changed, 267 insertions(+), 6 deletions(-) diff --git a/WDAC-Policy-Wizard/app/src/BuildPage.cs b/WDAC-Policy-Wizard/app/src/BuildPage.cs index 0b41d04e..9ee41476 100644 --- a/WDAC-Policy-Wizard/app/src/BuildPage.cs +++ b/WDAC-Policy-Wizard/app/src/BuildPage.cs @@ -100,7 +100,7 @@ public void SetScanSummary(long totalFilesOnDisk, int policyRelevantFiles, int h $" Signer rules (PCA/Pub) {signerRules,8:N0}\r\n" + $" Hash rules in XML {hashRules,8:N0}\r\n" + $" Unique hashes {uniqueHashes,8:N0}\r\n" + - $" Duplicate hashes removed {duplicateHashes,8:N0}\r\n" + + $" Duplicate hashes (collisions) {duplicateHashes,5:N0}\r\n" + $"\r\n" + $" Elapsed time {elapsedText,8}"; diff --git a/WDAC-Policy-Wizard/app/src/MainForm.cs b/WDAC-Policy-Wizard/app/src/MainForm.cs index 6fe50737..f77e41e2 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.ComponentModel; using System.Drawing; +using System.Linq; +using System.Text.RegularExpressions; using System.Windows.Forms; using System.IO; using WDAC_Wizard.src; @@ -53,6 +55,224 @@ private class FolderScanStats public TimeSpan Elapsed; } + // Matches the trailing hash-flavor descriptor that New-CIPolicy -Level Hash appends to a + // rule's FriendlyName. Designed to be forward-compatible with new SHA bit lengths and + // any future "Hash Sha" combinations the cmdlet may emit. + // + // Observed today (Windows 10/11): + // "Hash Sha1", "Hash Sha256", + // "Hash Page Sha1", "Hash Page Sha256", + // "Hash Authenticode SIP Sha256" + // + // Pattern breakdown: + // \s+ literal whitespace before "Hash" + // (? capture the descriptor for downstream classification + // Hash literal "Hash" + // (?:\s+(?:Page|Authenticode\s+SIP))? optional qualifier + // \s+Sha\d+ "Sha" followed by 1+ digits (Sha1, Sha256, Sha384, ...) + // ) + // \s*$ optional trailing whitespace + private static readonly Regex HashRuleFriendlyNameSuffixRegex = new Regex( + @"\s+(?Hash(?:\s+(?:Page|Authenticode\s+SIP))?\s+Sha\d+)\s*$", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /// + /// Extracts the source binary path and hash-flavor descriptor from the FriendlyName of a + /// hash rule produced by New-CIPolicy. New-CIPolicy emits 1, 2, or 4 hash rules per + /// scanned file depending on file type, all sharing a FriendlyName of + /// "<path> <flavor>" where the flavor matches + /// . + /// + /// The Allow/Deny rule's FriendlyName attribute. + /// + /// The matched flavor descriptor (e.g. "Hash Sha256", "Hash Page Sha1", + /// "Hash Authenticode SIP Sha256"), or null when the regex didn't match. + /// + /// The trimmed source path, or the trimmed full FriendlyName when no match. + private static string ExtractScannedBinaryPath(string friendlyName, out string flavor) + { + flavor = null; + if (string.IsNullOrEmpty(friendlyName)) + { + return null; + } + + Match match = HashRuleFriendlyNameSuffixRegex.Match(friendlyName); + if (match.Success) + { + flavor = NormalizeFlavor(match.Groups["flavor"].Value); + return friendlyName.Substring(0, match.Index).Trim(); + } + + // FriendlyName didn't match the expected New-CIPolicy pattern; fall back to using + // the whole string so the caller still gets a stable per-binary key. + return friendlyName.Trim(); + } + + /// + /// Collapses any internal whitespace runs in a captured flavor descriptor to a single + /// space and normalizes casing so flavors group identically regardless of how the + /// upstream cmdlet formatted them. + /// + private static string NormalizeFlavor(string flavor) + { + if (string.IsNullOrEmpty(flavor)) + { + return flavor; + } + + return Regex.Replace(flavor.Trim(), @"\s+", " "); + } + + /// + /// Writes a detailed per-class breakdown of the folder scan results to the log file. Each + /// scanned binary is bucketed by the unique set of hash flavors emitted for it (e.g. + /// "Sha1+Sha256+Page Sha1+Page Sha256") and binaries the cmdlet emitted multiple rules + /// for the same flavor are split into a separate "(emitted multiple times)" bucket. The + /// classification is data-driven, so any new flavor combination produced by future + /// New-CIPolicy versions appears automatically. + /// + private static void LogFolderScanBreakdown( + string scanPath, + FolderScanStats stats, + Dictionary> binaryFlavors) + { + if (binaryFlavors == null || stats == null) + { + return; + } + + // Bucket each binary by its (flavor-set, has-multiples) signature. + // Key = pipe-joined sorted flavors; "*" appended when any flavor count > 1. + var classes = new Dictionary(StringComparer.Ordinal); + foreach (var kvp in binaryFlavors) + { + Dictionary flavorCounts = kvp.Value; + bool hasMultiples = false; + int totalRules = 0; + foreach (int n in flavorCounts.Values) + { + totalRules += n; + if (n > 1) hasMultiples = true; + } + + var sortedFlavors = new List(flavorCounts.Keys); + sortedFlavors.Sort(StringComparer.OrdinalIgnoreCase); + string flavorSetKey = string.Join("|", sortedFlavors) + (hasMultiples ? "|*" : ""); + + if (!classes.TryGetValue(flavorSetKey, out var bucket)) + { + bucket = new ClassBucket + { + Label = BuildClassLabel(sortedFlavors, hasMultiples), + RulesPerBinaryDisplay = BuildRulesPerBinaryDisplay(flavorCounts, hasMultiples), + }; + classes[flavorSetKey] = bucket; + } + + bucket.Binaries++; + bucket.TotalRules += totalRules; + } + + // Emit the breakdown as a fixed-width table in the log. + try + { + Logger.Log.AddNewSeparationLine("Folder Scan Breakdown"); + Logger.Log.AddInfoMsg($"Scan path: {scanPath}"); + Logger.Log.AddInfoMsg($"Elapsed: {stats.Elapsed}"); + Logger.Log.AddInfoMsg(""); + Logger.Log.AddInfoMsg( + string.Format("{0,-60} {1,10} {2,-40} {3,12}", + "File class", "Binaries", "Rules each", "Total rules")); + Logger.Log.AddInfoMsg(new string('-', 124)); + + // Most common classes first for readability. + foreach (var c in classes.Values + .OrderByDescending(b => b.Binaries) + .ThenBy(b => b.Label, StringComparer.OrdinalIgnoreCase)) + { + Logger.Log.AddInfoMsg( + string.Format("{0,-60} {1,10:N0} {2,-40} {3,12:N0}", + Truncate(c.Label, 60), + c.Binaries, + Truncate(c.RulesPerBinaryDisplay, 40), + c.TotalRules)); + } + + Logger.Log.AddInfoMsg(new string('-', 124)); + Logger.Log.AddInfoMsg( + string.Format("{0,-60} {1,10:N0} {2,-40} {3,12:N0}", + "TOTAL", + stats.PolicyRelevantFiles, + "-", + stats.HashRules)); + Logger.Log.AddInfoMsg(""); + Logger.Log.AddInfoMsg($"Total files on disk: {stats.TotalFilesOnDisk:N0}"); + Logger.Log.AddInfoMsg($"Policy-relevant binaries: {stats.PolicyRelevantFiles:N0}"); + Logger.Log.AddInfoMsg($"Signer rules (PCA/Pub): {stats.SignerRules:N0}"); + Logger.Log.AddInfoMsg($"Hash rules in XML: {stats.HashRules:N0}"); + Logger.Log.AddInfoMsg($"Unique hashes: {stats.UniqueHashes:N0}"); + Logger.Log.AddInfoMsg($"Duplicate hashes (collisions): {stats.DuplicateHashes:N0}"); + } + catch (Exception ex) + { + // Logging is best-effort; never let a logging failure break the build flow. + Logger.Log.AddWarningMsg($"LogFolderScanBreakdown failed: {ex.Message}"); + } + } + + private sealed class ClassBucket + { + public string Label; + public string RulesPerBinaryDisplay; + public int Binaries; + public int TotalRules; + } + + /// + /// Builds a human-friendly class label from a sorted list of flavor descriptors + /// (e.g. "Hash Sha1, Hash Sha256, Hash Page Sha1, Hash Page Sha256"). Marks the bucket + /// as "(emitted multiple times)" when one or more flavors were emitted more than once + /// for the same binary. + /// + private static string BuildClassLabel(List sortedFlavors, bool hasMultiples) + { + string joined = sortedFlavors.Count > 0 + ? string.Join(", ", sortedFlavors) + : "Unknown"; + return hasMultiples + ? joined + " (emitted multiple times)" + : joined; + } + + /// + /// Builds the "Rules each" cell, e.g. "4" or "8 (2x of 4 flavors)". + /// + private static string BuildRulesPerBinaryDisplay(Dictionary flavorCounts, bool hasMultiples) + { + int total = 0; + foreach (int n in flavorCounts.Values) total += n; + + if (!hasMultiples) + { + return total.ToString(); + } + + // Find the most common multiplier (typically 2) for an informative label. + int maxMultiplier = 1; + foreach (int n in flavorCounts.Values) + { + if (n > maxMultiplier) maxMultiplier = n; + } + return $"{total} ({maxMultiplier}x of {flavorCounts.Count} flavor{(flavorCounts.Count == 1 ? "" : "s")})"; + } + + private static string Truncate(string s, int maxLen) + { + if (string.IsNullOrEmpty(s) || s.Length <= maxLen) return s ?? string.Empty; + return s.Substring(0, maxLen - 1) + "…"; + } + public enum EditWorkflowType { Edit = 0, @@ -1434,13 +1654,27 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) scanTask.Wait(); sw.Stop(); - // Compute scan summary stats from the generated policy + // Compute scan summary stats from the generated policy. + // + // New-CIPolicy -Level Hash emits 1, 2, or 4 hash rules per scanned file: + // - PE binaries with page hashes: 4 rules (flat Sha1/Sha256, page Sha1/Sha256) + // - PE binaries without page hashes: 2 rules (flat Sha1, Sha256) + // - Non-PE scripts/data (.js, .ENU, etc.): 1 rule (Authenticode SIP Sha256) + // It can also emit the *same* rule twice for some files (a known cmdlet bug + // we suppress the warning for in CreateScannedPolicy.ps1), which is what + // produces the "duplicate hashes" we report. + // + // FileRules.Length is therefore the rule count, NOT the binary count. We + // derive the binary count by parsing each rule's FriendlyName. if (signerSiPolicy != null) { int hashRuleCount = 0; int signerRuleCount = signerSiPolicy.Signers?.Length ?? 0; - int totalFileRules = signerSiPolicy.FileRules?.Length ?? 0; var uniqueHashes = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Per-binary flavor frequencies, e.g. { "Hash Sha1" -> 2, "Hash Sha256" -> 2 }. + // Used to classify each binary into a flavor-set bucket for the breakdown log. + var binaryFlavors = new Dictionary>(StringComparer.OrdinalIgnoreCase); int duplicateHashCount = 0; if (signerSiPolicy.FileRules != null) @@ -1448,8 +1682,17 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) foreach (var rule in signerSiPolicy.FileRules) { byte[] hash = null; - if (rule is Allow allow) hash = allow.Hash; - else if (rule is Deny deny) hash = deny.Hash; + string friendlyName = null; + if (rule is Allow allow) + { + hash = allow.Hash; + friendlyName = allow.FriendlyName; + } + else if (rule is Deny deny) + { + hash = deny.Hash; + friendlyName = deny.FriendlyName; + } if (hash != null && hash.Length > 0) { @@ -1457,6 +1700,20 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) string hex = BitConverter.ToString(hash); if (!uniqueHashes.Add(hex)) duplicateHashCount++; + + string sourcePath = ExtractScannedBinaryPath(friendlyName, out string flavor); + if (!string.IsNullOrEmpty(sourcePath)) + { + if (!binaryFlavors.TryGetValue(sourcePath, out var flavorCounts)) + { + flavorCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); + binaryFlavors[sourcePath] = flavorCounts; + } + + string flavorKey = string.IsNullOrEmpty(flavor) ? "Unknown" : flavor; + flavorCounts.TryGetValue(flavorKey, out int n); + flavorCounts[flavorKey] = n + 1; + } } } } @@ -1465,7 +1722,7 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) _folderScanStats = new FolderScanStats { TotalFilesOnDisk = totalFiles >= 0 ? totalFiles : 0, - PolicyRelevantFiles = totalFileRules, + PolicyRelevantFiles = binaryFlavors.Count, HashRules = hashRuleCount, SignerRules = signerRuleCount, UniqueHashes = uniqueHashes.Count, @@ -1473,6 +1730,10 @@ public SiPolicy ProcessSignerRules(BackgroundWorker worker, SiPolicy siPolicy) Elapsed = sw.Elapsed }; + // Write a detailed per-class breakdown to the log only - keeps the UI + // summary compact while preserving the diagnostic data for support cases. + LogFolderScanBreakdown(scanPathDisplay, _folderScanStats, binaryFlavors); + siPolicy = PolicyHelper.MergePolicies(signerSiPolicy, siPolicy); } }