Compare-ConsoleObject -View Baseline puts two or more objects side by side: one row per property (nested properties by their path), one column per object, and a status for every row. One object is the baseline; every cell that differs from it is highlighted, and a property an object doesn't have says (missing) instead of looking empty. I use it for servers that should be configured alike, such as the domain controllers of one forest.
Try it
Install-Module ConsoleMonster -Scope CurrentUser# Four domain controllers that should be configured alike.
$controllers = @(
[pscustomobject]@{
Name = 'dc01'; Site = 'HQ'; LdapPort = 389; GlobalCatalog = $true; DnsEnabled = $true
Spn = 'ldap/dc01.ad.evotec.xyz'
Security = [ordered]@{ Signing = 'Required'; ChannelBinding = 'Always' }
}
[pscustomobject]@{
Name = 'dc02'; Site = 'HQ'; LdapPort = 636; GlobalCatalog = $true; DnsEnabled = $true
Spn = 'ldap/dc02.ad.evotec.xyz'
Security = [ordered]@{ Signing = 'Required'; ChannelBinding = 'Always' }
}
[pscustomobject]@{
Name = 'dc03'; Site = 'Branch'; LdapPort = 389; GlobalCatalog = $false; DnsEnabled = $true
Spn = 'ldap/dc03.ad.evotec.xyz'
Security = [ordered]@{ Signing = 'Negotiate'; ChannelBinding = 'Always' }
}
[pscustomobject]@{
Name = 'dc04'; Site = 'Branch'; LdapPort = 389; GlobalCatalog = $true
Spn = 'ldap/dc04.ad.evotec.xyz'
Security = [ordered]@{ Signing = 'Required'; ChannelBinding = 'Never' }
}
)
# dc01 is the reference; each SPN is compared without its host name, so only real drift remains.
$controllers | Compare-ConsoleObject -View Baseline -NameProperty Name -Baseline dc01 -Ignore Name `
-Normalize @{ Spn = { $_ -replace '/[^.]+', '/HOST' } } -Title 'Domain controller drift'Full program
Import-Module ConsoleMonster
# Four domain controllers that should be configured alike.
$controllers = @(
[pscustomobject]@{
Name = 'dc01'; Site = 'HQ'; LdapPort = 389; GlobalCatalog = $true; DnsEnabled = $true
Spn = 'ldap/dc01.ad.evotec.xyz'
Security = [ordered]@{ Signing = 'Required'; ChannelBinding = 'Always' }
}
[pscustomobject]@{
Name = 'dc02'; Site = 'HQ'; LdapPort = 636; GlobalCatalog = $true; DnsEnabled = $true
Spn = 'ldap/dc02.ad.evotec.xyz'
Security = [ordered]@{ Signing = 'Required'; ChannelBinding = 'Always' }
}
[pscustomobject]@{
Name = 'dc03'; Site = 'Branch'; LdapPort = 389; GlobalCatalog = $false; DnsEnabled = $true
Spn = 'ldap/dc03.ad.evotec.xyz'
Security = [ordered]@{ Signing = 'Negotiate'; ChannelBinding = 'Always' }
}
[pscustomobject]@{
Name = 'dc04'; Site = 'Branch'; LdapPort = 389; GlobalCatalog = $true
Spn = 'ldap/dc04.ad.evotec.xyz'
Security = [ordered]@{ Signing = 'Required'; ChannelBinding = 'Never' }
}
)
# dc01 is the reference; each SPN is compared without its host name, so only real drift remains.
$controllers | Compare-ConsoleObject -View Baseline -NameProperty Name -Baseline dc01 -Ignore Name `
-Normalize @{ Spn = { $_ -replace '/[^.]+', '/HOST' } } -Title 'Domain controller drift'Run: ./object-drift.ps1
dotnet new console -n ObjectDrift
cd ObjectDrift
dotnet add package ConsoleMonster// Four domain controllers that should be configured alike.
object[] controllers = {
new {
Name = "dc01", Site = "HQ", LdapPort = 389, GlobalCatalog = true, DnsEnabled = true,
Spn = "ldap/dc01.ad.evotec.xyz",
Security = new { Signing = "Required", ChannelBinding = "Always" }
},
new {
Name = "dc02", Site = "HQ", LdapPort = 636, GlobalCatalog = true, DnsEnabled = true,
Spn = "ldap/dc02.ad.evotec.xyz",
Security = new { Signing = "Required", ChannelBinding = "Always" }
},
new {
Name = "dc03", Site = "Branch", LdapPort = 389, GlobalCatalog = false, DnsEnabled = true,
Spn = "ldap/dc03.ad.evotec.xyz",
Security = new { Signing = "Negotiate", ChannelBinding = "Always" }
},
new {
Name = "dc04", Site = "Branch", LdapPort = 389, GlobalCatalog = true,
Spn = "ldap/dc04.ad.evotec.xyz",
Security = new { Signing = "Required", ChannelBinding = "Never" }
}
};
// dc01, the first object, is the reference; each SPN is compared without its host name, so only real drift remains.
var options = new DiffOptions();
options.Ignore.Add("Name");
options.Normalize["Spn"] = value => Regex.Replace(value?.ToString() ?? "", "/[^.]+", "/HOST");
BaselineComparison comparison = ObjectDiff.CompareBaseline(controllers, options);
Terminal.Write(new BaselineView(comparison) { Title = "Domain controller drift" });Full program
using System.Text.RegularExpressions;
using ConsoleMonster;
using ConsoleMonster.Changes;
// Four domain controllers that should be configured alike.
object[] controllers = {
new {
Name = "dc01", Site = "HQ", LdapPort = 389, GlobalCatalog = true, DnsEnabled = true,
Spn = "ldap/dc01.ad.evotec.xyz",
Security = new { Signing = "Required", ChannelBinding = "Always" }
},
new {
Name = "dc02", Site = "HQ", LdapPort = 636, GlobalCatalog = true, DnsEnabled = true,
Spn = "ldap/dc02.ad.evotec.xyz",
Security = new { Signing = "Required", ChannelBinding = "Always" }
},
new {
Name = "dc03", Site = "Branch", LdapPort = 389, GlobalCatalog = false, DnsEnabled = true,
Spn = "ldap/dc03.ad.evotec.xyz",
Security = new { Signing = "Negotiate", ChannelBinding = "Always" }
},
new {
Name = "dc04", Site = "Branch", LdapPort = 389, GlobalCatalog = true,
Spn = "ldap/dc04.ad.evotec.xyz",
Security = new { Signing = "Required", ChannelBinding = "Never" }
}
};
// dc01, the first object, is the reference; each SPN is compared without its host name, so only real drift remains.
var options = new DiffOptions();
options.Ignore.Add("Name");
options.Normalize["Spn"] = value => Regex.Replace(value?.ToString() ?? "", "/[^.]+", "/HOST");
BaselineComparison comparison = ObjectDiff.CompareBaseline(controllers, options);
Terminal.Write(new BaselineView(comparison) { Title = "Domain controller drift" });Run: dotnet run
dotnet new console -n ObjectDrift
cd ObjectDrift
dotnet add package ConsoleMonster// Four domain controllers that should be configured alike.
object[] controllers =
{
new
{
Name = "dc01",
Site = "HQ",
LdapPort = 389,
GlobalCatalog = true,
DnsEnabled = true,
Spn = "ldap/dc01.ad.evotec.xyz",
Security = new
{
Signing = "Required",
ChannelBinding = "Always"
}
},
new
{
Name = "dc02",
Site = "HQ",
LdapPort = 636,
GlobalCatalog = true,
DnsEnabled = true,
Spn = "ldap/dc02.ad.evotec.xyz",
Security = new
{
Signing = "Required",
ChannelBinding = "Always"
}
},
new
{
Name = "dc03",
Site = "Branch",
LdapPort = 389,
GlobalCatalog = false,
DnsEnabled = true,
Spn = "ldap/dc03.ad.evotec.xyz",
Security = new
{
Signing = "Negotiate",
ChannelBinding = "Always"
}
},
new
{
Name = "dc04",
Site = "Branch",
LdapPort = 389,
GlobalCatalog = true,
Spn = "ldap/dc04.ad.evotec.xyz",
Security = new
{
Signing = "Required",
ChannelBinding = "Never"
}
}
};
// dc01, the first object, is the reference; each SPN is compared without its host name, so only real drift remains.
var options = new DiffOptions();
options.Ignore.Add("Name");
options.Normalize["Spn"] = value => Regex.Replace(value?.ToString() ?? "", "/[^.]+", "/HOST");
BaselineComparison comparison = ObjectDiff.CompareBaseline(controllers, options);
new BaselineView(comparison).Title("Domain controller drift").Show();
Full program
using System.Text.RegularExpressions;
using ConsoleMonster;
using ConsoleMonster.Changes;
// Four domain controllers that should be configured alike.
object[] controllers =
{
new
{
Name = "dc01",
Site = "HQ",
LdapPort = 389,
GlobalCatalog = true,
DnsEnabled = true,
Spn = "ldap/dc01.ad.evotec.xyz",
Security = new
{
Signing = "Required",
ChannelBinding = "Always"
}
},
new
{
Name = "dc02",
Site = "HQ",
LdapPort = 636,
GlobalCatalog = true,
DnsEnabled = true,
Spn = "ldap/dc02.ad.evotec.xyz",
Security = new
{
Signing = "Required",
ChannelBinding = "Always"
}
},
new
{
Name = "dc03",
Site = "Branch",
LdapPort = 389,
GlobalCatalog = false,
DnsEnabled = true,
Spn = "ldap/dc03.ad.evotec.xyz",
Security = new
{
Signing = "Negotiate",
ChannelBinding = "Always"
}
},
new
{
Name = "dc04",
Site = "Branch",
LdapPort = 389,
GlobalCatalog = true,
Spn = "ldap/dc04.ad.evotec.xyz",
Security = new
{
Signing = "Required",
ChannelBinding = "Never"
}
}
};
// dc01, the first object, is the reference; each SPN is compared without its host name, so only real drift remains.
var options = new DiffOptions();
options.Ignore.Add("Name");
options.Normalize["Spn"] = value => Regex.Replace(value?.ToString() ?? "", "/[^.]+", "/HOST");
BaselineComparison comparison = ObjectDiff.CompareBaseline(controllers, options);
new BaselineView(comparison).Title("Domain controller drift").Show();
Run: dotnet run
Domain controller drift ╭─────────────────────────┬─────────────────┬──────────┬───────────┬───────────┬──────────────╮ │ Property │ dc01 (baseline) │ dc02 │ dc03 │ dc04 │ Status │ ├─────────────────────────┼─────────────────┼──────────┼───────────┼───────────┼──────────────┤ │ Site │ HQ │ HQ │ Branch │ Branch │ differs │ │ LdapPort │ 389 │ 636 │ 389 │ 389 │ differs │ │ GlobalCatalog │ True │ True │ False │ True │ differs │ │ DnsEnabled │ True │ True │ True │ (missing) │ missing on 1 │ │ Security.Signing │ Required │ Required │ Negotiate │ Required │ differs │ │ Security.ChannelBinding │ Always │ Always │ Always │ Never │ differs │ ╰─────────────────────────┴─────────────────┴──────────┴───────────┴───────────┴──────────────╯ 6 properties shown, 1 matching hidden
Domain controller drift ╭─────────────────────────┬─────────────────┬──────────┬───────────┬───────────┬──────────────╮ │ Property │ dc01 (baseline) │ dc02 │ dc03 │ dc04 │ Status │ ├─────────────────────────┼─────────────────┼──────────┼───────────┼───────────┼──────────────┤ │ Site │ HQ │ HQ │ Branch │ Branch │ differs │ │ LdapPort │ 389 │ 636 │ 389 │ 389 │ differs │ │ GlobalCatalog │ True │ True │ False │ True │ differs │ │ DnsEnabled │ True │ True │ True │ (missing) │ missing on 1 │ │ Security.Signing │ Required │ Required │ Negotiate │ Required │ differs │ │ Security.ChannelBinding │ Always │ Always │ Always │ Never │ differs │ ╰─────────────────────────┴─────────────────┴──────────┴───────────┴───────────┴──────────────╯ 6 properties shown, 1 matching hidden
Four domain controllers compared with dc01. Only the properties that drift are listed; the SPNs differ only by host name, so they don't count.
How it works
Pass the objects through the pipeline or -InputObject, or to ObjectDiff.CompareBaseline in C#, and render the result with BaselineView:
$controllers | Compare-ConsoleObject -View Baseline -NameProperty Name -Baseline dc01 -Ignore Name `
-Normalize @{ Spn = { $_ -replace '/[^.]+', '/HOST' } }
- Columns. Each column is named by
-NameProperty, by-Namewith one name per object, or by the usual key properties (Name,Idand similar). The first object is the baseline unless-Baselinenames another column or gives its zero-based index. - Rows. Properties are compared to
-Depthlevels, soSecurity.Signingis a row of its own. Matching rows are left out;-IncludeEqualshows them too.-Propertyand-Ignorechoose the paths. - Case. Strings compare case-insensitively, as
Compare-Objectdoes, and-CaseSensitive(DiffOptions.CaseSensitive) changes both values and names. A difference only in case is still shown, with its own status, so it never disappears silently. - Noise.
-Normalizemaps a property path to a script block (DiffOptions.Normalizetakes a delegate) whose result is compared instead of the value. The matrix still shows the original values. - Narrow windows. When the columns don't fit, each property becomes a small card listing every object's value and the status, like other ConsoleMonster tables.
Domain controller drift Site dc01 (baseline) HQ dc02 HQ dc03 Branch dc04 Branch Status differs LdapPort dc01 (baseline) 389 dc02 636 dc03 389 dc04 389 Status differs GlobalCatalog dc01 (baseline) True dc02 True dc03 False dc04 True Status differs DnsEnabled dc01 (baseline) True dc02 True dc03 True dc04 (missing) Status missing on 1 Security.Signing dc01 (baseline) Required dc02 Required dc03 Negotiate dc04 Required Status differs Security.ChannelBinding dc01 (baseline) Always dc02 Always dc03 Always dc04 Never Status differs 6 properties shown, 1 matching hidden
Domain controller drift Site dc01 (baseline) HQ dc02 HQ dc03 Branch dc04 Branch Status differs LdapPort dc01 (baseline) 389 dc02 636 dc03 389 dc04 389 Status differs GlobalCatalog dc01 (baseline) True dc02 True dc03 False dc04 True Status differs DnsEnabled dc01 (baseline) True dc02 True dc03 True dc04 (missing) Status missing on 1 Security.Signing dc01 (baseline) Required dc02 Required dc03 Negotiate dc04 Required Status differs Security.ChannelBinding dc01 (baseline) Always dc02 Always dc03 Always dc04 Never Status differs 6 properties shown, 1 matching hidden
Two sets of objects
Without -View Baseline, Compare-ConsoleObject -ReferenceObject $before -DifferenceObject $after matches two collections by key and lists what was added, removed and changed, with -View Summary, Table, SideBySide or Unified. The same case rule, missing marker and -Normalize apply there, and -PassThru returns the change records instead of drawing them.
In a layout or a report
-AsWidget returns the comparison as a widget instead of writing it, so it goes into Show-ConsoleApp layouts and Export-ConsoleFrame exports:
$matrix = $controllers | Compare-ConsoleObject -View Baseline -NameProperty Name -Ignore Name -AsWidget
$matrix | Export-ConsoleFrame -Format Html -Width 120 -Path ./controller-drift.html
In C#, BaselineView is an ordinary widget; BaselineComparison.Rows gives each property's cells, presence and status for your own checks.
Related
The Compare-ConsoleObject reference lists every parameter, Compare text and files compares strings and files line by line, Interactive apps shows layouts the matrix can sit in, and Recording and replay covers HTML exports.