From 431783bdedd728001167b888a73785a927acba69 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Fri, 12 Jun 2026 10:19:57 -0400 Subject: [PATCH 1/2] Comparer Policy feature --- .../app/Properties/Resources.Designer.cs | 10 + .../app/Properties/Resources.resx | 3 + .../app/Resources/imgs/compare.png | Bin 0 -> 642 bytes WDAC-Policy-Wizard/app/WDAC Wizard.csproj | 1 + .../app/src/MainForm.Designer.cs | 73 +- WDAC-Policy-Wizard/app/src/MainForm.cs | 27 + .../app/src/PolicyCompareReport.cs | 323 ++++++++ .../app/src/PolicyCompare_Form.Designer.cs | 391 +++++++++ .../app/src/PolicyCompare_Form.cs | 784 ++++++++++++++++++ WDAC-Policy-Wizard/app/src/PolicyComparer.cs | 617 ++++++++++++++ 10 files changed, 2211 insertions(+), 18 deletions(-) create mode 100644 WDAC-Policy-Wizard/app/Resources/imgs/compare.png create mode 100644 WDAC-Policy-Wizard/app/src/PolicyCompareReport.cs create mode 100644 WDAC-Policy-Wizard/app/src/PolicyCompare_Form.Designer.cs create mode 100644 WDAC-Policy-Wizard/app/src/PolicyCompare_Form.cs create mode 100644 WDAC-Policy-Wizard/app/src/PolicyComparer.cs diff --git a/WDAC-Policy-Wizard/app/Properties/Resources.Designer.cs b/WDAC-Policy-Wizard/app/Properties/Resources.Designer.cs index b3000e73..01f904b0 100644 --- a/WDAC-Policy-Wizard/app/Properties/Resources.Designer.cs +++ b/WDAC-Policy-Wizard/app/Properties/Resources.Designer.cs @@ -865,6 +865,16 @@ internal static System.Drawing.Bitmap merge { return ((System.Drawing.Bitmap)(obj)); } } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap compare { + get { + object obj = ResourceManager.GetObject("compare", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } /// /// Looks up a localized string similar to AllowMicrosoft - SingleFormat.xml. diff --git a/WDAC-Policy-Wizard/app/Properties/Resources.resx b/WDAC-Policy-Wizard/app/Properties/Resources.resx index 47072e86..4012751b 100644 --- a/WDAC-Policy-Wizard/app/Properties/Resources.resx +++ b/WDAC-Policy-Wizard/app/Properties/Resources.resx @@ -121,6 +121,9 @@ ..\Resources\imgs\merge.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\imgs\compare.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\imgs\next.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/WDAC-Policy-Wizard/app/Resources/imgs/compare.png b/WDAC-Policy-Wizard/app/Resources/imgs/compare.png new file mode 100644 index 0000000000000000000000000000000000000000..cc635d2dbad881fd9db697fb47b523f549d8056a GIT binary patch literal 642 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3{2vlE{-7;ac^fW^wV|}X>(Va=po10>GDa! zL+(Latigi{5f!1%9f~KqZofNW$0XF)a7J&!v-fg0=KX!P>a(i)+4+9^>u+U-=gbNc zU~z;XgGT$)%~witvv#}xe{%cElf(ZXxoXbN`D|@|`tqu{IcoQ7rkljxvuo6xEG|8J z@9I}aCA->u&pkXHEN5=~_UJ;cD~oS(KQ%Blq5@9+wkbUI%dkJ~RQF=mE1n?1 zkjAQ8SA#|M`2sII*?gt!wg35D_6N=OFFideyuS3OpTO_m6L+%yoqhku+r0D9yQ1V? zo>nt1uitO9{Xx}6`#C#4@@DL6-It>N?A`ltkUd*h*N1{_+P?qTH!8c+qHDldBj?}Zyqlq`@Avb$t7gWo>kH3V|4jYxq#YRg-S2*UUNS?oyd$e>wZWZhyYBz{ qQa<-;<*vB*q2Jk{5h&2}fpMz99=rZo^>M&-!{F)a=d#Wzp$Pz`LMvJT literal 0 HcmV?d00001 diff --git a/WDAC-Policy-Wizard/app/WDAC Wizard.csproj b/WDAC-Policy-Wizard/app/WDAC Wizard.csproj index 2b7d294f..13c9aec7 100644 --- a/WDAC-Policy-Wizard/app/WDAC Wizard.csproj +++ b/WDAC-Policy-Wizard/app/WDAC Wizard.csproj @@ -195,6 +195,7 @@ + diff --git a/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs b/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs index 9e654d47..3418107e 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs @@ -39,6 +39,7 @@ private void InitializeComponent() backgroundWorker1 = new System.ComponentModel.BackgroundWorker(); button_Edit = new System.Windows.Forms.Button(); button_Merge = new System.Windows.Forms.Button(); + button_Compare = new System.Windows.Forms.Button(); control_Panel = new System.Windows.Forms.Panel(); workflow_Label = new System.Windows.Forms.Label(); page5_Button = new System.Windows.Forms.Button(); @@ -54,6 +55,7 @@ private void InitializeComponent() label2 = new System.Windows.Forms.Label(); label3 = new System.Windows.Forms.Label(); label4 = new System.Windows.Forms.Label(); + label5 = new System.Windows.Forms.Label(); control_Panel.SuspendLayout(); SuspendLayout(); // @@ -78,10 +80,10 @@ private void InitializeComponent() button_New.FlatStyle = System.Windows.Forms.FlatStyle.Flat; button_New.Font = new System.Drawing.Font("Tahoma", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0); button_New.Image = Properties.Resources.newPolicy; - button_New.Location = new System.Drawing.Point(328, 225); + button_New.Location = new System.Drawing.Point(215, 225); button_New.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); button_New.Name = "button_New"; - button_New.Size = new System.Drawing.Size(195, 217); + button_New.Size = new System.Drawing.Size(165, 217); button_New.TabIndex = 10; button_New.Text = "Policy Creator"; button_New.TextAlign = System.Drawing.ContentAlignment.BottomCenter; @@ -118,10 +120,10 @@ private void InitializeComponent() button_Edit.FlatStyle = System.Windows.Forms.FlatStyle.Flat; button_Edit.Font = new System.Drawing.Font("Tahoma", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0); button_Edit.Image = Properties.Resources.tools; - button_Edit.Location = new System.Drawing.Point(567, 225); + button_Edit.Location = new System.Drawing.Point(425, 225); button_Edit.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); button_Edit.Name = "button_Edit"; - button_Edit.Size = new System.Drawing.Size(195, 217); + button_Edit.Size = new System.Drawing.Size(165, 217); button_Edit.TabIndex = 25; button_Edit.Text = "Policy Editor"; button_Edit.TextAlign = System.Drawing.ContentAlignment.BottomCenter; @@ -137,16 +139,35 @@ private void InitializeComponent() button_Merge.FlatStyle = System.Windows.Forms.FlatStyle.Flat; button_Merge.Font = new System.Drawing.Font("Tahoma", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0); button_Merge.Image = Properties.Resources.merge; - button_Merge.Location = new System.Drawing.Point(813, 225); + button_Merge.Location = new System.Drawing.Point(635, 225); button_Merge.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); button_Merge.Name = "button_Merge"; - button_Merge.Size = new System.Drawing.Size(195, 217); + button_Merge.Size = new System.Drawing.Size(165, 217); button_Merge.TabIndex = 26; button_Merge.Text = "Policy Merger"; button_Merge.TextAlign = System.Drawing.ContentAlignment.BottomCenter; button_Merge.UseVisualStyleBackColor = false; button_Merge.Click += Button_Merge_Click; // + // button_Compare + // + button_Compare.BackColor = System.Drawing.Color.Transparent; + button_Compare.FlatAppearance.BorderSize = 0; + button_Compare.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(50, 30, 144, 255); + button_Compare.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(50, 30, 144, 255); + button_Compare.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + button_Compare.Font = new System.Drawing.Font("Tahoma", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0); + button_Compare.Image = Properties.Resources.compare; + button_Compare.Location = new System.Drawing.Point(845, 225); + button_Compare.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + button_Compare.Name = "button_Compare"; + button_Compare.Size = new System.Drawing.Size(165, 217); + button_Compare.TabIndex = 27; + button_Compare.Text = "Policy Comparer"; + button_Compare.TextAlign = System.Drawing.ContentAlignment.BottomCenter; + button_Compare.UseVisualStyleBackColor = false; + button_Compare.Click += Button_Compare_Click; + // // control_Panel // control_Panel.BackColor = System.Drawing.Color.FromArgb(230, 230, 230); @@ -365,39 +386,51 @@ private void InitializeComponent() // // label2 // - label2.AutoSize = true; label2.BackColor = System.Drawing.Color.Transparent; label2.Font = new System.Drawing.Font("Tahoma", 9F); - label2.Location = new System.Drawing.Point(296, 453); + label2.Location = new System.Drawing.Point(198, 453); label2.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); label2.Name = "label2"; - label2.Size = new System.Drawing.Size(244, 36); + label2.Size = new System.Drawing.Size(200, 50); label2.TabIndex = 33; - label2.Text = "Create a new base or supplemental \r\npolicy"; + label2.Text = "Create a new base or\r\nsupplemental policy"; + label2.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; // // label3 // - label3.AutoSize = true; label3.BackColor = System.Drawing.Color.Transparent; label3.Font = new System.Drawing.Font("Tahoma", 9F); - label3.Location = new System.Drawing.Point(563, 453); + label3.Location = new System.Drawing.Point(408, 453); label3.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); label3.Name = "label3"; - label3.Size = new System.Drawing.Size(218, 36); + label3.Size = new System.Drawing.Size(200, 50); label3.TabIndex = 34; - label3.Text = "Edit an existing policy on disk or \r\nconvert event logs to a policy"; + label3.Text = "Edit an existing policy or\r\nconvert event logs to a policy"; + label3.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; // // label4 // - label4.AutoSize = true; label4.BackColor = System.Drawing.Color.Transparent; label4.Font = new System.Drawing.Font("Tahoma", 9F); - label4.Location = new System.Drawing.Point(802, 453); + label4.Location = new System.Drawing.Point(618, 453); label4.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); label4.Name = "label4"; - label4.Size = new System.Drawing.Size(236, 18); + label4.Size = new System.Drawing.Size(200, 50); label4.TabIndex = 35; - label4.Text = "Merge two existing policies into one\r\n"; + label4.Text = "Merge two existing\r\npolicies into one"; + label4.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // label5 + // + label5.BackColor = System.Drawing.Color.Transparent; + label5.Font = new System.Drawing.Font("Tahoma", 9F); + label5.Location = new System.Drawing.Point(828, 453); + label5.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + label5.Name = "label5"; + label5.Size = new System.Drawing.Size(200, 50); + label5.TabIndex = 36; + label5.Text = "Compare two or more policies\r\nand view their differences"; + label5.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; // // MainWindow // @@ -405,6 +438,7 @@ private void InitializeComponent() AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; BackColor = System.Drawing.Color.White; ClientSize = new System.Drawing.Size(1232, 703); + Controls.Add(label5); Controls.Add(label4); Controls.Add(label3); Controls.Add(label2); @@ -412,6 +446,7 @@ private void InitializeComponent() Controls.Add(button_Next); Controls.Add(control_Panel); Controls.Add(label_Welcome); + Controls.Add(button_Compare); Controls.Add(button_Merge); Controls.Add(button_Edit); Controls.Add(label_Info); @@ -439,6 +474,7 @@ private void InitializeComponent() private System.Windows.Forms.Button button_New; private System.Windows.Forms.Button button_Edit; private System.Windows.Forms.Button button_Merge; + private System.Windows.Forms.Button button_Compare; private System.Windows.Forms.Panel control_Panel; private System.Windows.Forms.Button settings_Button; private System.Windows.Forms.Button home_Button; @@ -453,6 +489,7 @@ private void InitializeComponent() private System.Windows.Forms.Label label2; private System.Windows.Forms.Label label3; private System.Windows.Forms.Label label4; + private System.Windows.Forms.Label label5; private System.Windows.Forms.Label workflow_Label; } } diff --git a/WDAC-Policy-Wizard/app/src/MainForm.cs b/WDAC-Policy-Wizard/app/src/MainForm.cs index 6fe50737..83b2fde1 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.cs @@ -186,6 +186,33 @@ private void Button_Merge_Click(object sender, EventArgs e) } } + /// + /// Compare policy button selected: opens a separate window where the user can load 2 or + /// more policies (XML or binary) and view their differences side-by-side. The comparison + /// flow is non-destructive and does not affect the active wizard workflow state. + /// + private void Button_Compare_Click(object sender, EventArgs e) + { + Logger.Log.AddNewSeparationLine("Workflow -- Compare Policies Selected"); + + try + { + using (var compareForm = new PolicyCompare_Form()) + { + compareForm.ShowDialog(this); + } + } + catch (Exception ex) + { + Logger.Log.AddErrorMsg("Button_Compare_Click caught the following exception", ex); + MessageBox.Show(this, + "Unable to open the Policy Compare window: " + ex.Message, + "Policy Compare Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + } + // ##################### // CONTROL PANEL CONTROLS // ##################### diff --git a/WDAC-Policy-Wizard/app/src/PolicyCompareReport.cs b/WDAC-Policy-Wizard/app/src/PolicyCompareReport.cs new file mode 100644 index 00000000..a2d6459b --- /dev/null +++ b/WDAC-Policy-Wizard/app/src/PolicyCompareReport.cs @@ -0,0 +1,323 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace WDAC_Wizard +{ + /// + /// Renders a to text-based report formats + /// (CSV, HTML, Markdown). All output is UTF-8 and self-contained. + /// + internal static class PolicyCompareReport + { + public enum ReportFormat + { + Csv, + Html, + Markdown, + } + + /// + /// Writes to in the chosen format. + /// + /// Comparison result to render. Must not be null. + /// Destination file path. + /// CSV, HTML, or Markdown. + /// When true, only entries marked IsDifferent are included. + public static void Write(PolicyComparer.ComparisonResult result, + string path, + ReportFormat format, + bool differencesOnly) + { + if (result == null) throw new ArgumentNullException(nameof(result)); + if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); + + string content; + switch (format) + { + case ReportFormat.Csv: + content = BuildCsv(result, differencesOnly); + break; + case ReportFormat.Html: + content = BuildHtml(result, differencesOnly); + break; + case ReportFormat.Markdown: + content = BuildMarkdown(result, differencesOnly); + break; + default: + throw new ArgumentOutOfRangeException(nameof(format)); + } + + File.WriteAllText(path, content, new UTF8Encoding(false)); + } + + private static IEnumerable ValidPolicies(PolicyComparer.ComparisonResult r) + { + return r.Policies.Where(p => p.Policy != null).ToList(); + } + + private static IEnumerable> OrderedGroups( + PolicyComparer.ComparisonResult result, bool differencesOnly) + { + return result.Entries + .Where(en => !differencesOnly || en.IsDifferent) + .GroupBy(en => en.Section) + .OrderBy(g => PolicyComparer.OrderOf(g.Key)) + .ThenBy(g => g.Key, StringComparer.OrdinalIgnoreCase); + } + + // ----------------- CSV ----------------- + + private static string BuildCsv(PolicyComparer.ComparisonResult result, bool differencesOnly) + { + var sb = new StringBuilder(); + var policies = ValidPolicies(result).ToList(); + + // Header + sb.Append("Section,Item,Different"); + foreach (var p in policies) + { + sb.Append(',').Append(CsvEscape(p.DisplayName)); + } + sb.AppendLine(); + + foreach (var grp in OrderedGroups(result, differencesOnly)) + { + foreach (var entry in grp.OrderBy(e => e.DisplayName, StringComparer.OrdinalIgnoreCase)) + { + sb.Append(CsvEscape(entry.Section)).Append(','); + sb.Append(CsvEscape(entry.DisplayName)).Append(','); + sb.Append(entry.IsDifferent ? "Yes" : "No"); + foreach (var p in policies) + { + entry.Values.TryGetValue(p.DisplayName, out string v); + sb.Append(',').Append(CsvEscape(v ?? "")); + } + sb.AppendLine(); + } + } + + return sb.ToString(); + } + + private static string CsvEscape(string value) + { + if (value == null) return string.Empty; + bool needsQuotes = value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) >= 0; + if (!needsQuotes) return value; + return "\"" + value.Replace("\"", "\"\"") + "\""; + } + + // ----------------- HTML ----------------- + + private static string BuildHtml(PolicyComparer.ComparisonResult result, bool differencesOnly) + { + var sb = new StringBuilder(); + var policies = ValidPolicies(result).ToList(); + + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine("App Control Policy Comparison"); + sb.AppendLine(""); + + sb.Append("

App Control Policy Comparison

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

Policies

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

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

"); + sb.AppendLine(""); + sb.Append(""); + foreach (var p in policies) sb.Append(""); + sb.AppendLine(""); + + foreach (var entry in grp.OrderBy(e => e.DisplayName, StringComparer.OrdinalIgnoreCase)) + { + sb.Append(entry.IsDifferent ? "" : ""); + sb.Append(""); + foreach (var p in policies) + { + entry.Values.TryGetValue(p.DisplayName, out string v); + if (v == null) + { + sb.Append(""); + } + else + { + sb.Append(""); + } + } + sb.AppendLine(""); + } + sb.AppendLine("
Item").Append(HtmlEscape(p.DisplayName)).Append("
").Append(HtmlEscape(entry.DisplayName)).Append("<not present>").Append(HtmlEscape(v)).Append("
"); + } + + sb.AppendLine(""); + return sb.ToString(); + } + + private static string HtmlEscape(string value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + return value.Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace("\"", """); + } + + // ----------------- Markdown ----------------- + + private static string BuildMarkdown(PolicyComparer.ComparisonResult result, bool differencesOnly) + { + var sb = new StringBuilder(); + var policies = ValidPolicies(result).ToList(); + + sb.AppendLine("# App Control Policy Comparison"); + sb.AppendLine(); + sb.Append("_Generated ").Append(DateTime.Now.ToString("u")).AppendLine("_"); + sb.AppendLine(); + + sb.AppendLine("## Policies"); + sb.AppendLine(); + sb.AppendLine("| # | Display Name | Path | Size | Modified (UTC) | SHA-256 |"); + sb.AppendLine("|---|---|---|---|---|---|"); + for (int i = 0; i < policies.Count; i++) + { + var p = policies[i]; + sb.Append("| ").Append(i + 1) + .Append(" | ").Append(MdEscape(p.DisplayName)) + .Append(" | ").Append(MdEscape(p.SourcePath)) + .Append(" | ").Append(FormatBytes(p.FileSizeBytes)) + .Append(" | ").Append(p.LastWriteUtc.ToString("u")) + .Append(" | `").Append(p.Sha256 ?? string.Empty).Append("`") + .AppendLine(" |"); + } + sb.AppendLine(); + + // Summary + var counts = SummaryCounts(result); + sb.Append("**Differences:** "); + sb.AppendLine(string.Join(" · ", + counts.OrderBy(kv => PolicyComparer.OrderOf(kv.Key)) + .ThenBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase) + .Select(kv => kv.Key + ": " + kv.Value))); + sb.AppendLine(); + + foreach (var grp in OrderedGroups(result, differencesOnly)) + { + sb.Append("## ").AppendLine(grp.Key); + sb.AppendLine(); + + sb.Append("| Item"); + foreach (var p in policies) sb.Append(" | ").Append(MdEscape(p.DisplayName)); + sb.AppendLine(" |"); + + sb.Append("|---"); + foreach (var _ in policies) sb.Append("|---"); + sb.AppendLine("|"); + + foreach (var entry in grp.OrderBy(e => e.DisplayName, StringComparer.OrdinalIgnoreCase)) + { + sb.Append("| ").Append(MdEscape(entry.DisplayName)); + foreach (var p in policies) + { + entry.Values.TryGetValue(p.DisplayName, out string v); + sb.Append(" | ").Append(MdEscape(v ?? "")); + } + sb.AppendLine(" |"); + } + sb.AppendLine(); + } + + return sb.ToString(); + } + + private static string MdEscape(string value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + return value.Replace("|", "\\|").Replace("\r", string.Empty).Replace("\n", " "); + } + + // ----------------- Summary helpers ----------------- + + /// + /// Returns a dictionary of section -> number of entries flagged as different. + /// Sections with zero differences are still included. + /// + public static Dictionary SummaryCounts(PolicyComparer.ComparisonResult result) + { + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (result == null) return dict; + + foreach (var entry in result.Entries) + { + if (entry == null || string.IsNullOrEmpty(entry.Section)) continue; + if (!dict.ContainsKey(entry.Section)) dict[entry.Section] = 0; + if (entry.IsDifferent) dict[entry.Section]++; + } + return dict; + } + + private static string FormatBytes(long bytes) + { + if (bytes <= 0) return "0 B"; + string[] units = { "B", "KB", "MB", "GB" }; + double size = bytes; + int unit = 0; + while (size >= 1024 && unit < units.Length - 1) + { + size /= 1024; + unit++; + } + return string.Format("{0:0.##} {1}", size, units[unit]); + } + } +} diff --git a/WDAC-Policy-Wizard/app/src/PolicyCompare_Form.Designer.cs b/WDAC-Policy-Wizard/app/src/PolicyCompare_Form.Designer.cs new file mode 100644 index 00000000..63c27997 --- /dev/null +++ b/WDAC-Policy-Wizard/app/src/PolicyCompare_Form.Designer.cs @@ -0,0 +1,391 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace WDAC_Wizard +{ + partial class PolicyCompare_Form + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + label_Title = new System.Windows.Forms.Label(); + label_Subtitle = new System.Windows.Forms.Label(); + policiesDataGrid = new System.Windows.Forms.DataGridView(); + Column_Index = new System.Windows.Forms.DataGridViewTextBoxColumn(); + Column_Path = new System.Windows.Forms.DataGridViewTextBoxColumn(); + Column_Status = new System.Windows.Forms.DataGridViewTextBoxColumn(); + button_AddPolicy = new System.Windows.Forms.Button(); + button_RemovePolicy = new System.Windows.Forms.Button(); + button_Compare = new System.Windows.Forms.Button(); + button_Export = new System.Windows.Forms.Button(); + button_Close = new System.Windows.Forms.Button(); + checkBox_DifferencesOnly = new System.Windows.Forms.CheckBox(); + label_Section = new System.Windows.Forms.Label(); + comboBox_Section = new System.Windows.Forms.ComboBox(); + label_Filter = new System.Windows.Forms.Label(); + textBox_Filter = new System.Windows.Forms.TextBox(); + label_Status = new System.Windows.Forms.Label(); + progressBar = new System.Windows.Forms.ProgressBar(); + summaryFlow = new System.Windows.Forms.FlowLayoutPanel(); + resultsListView = new System.Windows.Forms.ListView(); + resultsContextMenu = new System.Windows.Forms.ContextMenuStrip(components); + menuItem_CopyCell = new System.Windows.Forms.ToolStripMenuItem(); + menuItem_CopyRow = new System.Windows.Forms.ToolStripMenuItem(); + menuItem_CopyRowTsv = new System.Windows.Forms.ToolStripMenuItem(); + ((System.ComponentModel.ISupportInitialize)policiesDataGrid).BeginInit(); + resultsContextMenu.SuspendLayout(); + SuspendLayout(); + // + // label_Title + // + label_Title.AutoSize = true; + label_Title.Font = new System.Drawing.Font("Tahoma", 14F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0); + label_Title.Location = new System.Drawing.Point(20, 18); + label_Title.Name = "label_Title"; + label_Title.Size = new System.Drawing.Size(389, 29); + label_Title.TabIndex = 0; + label_Title.Text = "Compare App Control Policy Files"; + // + // label_Subtitle + // + label_Subtitle.AutoSize = true; + label_Subtitle.Font = new System.Drawing.Font("Tahoma", 10F); + label_Subtitle.Location = new System.Drawing.Point(22, 55); + label_Subtitle.Name = "label_Subtitle"; + label_Subtitle.Size = new System.Drawing.Size(636, 21); + label_Subtitle.TabIndex = 1; + label_Subtitle.Text = "Add 2 or more policy files (XML, CIP, or P7B) — drag-and-drop is supported. Click Compare to view differences."; + // + // policiesDataGrid + // + policiesDataGrid.AllowUserToAddRows = false; + policiesDataGrid.AllowUserToDeleteRows = false; + policiesDataGrid.AllowDrop = true; + policiesDataGrid.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + policiesDataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + policiesDataGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { Column_Index, Column_Path, Column_Status }); + policiesDataGrid.Location = new System.Drawing.Point(22, 95); + policiesDataGrid.MultiSelect = true; + policiesDataGrid.Name = "policiesDataGrid"; + policiesDataGrid.ReadOnly = true; + policiesDataGrid.RowHeadersWidth = 30; + policiesDataGrid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect; + policiesDataGrid.Size = new System.Drawing.Size(1100, 170); + policiesDataGrid.TabIndex = 2; + // + // Column_Index + // + Column_Index.HeaderText = "#"; + Column_Index.MinimumWidth = 40; + Column_Index.Name = "Column_Index"; + Column_Index.ReadOnly = true; + Column_Index.Width = 50; + // + // Column_Path + // + Column_Path.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + Column_Path.HeaderText = "Policy Path"; + Column_Path.MinimumWidth = 200; + Column_Path.Name = "Column_Path"; + Column_Path.ReadOnly = true; + // + // Column_Status + // + Column_Status.HeaderText = "Status"; + Column_Status.MinimumWidth = 100; + Column_Status.Name = "Column_Status"; + Column_Status.ReadOnly = true; + Column_Status.Width = 220; + // + // button_AddPolicy + // + button_AddPolicy.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); + button_AddPolicy.Font = new System.Drawing.Font("Tahoma", 9F); + button_AddPolicy.Location = new System.Drawing.Point(22, 280); + button_AddPolicy.Name = "button_AddPolicy"; + button_AddPolicy.Size = new System.Drawing.Size(140, 32); + button_AddPolicy.TabIndex = 3; + button_AddPolicy.Text = "+ Add Policy"; + button_AddPolicy.UseVisualStyleBackColor = true; + button_AddPolicy.Click += Button_AddPolicy_Click; + // + // button_RemovePolicy + // + button_RemovePolicy.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); + button_RemovePolicy.Font = new System.Drawing.Font("Tahoma", 9F); + button_RemovePolicy.Location = new System.Drawing.Point(170, 280); + button_RemovePolicy.Name = "button_RemovePolicy"; + button_RemovePolicy.Size = new System.Drawing.Size(140, 32); + button_RemovePolicy.TabIndex = 4; + button_RemovePolicy.Text = "- Remove Policy"; + button_RemovePolicy.UseVisualStyleBackColor = true; + button_RemovePolicy.Click += Button_RemovePolicy_Click; + // + // button_Export + // + button_Export.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + button_Export.Enabled = false; + button_Export.Font = new System.Drawing.Font("Tahoma", 9F); + button_Export.Location = new System.Drawing.Point(835, 280); + button_Export.Name = "button_Export"; + button_Export.Size = new System.Drawing.Size(140, 32); + button_Export.TabIndex = 5; + button_Export.Text = "Export Report…"; + button_Export.UseVisualStyleBackColor = true; + button_Export.Click += Button_Export_Click; + // + // button_Compare + // + button_Compare.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + button_Compare.Font = new System.Drawing.Font("Tahoma", 9F, System.Drawing.FontStyle.Bold); + button_Compare.Location = new System.Drawing.Point(982, 280); + button_Compare.Name = "button_Compare"; + button_Compare.Size = new System.Drawing.Size(140, 32); + button_Compare.TabIndex = 6; + button_Compare.Text = "Compare"; + button_Compare.UseVisualStyleBackColor = true; + button_Compare.Click += Button_Compare_Click; + // + // button_Close + // + button_Close.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + button_Close.DialogResult = System.Windows.Forms.DialogResult.Cancel; + button_Close.Font = new System.Drawing.Font("Tahoma", 9F); + button_Close.Location = new System.Drawing.Point(982, 670); + button_Close.Name = "button_Close"; + button_Close.Size = new System.Drawing.Size(140, 32); + button_Close.TabIndex = 99; + button_Close.Text = "Close"; + button_Close.UseVisualStyleBackColor = true; + button_Close.Click += Button_Close_Click; + // + // checkBox_DifferencesOnly + // + checkBox_DifferencesOnly.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); + checkBox_DifferencesOnly.AutoSize = true; + checkBox_DifferencesOnly.Checked = true; + checkBox_DifferencesOnly.CheckState = System.Windows.Forms.CheckState.Checked; + checkBox_DifferencesOnly.Font = new System.Drawing.Font("Tahoma", 9F); + checkBox_DifferencesOnly.Location = new System.Drawing.Point(22, 333); + checkBox_DifferencesOnly.Name = "checkBox_DifferencesOnly"; + checkBox_DifferencesOnly.Size = new System.Drawing.Size(159, 22); + checkBox_DifferencesOnly.TabIndex = 7; + checkBox_DifferencesOnly.Text = "Show differences only"; + checkBox_DifferencesOnly.UseVisualStyleBackColor = true; + checkBox_DifferencesOnly.CheckedChanged += CheckBox_DifferencesOnly_CheckedChanged; + // + // label_Section + // + label_Section.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); + label_Section.AutoSize = true; + label_Section.Font = new System.Drawing.Font("Tahoma", 9F); + label_Section.Location = new System.Drawing.Point(195, 335); + label_Section.Name = "label_Section"; + label_Section.Size = new System.Drawing.Size(58, 18); + label_Section.TabIndex = 8; + label_Section.Text = "Section:"; + // + // comboBox_Section + // + comboBox_Section.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); + comboBox_Section.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + comboBox_Section.Font = new System.Drawing.Font("Tahoma", 9F); + comboBox_Section.Location = new System.Drawing.Point(258, 332); + comboBox_Section.Name = "comboBox_Section"; + comboBox_Section.Size = new System.Drawing.Size(200, 26); + comboBox_Section.TabIndex = 9; + comboBox_Section.SelectedIndexChanged += ComboBox_Section_SelectedIndexChanged; + // + // label_Filter + // + label_Filter.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); + label_Filter.AutoSize = true; + label_Filter.Font = new System.Drawing.Font("Tahoma", 9F); + label_Filter.Location = new System.Drawing.Point(475, 335); + label_Filter.Name = "label_Filter"; + label_Filter.Size = new System.Drawing.Size(46, 18); + label_Filter.TabIndex = 10; + label_Filter.Text = "Filter:"; + // + // textBox_Filter + // + textBox_Filter.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + textBox_Filter.Font = new System.Drawing.Font("Tahoma", 9F); + textBox_Filter.Location = new System.Drawing.Point(525, 332); + textBox_Filter.Name = "textBox_Filter"; + textBox_Filter.PlaceholderText = "Type to filter visible rows…"; + textBox_Filter.Size = new System.Drawing.Size(597, 26); + textBox_Filter.TabIndex = 11; + textBox_Filter.TextChanged += TextBox_Filter_TextChanged; + // + // summaryFlow + // + summaryFlow.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + summaryFlow.AutoScroll = true; + summaryFlow.BackColor = System.Drawing.Color.Transparent; + summaryFlow.FlowDirection = System.Windows.Forms.FlowDirection.LeftToRight; + summaryFlow.Location = new System.Drawing.Point(22, 365); + summaryFlow.Name = "summaryFlow"; + summaryFlow.Size = new System.Drawing.Size(1100, 38); + summaryFlow.TabIndex = 12; + summaryFlow.WrapContents = false; + // + // label_Status + // + label_Status.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + label_Status.AutoEllipsis = true; + label_Status.Font = new System.Drawing.Font("Tahoma", 9F); + label_Status.ForeColor = System.Drawing.Color.DodgerBlue; + label_Status.Location = new System.Drawing.Point(22, 678); + label_Status.Name = "label_Status"; + label_Status.Size = new System.Drawing.Size(640, 18); + label_Status.TabIndex = 13; + // + // progressBar + // + progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + progressBar.Location = new System.Drawing.Point(670, 675); + progressBar.Name = "progressBar"; + progressBar.Size = new System.Drawing.Size(160, 20); + progressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + progressBar.MarqueeAnimationSpeed = 30; + progressBar.TabIndex = 14; + progressBar.Visible = false; + // + // resultsListView + // + resultsListView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + resultsListView.ContextMenuStrip = resultsContextMenu; + resultsListView.FullRowSelect = true; + resultsListView.GridLines = true; + resultsListView.Location = new System.Drawing.Point(22, 410); + resultsListView.MultiSelect = false; + resultsListView.Name = "resultsListView"; + resultsListView.Size = new System.Drawing.Size(1100, 250); + resultsListView.TabIndex = 15; + resultsListView.UseCompatibleStateImageBehavior = false; + resultsListView.View = System.Windows.Forms.View.Details; + resultsListView.OwnerDraw = false; + // + // resultsContextMenu + // + resultsContextMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + menuItem_CopyCell, + menuItem_CopyRow, + menuItem_CopyRowTsv }); + resultsContextMenu.Name = "resultsContextMenu"; + resultsContextMenu.Size = new System.Drawing.Size(180, 70); + // + // menuItem_CopyCell + // + menuItem_CopyCell.Name = "menuItem_CopyCell"; + menuItem_CopyCell.Size = new System.Drawing.Size(180, 22); + menuItem_CopyCell.Text = "Copy cell"; + menuItem_CopyCell.Click += MenuItem_CopyCell_Click; + // + // menuItem_CopyRow + // + menuItem_CopyRow.Name = "menuItem_CopyRow"; + menuItem_CopyRow.Size = new System.Drawing.Size(180, 22); + menuItem_CopyRow.Text = "Copy row"; + menuItem_CopyRow.Click += MenuItem_CopyRow_Click; + // + // menuItem_CopyRowTsv + // + menuItem_CopyRowTsv.Name = "menuItem_CopyRowTsv"; + menuItem_CopyRowTsv.Size = new System.Drawing.Size(180, 22); + menuItem_CopyRowTsv.Text = "Copy row as TSV"; + menuItem_CopyRowTsv.Click += MenuItem_CopyRowTsv_Click; + // + // PolicyCompare_Form + // + AllowDrop = true; + AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; + ClientSize = new System.Drawing.Size(1144, 710); + Controls.Add(progressBar); + Controls.Add(resultsListView); + Controls.Add(summaryFlow); + Controls.Add(textBox_Filter); + Controls.Add(label_Filter); + Controls.Add(comboBox_Section); + Controls.Add(label_Section); + Controls.Add(label_Status); + Controls.Add(checkBox_DifferencesOnly); + Controls.Add(button_Close); + Controls.Add(button_Compare); + Controls.Add(button_Export); + Controls.Add(button_RemovePolicy); + Controls.Add(button_AddPolicy); + Controls.Add(policiesDataGrid); + Controls.Add(label_Subtitle); + Controls.Add(label_Title); + FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable; + MinimumSize = new System.Drawing.Size(960, 640); + Name = "PolicyCompare_Form"; + StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + Text = "Compare App Control Policies"; + Load += PolicyCompare_Form_Load; + ((System.ComponentModel.ISupportInitialize)policiesDataGrid).EndInit(); + resultsContextMenu.ResumeLayout(false); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private System.Windows.Forms.Label label_Title; + private System.Windows.Forms.Label label_Subtitle; + private System.Windows.Forms.DataGridView policiesDataGrid; + private System.Windows.Forms.DataGridViewTextBoxColumn Column_Index; + private System.Windows.Forms.DataGridViewTextBoxColumn Column_Path; + private System.Windows.Forms.DataGridViewTextBoxColumn Column_Status; + private System.Windows.Forms.Button button_AddPolicy; + private System.Windows.Forms.Button button_RemovePolicy; + private System.Windows.Forms.Button button_Compare; + private System.Windows.Forms.Button button_Export; + private System.Windows.Forms.Button button_Close; + private System.Windows.Forms.CheckBox checkBox_DifferencesOnly; + private System.Windows.Forms.Label label_Section; + private System.Windows.Forms.ComboBox comboBox_Section; + private System.Windows.Forms.Label label_Filter; + private System.Windows.Forms.TextBox textBox_Filter; + private System.Windows.Forms.Label label_Status; + private System.Windows.Forms.ProgressBar progressBar; + private System.Windows.Forms.FlowLayoutPanel summaryFlow; + private System.Windows.Forms.ListView resultsListView; + private System.Windows.Forms.ContextMenuStrip resultsContextMenu; + private System.Windows.Forms.ToolStripMenuItem menuItem_CopyCell; + private System.Windows.Forms.ToolStripMenuItem menuItem_CopyRow; + private System.Windows.Forms.ToolStripMenuItem menuItem_CopyRowTsv; + } +} diff --git a/WDAC-Policy-Wizard/app/src/PolicyCompare_Form.cs b/WDAC-Policy-Wizard/app/src/PolicyCompare_Form.cs new file mode 100644 index 00000000..4bc2e7e5 --- /dev/null +++ b/WDAC-Policy-Wizard/app/src/PolicyCompare_Form.cs @@ -0,0 +1,784 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using System.Windows.Forms; + +namespace WDAC_Wizard +{ + /// + /// Standalone window that lets the user select 2 or more policy files (XML or binary) and view a + /// side-by-side comparison of their settings, rule options, file rules, signers, etc. + /// + public partial class PolicyCompare_Form : Form + { + private const string AllSectionsLabel = "All sections"; + + private readonly List _policyPaths; + private PolicyComparer.ComparisonResult _lastResult; + private BackgroundWorker _compareWorker; + + public PolicyCompare_Form() + { + InitializeComponent(); + _policyPaths = new List(); + } + + // ----------------------------------------------------------------------- + // Lifecycle + // ----------------------------------------------------------------------- + + private void PolicyCompare_Form_Load(object sender, EventArgs e) + { + ApplyTheme(); + + resultsListView.SizeChanged += ResultsListView_SizeChanged; + + // Drag-and-drop on the form and on the policies grid + DragEnter += PolicyCompare_Form_DragEnter; + DragDrop += PolicyCompare_Form_DragDrop; + policiesDataGrid.DragEnter += PolicyCompare_Form_DragEnter; + policiesDataGrid.DragDrop += PolicyCompare_Form_DragDrop; + + // Section combo: start with "All sections"; populated more after a comparison runs + ResetSectionCombo(); + + UpdateButtonStates(); + } + + // ----------------------------------------------------------------------- + // Drag-and-drop + // ----------------------------------------------------------------------- + + private void PolicyCompare_Form_DragEnter(object sender, DragEventArgs e) + { + if (e.Data != null && e.Data.GetDataPresent(DataFormats.FileDrop)) + { + e.Effect = DragDropEffects.Copy; + } + else + { + e.Effect = DragDropEffects.None; + } + } + + private void PolicyCompare_Form_DragDrop(object sender, DragEventArgs e) + { + if (e.Data == null || !e.Data.GetDataPresent(DataFormats.FileDrop)) + { + return; + } + + string[] paths = e.Data.GetData(DataFormats.FileDrop) as string[]; + if (paths == null) return; + + int added = 0; + int skipped = 0; + foreach (var p in paths) + { + if (TryAddPolicyPath(p)) added++; else skipped++; + } + + if (added > 0 || skipped > 0) + { + SetStatus(string.Format("Drag-and-drop: {0} added, {1} skipped.", added, skipped), + added > 0 ? Color.SeaGreen : Color.OrangeRed); + } + UpdateButtonStates(); + } + + // ----------------------------------------------------------------------- + // Add / Remove policies + // ----------------------------------------------------------------------- + + private void Button_AddPolicy_Click(object sender, EventArgs e) + { + using (var openFileDialog = new OpenFileDialog()) + { + openFileDialog.Title = "Choose App Control policies to compare"; + openFileDialog.Filter = "App Control Policy Files (*.xml; *.cip; *.p7b)|*.xml;*.cip;*.p7b|" + + "XML Policy Files (*.xml)|*.xml|" + + "Binary Policy Files (*.cip; *.p7b)|*.cip;*.p7b|" + + "All Files (*.*)|*.*"; + openFileDialog.Multiselect = true; + openFileDialog.RestoreDirectory = true; + openFileDialog.CheckPathExists = true; + + if (openFileDialog.ShowDialog() != DialogResult.OK) + { + return; + } + + foreach (string path in openFileDialog.FileNames) + { + TryAddPolicyPath(path); + } + } + + UpdateButtonStates(); + } + + /// + /// Adds a single policy path to the list (if valid and not already present). Returns true + /// if the path was added, false if it was rejected (duplicate, missing, unsupported). + /// + private bool TryAddPolicyPath(string path) + { + if (string.IsNullOrEmpty(path) || !File.Exists(path)) + { + return false; + } + + string ext = Path.GetExtension(path).ToLowerInvariant(); + if (ext != ".xml" && ext != ".cip" && ext != ".p7b") + { + SetStatus(string.Format("Skipped {0}: unsupported extension.", Path.GetFileName(path)), + Color.OrangeRed); + return false; + } + + if (_policyPaths.Any(p => string.Equals(p, path, StringComparison.OrdinalIgnoreCase))) + { + SetStatus(string.Format("{0} is already added.", Path.GetFileName(path)), Color.OrangeRed); + return false; + } + + _policyPaths.Add(path); + policiesDataGrid.Rows.Add(_policyPaths.Count.ToString(), path, "Pending compare"); + Logger.Log?.AddInfoMsg("PolicyCompare: added policy to compare list: " + path); + return true; + } + + private void Button_RemovePolicy_Click(object sender, EventArgs e) + { + if (policiesDataGrid.SelectedRows.Count == 0) + { + SetStatus("Select a policy row to remove.", Color.OrangeRed); + return; + } + + var rowsToRemove = policiesDataGrid.SelectedRows + .Cast() + .OrderByDescending(r => r.Index) + .ToList(); + + foreach (var row in rowsToRemove) + { + if (row.Index >= 0 && row.Index < _policyPaths.Count) + { + _policyPaths.RemoveAt(row.Index); + } + policiesDataGrid.Rows.RemoveAt(row.Index); + } + + for (int i = 0; i < policiesDataGrid.Rows.Count; i++) + { + policiesDataGrid.Rows[i].Cells[Column_Index.Index].Value = (i + 1).ToString(); + } + + UpdateButtonStates(); + } + + // ----------------------------------------------------------------------- + // Compare (background) + // ----------------------------------------------------------------------- + + private void Button_Compare_Click(object sender, EventArgs e) + { + if (_policyPaths.Count < 2) + { + SetStatus("Add at least two policies to compare.", Color.OrangeRed); + return; + } + + if (_compareWorker != null && _compareWorker.IsBusy) + { + SetStatus("Comparison already in progress…", Color.DodgerBlue); + return; + } + + // Reset prior status text on the grid + for (int i = 0; i < policiesDataGrid.Rows.Count; i++) + { + policiesDataGrid.Rows[i].Cells[Column_Status.Index].Value = "Loading…"; + policiesDataGrid.Rows[i].Cells[Column_Status.Index].Style.ForeColor = Color.DodgerBlue; + } + + // Disable interactive elements during the load + SetCompareControlsEnabled(false); + progressBar.Visible = true; + SetStatus("Loading policies…", Color.DodgerBlue); + Logger.Log?.AddNewSeparationLine("Workflow -- Compare Policies"); + + // Snapshot paths to avoid concurrent modification + var pathsSnapshot = new List(_policyPaths); + + _compareWorker = new BackgroundWorker { WorkerReportsProgress = true }; + _compareWorker.DoWork += CompareWorker_DoWork; + _compareWorker.ProgressChanged += CompareWorker_ProgressChanged; + _compareWorker.RunWorkerCompleted += CompareWorker_RunWorkerCompleted; + _compareWorker.RunWorkerAsync(pathsSnapshot); + } + + private void CompareWorker_DoWork(object sender, DoWorkEventArgs e) + { + var paths = (List)e.Argument; + var loadedPolicies = new List(); + var worker = (BackgroundWorker)sender; + + for (int i = 0; i < paths.Count; i++) + { + var loaded = PolicyComparer.LoadPolicy(paths[i]); + loadedPolicies.Add(loaded); + + int percent = (int)((i + 1) / (double)paths.Count * 100.0); + worker.ReportProgress(percent, new LoadProgress { Index = i, Loaded = loaded }); + } + + var result = PolicyComparer.Compare(loadedPolicies); + e.Result = result; + } + + private class LoadProgress + { + public int Index; + public PolicyComparer.LoadedPolicy Loaded; + } + + private void CompareWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) + { + var p = e.UserState as LoadProgress; + if (p == null) return; + + string statusText = p.Loaded.Policy != null + ? "Loaded" + : ("Error: " + (p.Loaded.LoadError ?? "Unknown")); + + if (p.Index < policiesDataGrid.Rows.Count) + { + policiesDataGrid.Rows[p.Index].Cells[Column_Status.Index].Value = statusText; + policiesDataGrid.Rows[p.Index].Cells[Column_Status.Index].Style.ForeColor = + p.Loaded.Policy != null ? Color.SeaGreen : Color.Firebrick; + } + } + + private void CompareWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) + { + progressBar.Visible = false; + + try + { + if (e.Error != null) + { + Logger.Log?.AddErrorMsg("PolicyCompare: comparison failed", e.Error); + SetStatus("Comparison failed: " + e.Error.Message, Color.Firebrick); + _lastResult = null; + RenderComparison(null); + return; + } + + var result = e.Result as PolicyComparer.ComparisonResult; + int successfullyLoaded = result?.Policies.Count(lp => lp.Policy != null) ?? 0; + if (result == null || successfullyLoaded < 2) + { + SetStatus("Need at least two policies that load successfully to compare.", Color.Firebrick); + _lastResult = null; + RenderComparison(null); + return; + } + + _lastResult = result; + RebuildSectionCombo(); + RenderComparison(_lastResult); + BuildSummaryStrip(_lastResult); + + int diffCount = _lastResult.Entries.Count(en => en.IsDifferent); + SetStatus(string.Format("Compared {0} policies. {1} differences found.", + successfullyLoaded, diffCount), + diffCount == 0 ? Color.SeaGreen : Color.DodgerBlue); + } + finally + { + _compareWorker = null; + // Re-enable controls AFTER _lastResult has been assigned, so the Export button + // correctly reflects whether a comparison result is available. + SetCompareControlsEnabled(true); + } + } + + private void SetCompareControlsEnabled(bool enabled) + { + button_Compare.Enabled = enabled && _policyPaths.Count >= 2; + button_AddPolicy.Enabled = enabled; + button_RemovePolicy.Enabled = enabled && _policyPaths.Count > 0; + button_Export.Enabled = enabled && _lastResult != null + && _lastResult.Entries != null + && _lastResult.Entries.Count > 0; + } + + // ----------------------------------------------------------------------- + // Section combo / Filter / Differences-only + // ----------------------------------------------------------------------- + + private void ResetSectionCombo() + { + comboBox_Section.BeginUpdate(); + try + { + comboBox_Section.Items.Clear(); + comboBox_Section.Items.Add(AllSectionsLabel); + comboBox_Section.SelectedIndex = 0; + } + finally + { + comboBox_Section.EndUpdate(); + } + } + + private void RebuildSectionCombo() + { + if (_lastResult == null) + { + ResetSectionCombo(); + return; + } + + string previous = comboBox_Section.SelectedItem as string; + + comboBox_Section.BeginUpdate(); + try + { + comboBox_Section.Items.Clear(); + comboBox_Section.Items.Add(AllSectionsLabel); + + var sections = _lastResult.Entries + .Select(en => en.Section) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(PolicyComparer.OrderOf) + .ThenBy(s => s, StringComparer.OrdinalIgnoreCase) + .ToArray(); + foreach (var s in sections) comboBox_Section.Items.Add(s); + + int idx = 0; + if (!string.IsNullOrEmpty(previous)) + { + int found = comboBox_Section.Items.IndexOf(previous); + if (found >= 0) idx = found; + } + comboBox_Section.SelectedIndex = idx; + } + finally + { + comboBox_Section.EndUpdate(); + } + } + + private void ComboBox_Section_SelectedIndexChanged(object sender, EventArgs e) + { + if (_lastResult != null) RenderComparison(_lastResult); + } + + private void TextBox_Filter_TextChanged(object sender, EventArgs e) + { + if (_lastResult != null) RenderComparison(_lastResult); + } + + private void CheckBox_DifferencesOnly_CheckedChanged(object sender, EventArgs e) + { + if (_lastResult != null) RenderComparison(_lastResult); + } + + // ----------------------------------------------------------------------- + // Summary header strip + // ----------------------------------------------------------------------- + + private void BuildSummaryStrip(PolicyComparer.ComparisonResult result) + { + summaryFlow.Controls.Clear(); + if (result == null) return; + + var counts = PolicyCompareReport.SummaryCounts(result); + var ordered = counts.OrderBy(kv => PolicyComparer.OrderOf(kv.Key)) + .ThenBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase); + + foreach (var kv in ordered) + { + var btn = new Button + { + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + FlatStyle = FlatStyle.Flat, + Margin = new Padding(0, 4, 6, 0), + Padding = new Padding(8, 2, 8, 2), + Tag = kv.Key, + Text = string.Format("{0}: {1}", kv.Key, kv.Value), + UseVisualStyleBackColor = false, + Cursor = Cursors.Hand, + }; + + btn.FlatAppearance.BorderColor = Color.LightGray; + if (kv.Value > 0) + { + btn.BackColor = Color.FromArgb(231, 240, 250); + btn.ForeColor = Color.Black; + } + else + { + btn.BackColor = Color.FromArgb(240, 240, 240); + btn.ForeColor = Color.DimGray; + } + + btn.Click += SummaryButton_Click; + summaryFlow.Controls.Add(btn); + } + } + + private void SummaryButton_Click(object sender, EventArgs e) + { + if (_lastResult == null) return; + var btn = sender as Button; + if (btn == null) return; + + string section = btn.Tag as string; + if (string.IsNullOrEmpty(section)) return; + + // Set the section combo to that section. If not present (because filter was hiding it), + // fall back to "All sections". + int idx = comboBox_Section.Items.IndexOf(section); + comboBox_Section.SelectedIndex = idx >= 0 ? idx : 0; + + // Scroll the listview to the first item in that section + ScrollToSection(section); + } + + private void ScrollToSection(string section) + { + foreach (ListViewItem item in resultsListView.Items) + { + if (item.Group != null && string.Equals(item.Group.Header, section, StringComparison.OrdinalIgnoreCase)) + { + item.EnsureVisible(); + item.Selected = true; + item.Focused = true; + resultsListView.Focus(); + break; + } + } + } + + // ----------------------------------------------------------------------- + // Render comparison + // ----------------------------------------------------------------------- + + private void RenderComparison(PolicyComparer.ComparisonResult result) + { + resultsListView.BeginUpdate(); + try + { + resultsListView.Items.Clear(); + resultsListView.Groups.Clear(); + resultsListView.Columns.Clear(); + + if (result == null || result.Policies.Count == 0) + { + return; + } + + resultsListView.Columns.Add("Section", 160); + resultsListView.Columns.Add("Item", 260); + + var validPolicies = result.Policies.Where(p => p.Policy != null).ToList(); + foreach (var policy in validPolicies) + { + resultsListView.Columns.Add(policy.DisplayName, 200); + } + + ResizePolicyColumns(); + + bool diffOnly = checkBox_DifferencesOnly.Checked; + string sectionFilter = comboBox_Section.SelectedItem as string; + string textFilter = textBox_Filter.Text?.Trim(); + + IEnumerable source = result.Entries; + + if (diffOnly) + { + source = source.Where(en => en.IsDifferent); + } + + if (!string.IsNullOrEmpty(sectionFilter) + && !string.Equals(sectionFilter, AllSectionsLabel, StringComparison.OrdinalIgnoreCase)) + { + source = source.Where(en => string.Equals(en.Section, sectionFilter, StringComparison.OrdinalIgnoreCase)); + } + + if (!string.IsNullOrEmpty(textFilter)) + { + source = source.Where(en => MatchesTextFilter(en, validPolicies, textFilter)); + } + + var grouped = source.GroupBy(en => en.Section) + .OrderBy(g => PolicyComparer.OrderOf(g.Key)) + .ThenBy(g => g.Key, StringComparer.OrdinalIgnoreCase); + + foreach (var sectionGroup in grouped) + { + var lvGroup = new ListViewGroup(sectionGroup.Key, sectionGroup.Key); + resultsListView.Groups.Add(lvGroup); + + foreach (var entry in sectionGroup.OrderBy(en => en.DisplayName, StringComparer.OrdinalIgnoreCase)) + { + var item = new ListViewItem(entry.Section) { Group = lvGroup }; + item.SubItems.Add(entry.DisplayName); + + foreach (var policy in validPolicies) + { + entry.Values.TryGetValue(policy.DisplayName, out string value); + item.SubItems.Add(value ?? ""); + } + + if (entry.IsDifferent) + { + item.BackColor = Color.FromArgb(255, 245, 220); + item.ForeColor = Color.Black; + } + + resultsListView.Items.Add(item); + } + } + } + finally + { + resultsListView.EndUpdate(); + } + } + + private static bool MatchesTextFilter(PolicyComparer.ComparisonEntry entry, + List validPolicies, + string filter) + { + if (entry.Section != null && entry.Section.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0) return true; + if (entry.DisplayName != null && entry.DisplayName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0) return true; + + foreach (var p in validPolicies) + { + entry.Values.TryGetValue(p.DisplayName, out string v); + if (!string.IsNullOrEmpty(v) && v.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0) + { + return true; + } + } + return false; + } + + // ----------------------------------------------------------------------- + // Context menu (copy) + // ----------------------------------------------------------------------- + + private void MenuItem_CopyCell_Click(object sender, EventArgs e) + { + var hit = resultsListView.PointToClient(MousePosition); + var info = resultsListView.HitTest(hit); + if (info?.SubItem != null && !string.IsNullOrEmpty(info.SubItem.Text)) + { + SafeSetClipboard(info.SubItem.Text); + return; + } + + // Fall back: copy first selected row first cell + if (resultsListView.SelectedItems.Count > 0) + { + SafeSetClipboard(resultsListView.SelectedItems[0].Text); + } + } + + private void MenuItem_CopyRow_Click(object sender, EventArgs e) + { + CopySelectedRow(separator: " | "); + } + + private void MenuItem_CopyRowTsv_Click(object sender, EventArgs e) + { + CopySelectedRow(separator: "\t"); + } + + private void CopySelectedRow(string separator) + { + if (resultsListView.SelectedItems.Count == 0) return; + var item = resultsListView.SelectedItems[0]; + var sb = new StringBuilder(); + for (int i = 0; i < item.SubItems.Count; i++) + { + if (i > 0) sb.Append(separator); + sb.Append(item.SubItems[i].Text); + } + SafeSetClipboard(sb.ToString()); + } + + private static void SafeSetClipboard(string text) + { + try { Clipboard.SetText(text ?? string.Empty); } + catch (Exception ex) { Logger.Log?.AddWarningMsg("PolicyCompare: clipboard set failed: " + ex.Message); } + } + + // ----------------------------------------------------------------------- + // Export + // ----------------------------------------------------------------------- + + private void Button_Export_Click(object sender, EventArgs e) + { + if (_lastResult == null || _lastResult.Entries.Count == 0) + { + SetStatus("Run a comparison first, then export.", Color.OrangeRed); + return; + } + + using (var sfd = new SaveFileDialog()) + { + sfd.Title = "Export comparison report"; + sfd.Filter = "HTML Report (*.html)|*.html|" + + "CSV File (*.csv)|*.csv|" + + "Markdown File (*.md)|*.md"; + sfd.FilterIndex = 1; + sfd.AddExtension = true; + sfd.RestoreDirectory = true; + sfd.FileName = "policy-comparison"; + + if (sfd.ShowDialog(this) != DialogResult.OK) return; + + PolicyCompareReport.ReportFormat format; + string ext = Path.GetExtension(sfd.FileName).ToLowerInvariant(); + switch (ext) + { + case ".csv": format = PolicyCompareReport.ReportFormat.Csv; break; + case ".md": format = PolicyCompareReport.ReportFormat.Markdown; break; + case ".html": + case ".htm": + default: format = PolicyCompareReport.ReportFormat.Html; break; + } + + try + { + PolicyCompareReport.Write(_lastResult, sfd.FileName, format, + checkBox_DifferencesOnly.Checked); + SetStatus("Exported report to " + sfd.FileName, Color.SeaGreen); + Logger.Log?.AddInfoMsg("PolicyCompare: exported " + format + " report to " + sfd.FileName); + } + catch (Exception ex) + { + Logger.Log?.AddErrorMsg("PolicyCompare: export failed", ex); + SetStatus("Export failed: " + ex.Message, Color.Firebrick); + } + } + } + + // ----------------------------------------------------------------------- + // Misc + // ----------------------------------------------------------------------- + + private void Button_Close_Click(object sender, EventArgs e) + { + Close(); + } + + private void ResultsListView_SizeChanged(object sender, EventArgs e) + { + ResizePolicyColumns(); + } + + private void ResizePolicyColumns() + { + const int fixedColumnsWidth = 160 + 260; + const int verticalScrollAllowance = 24; + + if (resultsListView.Columns.Count <= 2) + { + return; + } + + int policyColumnCount = resultsListView.Columns.Count - 2; + int available = Math.Max(0, resultsListView.ClientSize.Width - fixedColumnsWidth - verticalScrollAllowance); + int width = Math.Max(140, available / policyColumnCount); + + for (int i = 2; i < resultsListView.Columns.Count; i++) + { + resultsListView.Columns[i].Width = width; + } + } + + private void UpdateButtonStates() + { + button_Compare.Enabled = _policyPaths.Count >= 2 && (_compareWorker == null || !_compareWorker.IsBusy); + button_RemovePolicy.Enabled = _policyPaths.Count > 0; + button_Export.Enabled = _lastResult != null + && _lastResult.Entries != null + && _lastResult.Entries.Count > 0; + } + + private void SetStatus(string text, Color color) + { + label_Status.Text = text; + label_Status.ForeColor = color; + } + + /// + /// Apply Light/Dark theming consistent with other forms in the Wizard. + /// + private void ApplyTheme() + { + bool dark = Properties.Settings.Default.useDarkMode; + + BackColor = dark ? Color.FromArgb(15, 15, 15) : Color.White; + ForeColor = dark ? Color.White : Color.Black; + + foreach (Control ctrl in Controls) + { + if (ctrl is Label || ctrl is CheckBox) + { + ctrl.BackColor = BackColor; + if (ctrl != label_Status) // keep status label highlight color + { + ctrl.ForeColor = ForeColor; + } + } + else if (ctrl is TextBox tb) + { + tb.BackColor = dark ? Color.FromArgb(32, 32, 32) : Color.White; + tb.ForeColor = dark ? Color.White : Color.Black; + } + else if (ctrl is ComboBox cb) + { + cb.BackColor = dark ? Color.FromArgb(32, 32, 32) : Color.White; + cb.ForeColor = dark ? Color.White : Color.Black; + } + else if (ctrl is Button btn) + { + btn.FlatStyle = FlatStyle.Flat; + btn.FlatAppearance.BorderColor = dark ? Color.DodgerBlue : Color.Black; + btn.BackColor = dark ? Color.Transparent : Color.WhiteSmoke; + btn.ForeColor = dark ? Color.DodgerBlue : Color.Black; + } + } + + policiesDataGrid.BackgroundColor = dark ? Color.FromArgb(15, 15, 15) : Color.White; + policiesDataGrid.DefaultCellStyle.BackColor = dark ? Color.FromArgb(32, 32, 32) : Color.White; + policiesDataGrid.DefaultCellStyle.ForeColor = dark ? Color.White : Color.Black; + policiesDataGrid.AlternatingRowsDefaultCellStyle.BackColor = dark ? Color.FromArgb(24, 24, 24) : Color.WhiteSmoke; + policiesDataGrid.ColumnHeadersDefaultCellStyle.BackColor = dark ? Color.Black : Color.WhiteSmoke; + policiesDataGrid.ColumnHeadersDefaultCellStyle.ForeColor = dark ? Color.White : Color.Black; + policiesDataGrid.EnableHeadersVisualStyles = false; + policiesDataGrid.GridColor = dark ? Color.LightSlateGray : Color.LightGray; + + resultsListView.BackColor = dark ? Color.FromArgb(15, 15, 15) : Color.White; + resultsListView.ForeColor = dark ? Color.White : Color.Black; + + summaryFlow.BackColor = BackColor; + } + } +} diff --git a/WDAC-Policy-Wizard/app/src/PolicyComparer.cs b/WDAC-Policy-Wizard/app/src/PolicyComparer.cs new file mode 100644 index 00000000..510f6324 --- /dev/null +++ b/WDAC-Policy-Wizard/app/src/PolicyComparer.cs @@ -0,0 +1,617 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Xml.Serialization; + +namespace WDAC_Wizard +{ + /// + /// Loads one or more App Control policies from disk and compares them, producing a hierarchical + /// list of differences grouped by section (metadata, rule options, file rules, signers, etc.). + /// + internal static class PolicyComparer + { + /// + /// Canonical ordering of comparison sections. Sections not in this list are treated as + /// having a sort key larger than any listed section and are sorted alphabetically among + /// themselves at the end. Centralized here so the renderer, exporter, and summary header + /// all agree on order. + /// + public static readonly string[] SectionOrder = new[] + { + "Rule Options", + "Policy Metadata", + "Settings", + "Signing Scenarios", + "Signers", + "Update Policy Signers", + "Supplemental Policy Signers", + "CI Signers", + "EKUs", + "File Rules", + }; + + /// + /// Returns the canonical sort key for a section. Lower numbers sort first; sections not in + /// the canonical list return int.MaxValue so they fall to the end (still alphabetical + /// among themselves when ThenBy is applied). + /// + public static int OrderOf(string section) + { + if (section == null) return int.MaxValue; + int idx = Array.IndexOf(SectionOrder, section); + return idx < 0 ? int.MaxValue : idx; + } + + /// + /// Represents a single policy that has been loaded from disk and prepared for comparison. + /// + public class LoadedPolicy + { + public string SourcePath { get; set; } + public string DisplayName { get; set; } + public SiPolicy Policy { get; set; } + public string LoadError { get; set; } + + // File metadata for traceability and reports + public long FileSizeBytes { get; set; } + public DateTime LastWriteUtc { get; set; } + public string Sha256 { get; set; } + } + + /// + /// A single comparison entry: identifies an item (e.g. a rule option, file rule, signer) and which + /// policies contain it. The Values dictionary stores per-policy display strings. If a policy does + /// not contain the item, its value is null. + /// + public class ComparisonEntry + { + public string Section { get; set; } // e.g. "Rule Options", "File Rules" + public string Key { get; set; } // unique within section + public string DisplayName { get; set; } // human-readable name shown in UI + public Dictionary Values { get; set; } // policyDisplayName -> value (null if missing) + public bool IsDifferent { get; set; } // true if not all policies have the same value + + public ComparisonEntry() + { + Values = new Dictionary(); + } + } + + /// + /// Result of a multi-policy comparison: list of policies that were compared and a list of + /// every entry seen in any of the policies for each section. + /// + public class ComparisonResult + { + public List Policies { get; set; } = new List(); + public List Entries { get; set; } = new List(); + } + + /// + /// Loads a policy file from disk. Supports both XML policies and binary (.cip/.p7b) policies. + /// + /// Path to the policy file + /// LoadedPolicy with either a populated Policy or a LoadError + public static LoadedPolicy LoadPolicy(string policyPath) + { + var loaded = new LoadedPolicy + { + SourcePath = policyPath, + DisplayName = string.IsNullOrEmpty(policyPath) ? "" : Path.GetFileName(policyPath) + }; + + if (string.IsNullOrEmpty(policyPath) || !File.Exists(policyPath)) + { + loaded.LoadError = "File not found."; + return loaded; + } + + try + { + // Capture file metadata up front (best-effort). These are useful in the report + // even if parsing later fails. + try + { + var fi = new FileInfo(policyPath); + loaded.FileSizeBytes = fi.Length; + loaded.LastWriteUtc = fi.LastWriteTimeUtc; + loaded.Sha256 = ComputeSha256(policyPath); + } + catch (Exception metaEx) + { + Logger.Log?.AddWarningMsg("PolicyComparer.LoadPolicy: failed to read file metadata: " + metaEx.Message); + } + + string xmlPath = policyPath; + + // Convert binary policies to XML on disk first, then deserialize + if (Helper.IsBinaryPolicyFile(policyPath)) + { + xmlPath = BinaryPolicyConverter.ConvertToXml(policyPath); + if (string.IsNullOrEmpty(xmlPath)) + { + loaded.LoadError = "Unable to convert binary policy to XML."; + return loaded; + } + } + + SiPolicy siPolicy = Helper.DeserializeXMLtoPolicy(xmlPath); + if (siPolicy == null) + { + loaded.LoadError = "Unable to parse policy XML."; + return loaded; + } + + loaded.Policy = siPolicy; + } + catch (Exception ex) + { + Logger.Log?.AddErrorMsg("PolicyComparer.LoadPolicy caught the following exception", ex); + loaded.LoadError = ex.Message; + } + + return loaded; + } + + private static string ComputeSha256(string path) + { + using (var stream = File.OpenRead(path)) + using (var sha = SHA256.Create()) + { + byte[] hash = sha.ComputeHash(stream); + var sb = new StringBuilder(hash.Length * 2); + for (int i = 0; i < hash.Length; i++) + { + sb.Append(hash[i].ToString("X2")); + } + return sb.ToString(); + } + } + + /// + /// Compares the provided list of policies and returns a ComparisonResult containing per-section + /// entries with the value from each policy. + /// + public static ComparisonResult Compare(List policies) + { + var result = new ComparisonResult(); + if (policies == null || policies.Count == 0) + { + return result; + } + + // Ensure unique display names so duplicate file names do not collide as dictionary keys + EnsureUniqueDisplayNames(policies); + result.Policies.AddRange(policies); + + // Only operate on policies that loaded successfully + var validPolicies = policies.Where(p => p.Policy != null).ToList(); + + CompareMetadata(validPolicies, result); + CompareRuleOptions(validPolicies, result); + CompareFileRules(validPolicies, result); + CompareSigners(validPolicies, result); + CompareSigningScenarios(validPolicies, result); + CompareUpdateSigners(validPolicies, result); + CompareSupplementalSigners(validPolicies, result); + CompareCiSigners(validPolicies, result); + CompareSettings(validPolicies, result); + CompareEKUs(validPolicies, result); + + // Mark which entries are different across policies + foreach (var entry in result.Entries) + { + entry.IsDifferent = ComputeIsDifferent(entry, validPolicies); + } + + return result; + } + + private static void EnsureUniqueDisplayNames(List policies) + { + var seen = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var p in policies) + { + string baseName = string.IsNullOrEmpty(p.DisplayName) ? "policy" : p.DisplayName; + if (!seen.ContainsKey(baseName)) + { + seen[baseName] = 1; + p.DisplayName = baseName; + } + else + { + seen[baseName]++; + p.DisplayName = string.Format("{0} ({1})", baseName, seen[baseName]); + } + } + } + + private static bool ComputeIsDifferent(ComparisonEntry entry, List validPolicies) + { + // An entry is "different" if any valid policy has a value that differs from any other, + // including the case where some policies are missing the item entirely. + string firstValue = null; + bool firstSet = false; + foreach (var p in validPolicies) + { + entry.Values.TryGetValue(p.DisplayName, out string v); + if (!firstSet) + { + firstValue = v; + firstSet = true; + } + else if (!string.Equals(firstValue, v, StringComparison.Ordinal)) + { + return true; + } + } + return false; + } + + private static void AddOrUpdateEntry(ComparisonResult result, + string section, + string key, + string displayName, + string policyName, + string value) + { + var entry = result.Entries.FirstOrDefault(e => e.Section == section && e.Key == key); + if (entry == null) + { + entry = new ComparisonEntry + { + Section = section, + Key = key, + DisplayName = displayName, + }; + result.Entries.Add(entry); + } + + // If the same key was already added for this policy, prefer the latest non-null value + entry.Values[policyName] = value; + } + + private static void CompareMetadata(List policies, ComparisonResult result) + { + const string section = "Policy Metadata"; + foreach (var p in policies) + { + AddOrUpdateEntry(result, section, "FriendlyName", "Friendly Name", + p.DisplayName, p.Policy.FriendlyName); + AddOrUpdateEntry(result, section, "PolicyType", "Policy Type", + p.DisplayName, + p.Policy.PolicyTypeSpecified ? p.Policy.PolicyType.ToString() : null); + AddOrUpdateEntry(result, section, "PolicyID", "Policy ID", + p.DisplayName, p.Policy.PolicyID); + AddOrUpdateEntry(result, section, "BasePolicyID", "Base Policy ID", + p.DisplayName, p.Policy.BasePolicyID); + AddOrUpdateEntry(result, section, "PolicyTypeID", "Policy Type ID", + p.DisplayName, p.Policy.PolicyTypeID); + AddOrUpdateEntry(result, section, "VersionEx", "Version", + p.DisplayName, p.Policy.VersionEx); + AddOrUpdateEntry(result, section, "PlatformID", "Platform ID", + p.DisplayName, p.Policy.PlatformID); + AddOrUpdateEntry(result, section, "HvciOptions", "HVCI Options", + p.DisplayName, + p.Policy.HvciOptionsSpecified ? p.Policy.HvciOptions.ToString() : null); + } + } + + private static void CompareRuleOptions(List policies, ComparisonResult result) + { + const string section = "Rule Options"; + + // Pre-compute friendly name for every known OptionType so the user always sees + // every option, even if no policy has it enabled. + var optionTypeToFriendly = GetOptionTypeFriendlyNames(); + + // First, ensure every option appears as a row, defaulting to "No" for each policy. + foreach (var kvp in optionTypeToFriendly.OrderBy(k => k.Value, StringComparer.OrdinalIgnoreCase)) + { + foreach (var p in policies) + { + AddOrUpdateEntry(result, section, kvp.Key.ToString(), kvp.Value, p.DisplayName, "No"); + } + } + + // Now mark each option present on a policy as "Yes". + foreach (var p in policies) + { + if (p.Policy.Rules == null) continue; + foreach (var rule in p.Policy.Rules) + { + if (rule == null) continue; + string key = rule.Item.ToString(); + string display = optionTypeToFriendly.TryGetValue(rule.Item, out string friendly) + ? friendly + : key; + AddOrUpdateEntry(result, section, key, display, p.DisplayName, "Yes"); + } + } + } + + /// + /// Builds a map from each OptionType enum value to its XML friendly name (e.g. + /// OptionType.EnabledUMCI -> "Enabled:UMCI"). Falls back to the enum name when no + /// XmlEnumAttribute is present. + /// + private static Dictionary GetOptionTypeFriendlyNames() + { + var map = new Dictionary(); + Type t = typeof(OptionType); + foreach (OptionType value in Enum.GetValues(t)) + { + FieldInfo field = t.GetField(value.ToString()); + string friendly = value.ToString(); + if (field != null) + { + var attr = field.GetCustomAttribute(); + if (attr != null && !string.IsNullOrEmpty(attr.Name)) + { + friendly = attr.Name; + } + } + map[value] = friendly; + } + return map; + } + + private static void CompareFileRules(List policies, ComparisonResult result) + { + const string section = "File Rules"; + foreach (var p in policies) + { + if (p.Policy.FileRules == null) continue; + foreach (var rule in p.Policy.FileRules) + { + if (rule == null) continue; + + string id = null; + string friendly = null; + string typeName = rule.GetType().Name; + string description = null; + + switch (rule) + { + case Allow a: + id = a.ID; + friendly = a.FriendlyName; + description = BuildFileRuleDescription("Allow", a.FilePath, a.FileName, + a.PackageFamilyName, a.MinimumFileVersion, + a.MaximumFileVersion, a.Hash); + break; + case Deny d: + id = d.ID; + friendly = d.FriendlyName; + description = BuildFileRuleDescription("Deny", d.FilePath, d.FileName, + d.PackageFamilyName, d.MinimumFileVersion, + d.MaximumFileVersion, d.Hash); + break; + case FileAttrib fa: + id = fa.ID; + friendly = fa.FriendlyName; + description = BuildFileRuleDescription("FileAttrib", fa.FilePath, fa.FileName, + fa.PackageFamilyName, fa.MinimumFileVersion, + fa.MaximumFileVersion, fa.Hash); + break; + case FileRule fr: + id = fr.ID; + friendly = fr.FriendlyName; + description = BuildFileRuleDescription(fr.Type.ToString(), fr.FilePath, + fr.FileName, fr.PackageFamilyName, + fr.MinimumFileVersion, fr.MaximumFileVersion, + fr.Hash); + break; + } + + string key = string.IsNullOrEmpty(id) + ? string.Format("{0}|{1}", typeName, friendly ?? description ?? Guid.NewGuid().ToString()) + : id; + string display = string.IsNullOrEmpty(friendly) ? (id ?? typeName) : friendly; + + AddOrUpdateEntry(result, section, key, display, p.DisplayName, description); + } + } + } + + private static string BuildFileRuleDescription(string ruleType, string filePath, string fileName, + string packageFamilyName, string minimumVersion, + string maximumVersion, byte[] hash) + { + var parts = new List { ruleType }; + if (!string.IsNullOrEmpty(filePath)) parts.Add("Path=" + filePath); + if (!string.IsNullOrEmpty(fileName)) parts.Add("FileName=" + fileName); + if (!string.IsNullOrEmpty(packageFamilyName)) parts.Add("PFN=" + packageFamilyName); + if (!string.IsNullOrEmpty(minimumVersion)) parts.Add("MinVer=" + minimumVersion); + if (!string.IsNullOrEmpty(maximumVersion)) parts.Add("MaxVer=" + maximumVersion); + if (hash != null && hash.Length > 0) + { + parts.Add("Hash=" + BitConverter.ToString(hash).Replace("-", string.Empty)); + } + return string.Join("; ", parts); + } + + private static void CompareSigners(List policies, ComparisonResult result) + { + const string section = "Signers"; + foreach (var p in policies) + { + if (p.Policy.Signers == null) continue; + foreach (var signer in p.Policy.Signers) + { + if (signer == null) continue; + string key = string.IsNullOrEmpty(signer.ID) + ? "Signer|" + (signer.Name ?? Guid.NewGuid().ToString()) + : signer.ID; + string display = string.IsNullOrEmpty(signer.Name) ? (signer.ID ?? "Signer") : signer.Name; + string description = BuildSignerDescription(signer); + AddOrUpdateEntry(result, section, key, display, p.DisplayName, description); + } + } + } + + private static string BuildSignerDescription(Signer signer) + { + var parts = new List(); + if (!string.IsNullOrEmpty(signer.Name)) parts.Add("Name=" + signer.Name); + if (signer.CertRoot != null && signer.CertRoot.Value != null && signer.CertRoot.Value.Length > 0) + { + parts.Add("CertRoot=" + BitConverter.ToString(signer.CertRoot.Value).Replace("-", string.Empty)); + } + if (signer.CertPublisher != null && !string.IsNullOrEmpty(signer.CertPublisher.Value)) + { + parts.Add("Publisher=" + signer.CertPublisher.Value); + } + if (signer.CertIssuer != null && !string.IsNullOrEmpty(signer.CertIssuer.Value)) + { + parts.Add("Issuer=" + signer.CertIssuer.Value); + } + if (signer.FileAttribRef != null && signer.FileAttribRef.Length > 0) + { + parts.Add("FileAttribRefs=" + signer.FileAttribRef.Length); + } + if (signer.SignTimeAfterSpecified) + { + parts.Add("SignTimeAfter=" + signer.SignTimeAfter.ToString("o")); + } + return parts.Count == 0 ? "Signer" : string.Join("; ", parts); + } + + private static void CompareSigningScenarios(List policies, ComparisonResult result) + { + const string section = "Signing Scenarios"; + foreach (var p in policies) + { + if (p.Policy.SigningScenarios == null) continue; + foreach (var scenario in p.Policy.SigningScenarios) + { + if (scenario == null) continue; + string scenarioLabel = ScenarioLabel(scenario.Value); + string key = string.IsNullOrEmpty(scenario.ID) + ? "Scenario|" + scenarioLabel + : scenario.ID; + string display = string.IsNullOrEmpty(scenario.FriendlyName) + ? scenarioLabel + : scenario.FriendlyName + " (" + scenarioLabel + ")"; + + int allowedSigners = scenario.ProductSigners?.AllowedSigners?.AllowedSigner?.Length ?? 0; + int deniedSigners = scenario.ProductSigners?.DeniedSigners?.DeniedSigner?.Length ?? 0; + int fileRulesRefs = scenario.ProductSigners?.FileRulesRef?.FileRuleRef?.Length ?? 0; + + string description = string.Format( + "Scenario={0}; AllowedSigners={1}; DeniedSigners={2}; FileRuleRefs={3}", + scenarioLabel, allowedSigners, deniedSigners, fileRulesRefs); + + AddOrUpdateEntry(result, section, key, display, p.DisplayName, description); + } + } + } + + private static string ScenarioLabel(byte value) + { + switch (value) + { + case 12: return "User Mode"; + case 131: return "Kernel Mode"; + default: return "Scenario " + value; + } + } + + private static void CompareUpdateSigners(List policies, ComparisonResult result) + { + const string section = "Update Policy Signers"; + foreach (var p in policies) + { + if (p.Policy.UpdatePolicySigners == null) continue; + foreach (var s in p.Policy.UpdatePolicySigners) + { + if (s == null) continue; + string key = string.IsNullOrEmpty(s.SignerId) ? "UpdateSigner|" + Guid.NewGuid() : s.SignerId; + AddOrUpdateEntry(result, section, key, s.SignerId ?? key, p.DisplayName, "Referenced"); + } + } + } + + private static void CompareSupplementalSigners(List policies, ComparisonResult result) + { + const string section = "Supplemental Policy Signers"; + foreach (var p in policies) + { + if (p.Policy.SupplementalPolicySigners == null) continue; + foreach (var s in p.Policy.SupplementalPolicySigners) + { + if (s == null) continue; + string key = string.IsNullOrEmpty(s.SignerId) ? "SuppSigner|" + Guid.NewGuid() : s.SignerId; + AddOrUpdateEntry(result, section, key, s.SignerId ?? key, p.DisplayName, "Referenced"); + } + } + } + + private static void CompareCiSigners(List policies, ComparisonResult result) + { + const string section = "CI Signers"; + foreach (var p in policies) + { + if (p.Policy.CiSigners == null) continue; + foreach (var s in p.Policy.CiSigners) + { + if (s == null) continue; + string key = string.IsNullOrEmpty(s.SignerId) ? "CiSigner|" + Guid.NewGuid() : s.SignerId; + AddOrUpdateEntry(result, section, key, s.SignerId ?? key, p.DisplayName, "Referenced"); + } + } + } + + private static void CompareSettings(List policies, ComparisonResult result) + { + const string section = "Settings"; + foreach (var p in policies) + { + if (p.Policy.Settings == null) continue; + foreach (var setting in p.Policy.Settings) + { + if (setting == null) continue; + string key = string.Format("{0}|{1}|{2}", + setting.Provider ?? string.Empty, + setting.Key ?? string.Empty, + setting.ValueName ?? string.Empty); + string display = string.Format("{0} / {1} / {2}", + setting.Provider ?? "", + setting.Key ?? "", + setting.ValueName ?? ""); + string value = setting.Value != null && setting.Value.Item != null + ? setting.Value.Item.ToString() + : ""; + AddOrUpdateEntry(result, section, key, display, p.DisplayName, value); + } + } + } + + private static void CompareEKUs(List policies, ComparisonResult result) + { + const string section = "EKUs"; + foreach (var p in policies) + { + if (p.Policy.EKUs == null) continue; + foreach (var eku in p.Policy.EKUs) + { + if (eku == null) continue; + string key = string.IsNullOrEmpty(eku.ID) ? "EKU|" + Guid.NewGuid() : eku.ID; + string display = string.IsNullOrEmpty(eku.FriendlyName) ? (eku.ID ?? "EKU") : eku.FriendlyName; + string value = eku.Value != null && eku.Value.Length > 0 + ? BitConverter.ToString(eku.Value).Replace("-", string.Empty) + : "Present"; + AddOrUpdateEntry(result, section, key, display, p.DisplayName, value); + } + } + } + } +} From 3495e115395fa0e5c5c9a64f8f7b5d7462c254b5 Mon Sep 17 00:00:00 2001 From: Chris LaPlante Date: Fri, 12 Jun 2026 10:46:04 -0400 Subject: [PATCH 2/2] updated to match other PR --- WDAC-Policy-Wizard/app/src/MainForm.Designer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs b/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs index 3418107e..56f4a72d 100644 --- a/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs +++ b/WDAC-Policy-Wizard/app/src/MainForm.Designer.cs @@ -386,6 +386,7 @@ 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(198, 453); @@ -398,6 +399,7 @@ private void InitializeComponent() // // label3 // + label3.AutoSize = true; label3.BackColor = System.Drawing.Color.Transparent; label3.Font = new System.Drawing.Font("Tahoma", 9F); label3.Location = new System.Drawing.Point(408, 453); @@ -410,6 +412,7 @@ private void InitializeComponent() // // label4 // + label4.AutoSize = true; label4.BackColor = System.Drawing.Color.Transparent; label4.Font = new System.Drawing.Font("Tahoma", 9F); label4.Location = new System.Drawing.Point(618, 453); @@ -422,6 +425,7 @@ private void InitializeComponent() // // label5 // + label5.AutoSize = true; label5.BackColor = System.Drawing.Color.Transparent; label5.Font = new System.Drawing.Font("Tahoma", 9F); label5.Location = new System.Drawing.Point(828, 453);