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 00000000..cc635d2d Binary files /dev/null and b/WDAC-Policy-Wizard/app/Resources/imgs/compare.png differ 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/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/BuildPage.cs b/WDAC-Policy-Wizard/app/src/BuildPage.cs index 0b41d04e..787363bf 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 @@ -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/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/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; 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/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.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..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. @@ -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/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.Designer.cs b/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs index 9e654d47..a10cf9a5 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(); @@ -50,10 +51,12 @@ 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(); label4 = new System.Windows.Forms.Label(); + label5 = new System.Windows.Forms.Label(); control_Panel.SuspendLayout(); SuspendLayout(); // @@ -78,10 +81,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; @@ -101,6 +104,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 // @@ -118,10 +122,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 +141,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); @@ -164,6 +187,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 +358,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,11 +371,25 @@ 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; 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.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; + button_Back.Text = "Back"; + button_Back.UseVisualStyleBackColor = true; + button_Back.Visible = false; + button_Back.Click += Button_Back_Click; + // // label1 // label1.AutoSize = true; @@ -365,39 +404,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,21 +456,26 @@ 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); Controls.Add(label1); Controls.Add(button_Next); + Controls.Add(button_Back); Controls.Add(control_Panel); Controls.Add(label_Welcome); + Controls.Add(button_Compare); Controls.Add(button_Merge); 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"; @@ -439,10 +495,12 @@ 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; 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; @@ -453,6 +511,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..7ea42ee7 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, @@ -83,9 +303,38 @@ 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 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 IWizardPage) + { + RegisterPage(e.Control); + } + } /// /// New policy button selected: User can select either base or suppl policy, @@ -186,6 +435,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 // ##################### @@ -196,6 +472,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 +514,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 +550,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; + } + /// /// /// @@ -1434,13 +1768,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 +1796,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 +1814,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 +1836,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 +1844,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); } } @@ -1699,6 +2074,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); } @@ -1787,6 +2185,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 +2408,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 +2544,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 +2562,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; } } @@ -2239,7 +2655,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/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); + } + } + } + } +} diff --git a/WDAC-Policy-Wizard/app/src/PolicyHelper.cs b/WDAC-Policy-Wizard/app/src/PolicyHelper.cs index 06c078b3..6de372f8 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; @@ -2045,6 +2046,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 +2065,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 +2209,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 +2301,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 +2316,178 @@ 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); + } + } - // New DeniedSigners + var merged = new List(newFileRules.Length + resultFileRules.Length); + + // 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 fileRulesCopy; + 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; + } + + 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 + }); } /// @@ -2382,6 +2569,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 /// 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..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; @@ -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/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.Designer.cs b/WDAC-Policy-Wizard/app/src/SigningRules_Control.Designer.cs index 1653d537..bd985246 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,62 +106,70 @@ 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); + this.rulesDataGrid.ColumnHeaderMouseClick += new System.Windows.Forms.DataGridViewCellMouseEventHandler(this.RulesDataGrid_ColumnHeaderMouseClick); // // 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"; this.column_Action.ReadOnly = true; + this.column_Action.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.column_Action.Width = 76; // // 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"; this.column_Level.ReadOnly = true; + this.column_Level.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.column_Level.Width = 71; // // 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"; this.Column_Name.ReadOnly = true; + this.Column_Name.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.Column_Name.Width = 74; // // 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"; this.Column_Files.ReadOnly = true; + this.Column_Files.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.Column_Files.Width = 127; // // 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"; this.Column_Exceptions.ReadOnly = true; + this.Column_Exceptions.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.Column_Exceptions.Width = 105; // // 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"; this.column_ID.ReadOnly = true; + this.column_ID.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.column_ID.Width = 77; // // label8 @@ -195,6 +203,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 +220,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 +234,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 +247,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 +260,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 +308,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..b2e1c589 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; @@ -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); } @@ -587,18 +591,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 +634,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. /// @@ -1137,6 +1275,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 /// @@ -1213,6 +1484,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); 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;