From 6cc5792dead3ce8933855b649b01c52ae3b4ccaa Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 22 Jan 2026 20:10:53 -0300 Subject: [PATCH 01/14] some progress --- .../Commands/GetADTreeGroupMemberCommand.cs | 14 ++-- ...etADTreePrincipalGroupMembershipCommand.cs | 8 +-- src/PSADTree/Extensions/MiscExtensions.cs | 32 ++++++++++ src/PSADTree/PSADTreeCmdletBase.cs | 7 +- src/PSADTree/TreeComputer.cs | 7 +- src/PSADTree/TreeGroup.cs | 21 +++++- src/PSADTree/TreeObjectBase.cs | 64 ++++++++++++++++++- src/PSADTree/TreeUser.cs | 24 +++++-- src/PSADTree/UserAccountControl.cs | 31 +++++++++ 9 files changed, 187 insertions(+), 21 deletions(-) create mode 100644 src/PSADTree/Extensions/MiscExtensions.cs create mode 100644 src/PSADTree/UserAccountControl.cs diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index 068d268..2253aa2 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -26,7 +26,7 @@ protected override void HandleFirstPrincipal(Principal principal) if (principal is GroupPrincipal group && !ShouldExclude(principal)) { string source = group.DistinguishedName; - PushToStack(new TreeGroup(source, group), group); + PushToStack(new TreeGroup(source, group, Properties), group); } } @@ -92,9 +92,15 @@ private void ProcessPrincipal( { TreeObjectBase treeObject = principal switch { - UserPrincipal user => AddTreeObject(new TreeUser(source, parent, user, depth)), - ComputerPrincipal computer => AddTreeObject(new TreeComputer(source, parent, computer, depth)), - GroupPrincipal group => ProcessGroup(parent, group, source, depth), + UserPrincipal user => + AddTreeObject(new TreeUser(source, parent, user, Properties, depth)), + + ComputerPrincipal computer => + AddTreeObject(new TreeComputer(source, parent, computer, Properties, depth)), + + GroupPrincipal group => + ProcessGroup(parent, group, source, depth), + _ => throw new ArgumentOutOfRangeException(nameof(principal)), }; diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index 06e65d6..a1faed9 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -24,15 +24,15 @@ protected override void HandleFirstPrincipal(Principal principal) switch (principal) { case UserPrincipal user: - HandleOther(new TreeUser(source, user), principal); + HandleOther(new TreeUser(source, user, Properties), principal); break; case ComputerPrincipal computer: - HandleOther(new TreeComputer(source, computer), principal); + HandleOther(new TreeComputer(source, computer, Properties), principal); break; case GroupPrincipal group: - HandleGroup(new TreeGroup(source, group), group); + HandleGroup(new TreeGroup(source, group, Properties), group); break; default: @@ -61,7 +61,7 @@ void HandleOther(TreeObjectBase treeObject, Principal principal) if (!ShouldExclude(parent)) { GroupPrincipal groupPrincipal = (GroupPrincipal)parent; - TreeGroup treeGroup = new(source, null, groupPrincipal, 1); + TreeGroup treeGroup = new(source, null, groupPrincipal, Properties, 1); PushToStack(treeGroup, groupPrincipal); } } diff --git a/src/PSADTree/Extensions/MiscExtensions.cs b/src/PSADTree/Extensions/MiscExtensions.cs new file mode 100644 index 0000000..bf655f1 --- /dev/null +++ b/src/PSADTree/Extensions/MiscExtensions.cs @@ -0,0 +1,32 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.DirectoryServices; +using System.DirectoryServices.AccountManagement; +using System.Management.Automation; + +namespace PSADTree.Extensions; + +internal static class MiscExtensions +{ + internal static T GetProperty( + this DirectoryEntry entry, + string property) + => LanguagePrimitives.ConvertTo(entry.Properties[property][0]); + + internal static bool TryGetProperty( + this SearchResult search, + string property, + [NotNullWhen(true)] out T? value) + { + value = default; + if (!search.Properties.Contains(property)) + { + return false; + } + + return LanguagePrimitives.TryConvertTo(search.Properties[property][0], out value); + } + + internal static DirectoryEntry GetDirectoryEntry(this Principal principal) + => (DirectoryEntry)principal.GetUnderlyingObject(); +} diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index ecd17b3..3651da5 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; +using System.DirectoryServices; using System.DirectoryServices.AccountManagement; using System.Linq; using System.Management.Automation; @@ -68,6 +70,9 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable [SupportsWildcards] public string[]? Exclude { get; set; } + [Parameter] + public string[]? Properties { get; set; } + protected override void BeginProcessing() { try @@ -225,7 +230,7 @@ protected TreeGroup ProcessGroup( return treeGroup; } - treeGroup = new TreeGroup(source, parent, group, depth); + treeGroup = new TreeGroup(source, parent, group, Properties, depth); PushToStack(treeGroup, group); return treeGroup; } diff --git a/src/PSADTree/TreeComputer.cs b/src/PSADTree/TreeComputer.cs index ae7590d..a9217f3 100644 --- a/src/PSADTree/TreeComputer.cs +++ b/src/PSADTree/TreeComputer.cs @@ -16,12 +16,13 @@ internal TreeComputer( string source, TreeGroup? parent, ComputerPrincipal computer, + string[]? properties, int depth) - : base(source, parent, computer, depth) + : base(source, parent, computer, properties, depth) { } - internal TreeComputer(string source, ComputerPrincipal computer) - : base(source, computer) + internal TreeComputer(string source, ComputerPrincipal computer, string[]? properties) + : base(source, computer, properties) { } internal override TreeObjectBase Clone(TreeGroup parent, string source, int depth) diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index 6aee386..8369673 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; +using System.DirectoryServices; using System.DirectoryServices.AccountManagement; +using PSADTree.Extensions; namespace PSADTree; @@ -18,6 +20,10 @@ public sealed class TreeGroup : TreeObjectBase private List _children; + public UserAccountControl UserAccountControl { get; private set; } + + public bool Enabled { get; private set; } + public ReadOnlyCollection Children => new(_children); public bool IsCircular { get; private set; } @@ -29,12 +35,14 @@ private TreeGroup( int depth) : base(group, parent, source, depth) { + UserAccountControl = group.UserAccountControl; + Enabled = group.Enabled; _children = group._children; IsCircular = group.IsCircular; } - internal TreeGroup(string source, GroupPrincipal group) - : base(source, group) + internal TreeGroup(string source, GroupPrincipal group, string[]? properties) + : base(source, group, properties) { _children = []; } @@ -43,8 +51,9 @@ internal TreeGroup( string source, TreeGroup? parent, GroupPrincipal group, + string[]? properties, int depth) - : base(source, parent, group, depth) + : base(source, parent, group, properties, depth) { _children = []; } @@ -84,6 +93,12 @@ internal void LinkCachedChildren(TreeCache cache) internal void AddChild(TreeObjectBase child) => _children.Add(child); + internal void SetUserAccountControl(DirectoryEntry entry) + { + UserAccountControl = entry.GetProperty("userAccountControl"); + Enabled = !UserAccountControl.HasFlag(UserAccountControl.ACCOUNTDISABLE); + } + internal override TreeObjectBase Clone(TreeGroup parent, string source, int depth) => new TreeGroup(this, parent, source, depth); } diff --git a/src/PSADTree/TreeObjectBase.cs b/src/PSADTree/TreeObjectBase.cs index b9d8680..c3b0a08 100644 --- a/src/PSADTree/TreeObjectBase.cs +++ b/src/PSADTree/TreeObjectBase.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.DirectoryServices; using System.DirectoryServices.AccountManagement; using System.Security.Principal; using PSADTree.Extensions; @@ -33,6 +36,8 @@ public abstract class TreeObjectBase public SecurityIdentifier ObjectSid { get; } + public ReadOnlyDictionary? AdditionalProperties { get; } + protected TreeObjectBase( TreeObjectBase treeObject, TreeGroup? parent, @@ -54,7 +59,7 @@ protected TreeObjectBase( DisplayName = treeObject.DisplayName; } - protected TreeObjectBase(string source, Principal principal) + protected TreeObjectBase(string source, Principal principal, string[]? properties) { Source = source; SamAccountName = principal.SamAccountName; @@ -67,20 +72,75 @@ protected TreeObjectBase(string source, Principal principal) UserPrincipalName = principal.UserPrincipalName; Description = principal.Description; DisplayName = principal.DisplayName; + AdditionalProperties = GetAdditionalProperties(principal.GetDirectoryEntry(), properties); } protected TreeObjectBase( string source, TreeGroup? parent, Principal principal, + string[]? properties, int depth) - : this(source, principal) + : this(source, principal, properties) { Depth = depth; Hierarchy = principal.SamAccountName.Indent(depth); Parent = parent; } + private ReadOnlyDictionary? GetAdditionalProperties( + DirectoryEntry entry, + string[]? Properties) + { + if (Properties is null or { Length: 0 }) + { + return null; + } + + Dictionary additionalProperties = []; + + foreach (string property in Properties) + { + if (property.Equals("nTSecurityDescriptor", StringComparison.OrdinalIgnoreCase)) + { + additionalProperties[property] = GetAcl(entry); + continue; + } + + if (entry.Properties.Contains(property)) + { + additionalProperties[property] = entry.Properties[property][0]; + } + + } + + return new ReadOnlyDictionary(additionalProperties); + } + + private static ActiveDirectorySecurity? GetAcl(DirectoryEntry entry) + { + using DirectorySearcher searcher = new(entry, null, ["nTSecurityDescriptor"]) + { + SecurityMasks = SecurityMasks.Group | SecurityMasks.Owner | SecurityMasks.Dacl + }; + + SearchResult? result = searcher.FindOne(); + + if (result is null) + { + return null; + } + + if (!result.TryGetProperty("nTSecurityDescriptor", out byte[]? descriptor)) + { + return null; + } + + ActiveDirectorySecurity acl = new(); + acl.SetSecurityDescriptorBinaryForm(descriptor); + return acl; + } + public override string ToString() => DistinguishedName; internal abstract TreeObjectBase Clone(TreeGroup parent, string source, int depth); diff --git a/src/PSADTree/TreeUser.cs b/src/PSADTree/TreeUser.cs index 9abf626..0f31aa0 100644 --- a/src/PSADTree/TreeUser.cs +++ b/src/PSADTree/TreeUser.cs @@ -1,29 +1,45 @@ +using System.DirectoryServices; using System.DirectoryServices.AccountManagement; +using PSADTree.Extensions; namespace PSADTree; public sealed class TreeUser : TreeObjectBase { + public UserAccountControl UserAccountControl { get; private set; } + + public bool Enabled { get; private set; } + private TreeUser( TreeUser user, TreeGroup parent, string source, int depth) : base(user, parent, source, depth) - { } + { + UserAccountControl = user.UserAccountControl; + Enabled = user.Enabled; + } internal TreeUser( string source, TreeGroup? parent, UserPrincipal user, + string[]? properties, int depth) - : base(source, parent, user, depth) + : base(source, parent, user, properties, depth) { } - internal TreeUser(string source, UserPrincipal user) - : base(source, user) + internal TreeUser(string source, UserPrincipal user, string[]? properties) + : base(source, user, properties) { } + internal void SetUserAccountControl(DirectoryEntry entry) + { + UserAccountControl = entry.GetProperty("userAccountControl"); + Enabled = !UserAccountControl.HasFlag(UserAccountControl.ACCOUNTDISABLE); + } + internal override TreeObjectBase Clone(TreeGroup parent, string source, int depth) => new TreeUser(this, parent, source, depth); } diff --git a/src/PSADTree/UserAccountControl.cs b/src/PSADTree/UserAccountControl.cs new file mode 100644 index 0000000..28aa99f --- /dev/null +++ b/src/PSADTree/UserAccountControl.cs @@ -0,0 +1,31 @@ +using System; + +namespace PSADTree; + +[Flags] +public enum UserAccountControl : uint +{ + None = 0, + SCRIPT = 1, + ACCOUNTDISABLE = 2, + HOMEDIR_REQUIRED = 8, + LOCKOUT = 16, + PASSWD_NOTREQD = 32, + PASSWD_CANT_CHANGE = 64, + ENCRYPTED_TEXT_PWD_ALLOWED = 128, + TEMP_DUPLICATE_ACCOUNT = 256, + NORMAL_ACCOUNT = 512, + INTERDOMAIN_TRUST_ACCOUNT = 2048, + WORKSTATION_TRUST_ACCOUNT = 4096, + SERVER_TRUST_ACCOUNT = 8192, + DONT_EXPIRE_PASSWORD = 65536, + MNS_LOGON_ACCOUNT = 131072, + SMARTCARD_REQUIRED = 262144, + TRUSTED_FOR_DELEGATION = 524288, + NOT_DELEGATED = 1048576, + USE_DES_KEY_ONLY = 2097152, + DONT_REQ_PREAUTH = 4194304, + PASSWORD_EXPIRED = 8388608, + TRUSTED_TO_AUTH_FOR_DELEGATION = 16777216, + PARTIAL_SECRETS_ACCOUNT = 67108864 +} From f10e4e63671fc39a30370fb910be62dda5395a4f Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 22 Jan 2026 15:25:23 -0800 Subject: [PATCH 02/14] ... --- src/PSADTree/TreeObjectBase.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/PSADTree/TreeObjectBase.cs b/src/PSADTree/TreeObjectBase.cs index c3b0a08..452f00c 100644 --- a/src/PSADTree/TreeObjectBase.cs +++ b/src/PSADTree/TreeObjectBase.cs @@ -97,7 +97,9 @@ protected TreeObjectBase( return null; } - Dictionary additionalProperties = []; + Dictionary additionalProperties = new( + capacity: Properties.Length, + StringComparer.OrdinalIgnoreCase); foreach (string property in Properties) { @@ -111,10 +113,11 @@ protected TreeObjectBase( { additionalProperties[property] = entry.Properties[property][0]; } - } - return new ReadOnlyDictionary(additionalProperties); + return additionalProperties.Count == 0 + ? null + : new ReadOnlyDictionary(additionalProperties); } private static ActiveDirectorySecurity? GetAcl(DirectoryEntry entry) From 2a4e5002c4c52b09ddc126dc12d414ec585efe12 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Fri, 23 Jan 2026 05:44:23 -0800 Subject: [PATCH 03/14] more progress --- src/PSADTree/Dbg.cs | 11 - src/PSADTree/Extensions/MiscExtensions.cs | 1 - src/PSADTree/LdapCompleter.cs | 29 +++ src/PSADTree/LdapMap.cs | 272 ++++++++++++++++++++++ src/PSADTree/Nullable.cs | 252 ++++++++++---------- src/PSADTree/PSADTreeCmdletBase.cs | 3 +- src/PSADTree/TreeObjectBase.cs | 38 ++- 7 files changed, 468 insertions(+), 138 deletions(-) delete mode 100644 src/PSADTree/Dbg.cs create mode 100644 src/PSADTree/LdapCompleter.cs create mode 100644 src/PSADTree/LdapMap.cs diff --git a/src/PSADTree/Dbg.cs b/src/PSADTree/Dbg.cs deleted file mode 100644 index 1dee2c7..0000000 --- a/src/PSADTree/Dbg.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; - -namespace PSADTree; - -internal static class Dbg -{ - [Conditional("DEBUG")] - public static void Assert([DoesNotReturnIf(false)] bool condition) => - Debug.Assert(condition); -} diff --git a/src/PSADTree/Extensions/MiscExtensions.cs b/src/PSADTree/Extensions/MiscExtensions.cs index bf655f1..a1a1171 100644 --- a/src/PSADTree/Extensions/MiscExtensions.cs +++ b/src/PSADTree/Extensions/MiscExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics.CodeAnalysis; using System.DirectoryServices; using System.DirectoryServices.AccountManagement; diff --git a/src/PSADTree/LdapCompleter.cs b/src/PSADTree/LdapCompleter.cs new file mode 100644 index 0000000..74942c0 --- /dev/null +++ b/src/PSADTree/LdapCompleter.cs @@ -0,0 +1,29 @@ +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Language; + +namespace PSADTree; + +public sealed class LdapCompleter : IArgumentCompleter +{ + public IEnumerable CompleteArgument( + string commandName, + string parameterName, + string wordToComplete, + CommandAst commandAst, + IDictionary fakeBoundParameters) + { + foreach (string key in LdapMap.Instance.Keys) + { + if (key.StartsWith(wordToComplete, System.StringComparison.OrdinalIgnoreCase)) + { + yield return new CompletionResult( + key, + key, + CompletionResultType.ParameterValue, + $"LDAP DisplayName: {LdapMap.Instance[key]}"); + } + } + } +} diff --git a/src/PSADTree/LdapMap.cs b/src/PSADTree/LdapMap.cs new file mode 100644 index 0000000..fd81289 --- /dev/null +++ b/src/PSADTree/LdapMap.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; + +namespace PSADTree; + +internal static class LdapMap +{ + internal static Dictionary Instance { get; } + + static LdapMap() => Instance = new(StringComparer.OrdinalIgnoreCase) + { + ["AccountExpirationDate"] = "accountExpires", + ["AccountLockoutTime"] = "lockoutTime", + ["AccountPassword"] = "unicodePwd", + ["ADAMAllowReversiblePasswordEncryption"] = "ms-DS-UserEncryptedTextPasswordAllowed", + ["ADAMDisabled"] = "msDS-UserAccountDisabled", + ["ADAMLockedOut"] = "ms-DS-UserAccountAutoLocked", + ["ADAMPasswordExpired"] = "msDS-UserPasswordExpired", + ["ADAMPasswordNeverExpires"] = "msDS-UserDontExpirePassword", + ["ADAMPasswordNotRequired"] = "ms-DS-UserPasswordNotRequired", + ["AllowedDNSSuffixes"] = "msDS-AllowedDNSSuffixes", + ["AllowedPasswordReplicationPolicy"] = "msDS-RevealOnDemandGroup", + ["AllowedToActOnBehalfOf"] = "msDS-AllowedToActOnBehalfOfOtherIdentity", + ["AppliesTo"] = "msDS-PSOAppliesTo", + ["AssignedAuthNPolicyBL"] = "msDS-AssignedAuthNPolicyBL", + ["AssignedAuthNPolicySiloBL"] = "msDS-AssignedAuthNPolicySiloBL", + ["AttributeSyntax"] = "attributeSyntax", + ["AuthenticatedAccounts"] = "msDS-AuthenticatedToAccountlist", + ["AuthenticationPolicy"] = "msDS-AssignedAuthNPolicy", + ["AuthenticationPolicySilo"] = "msDS-AssignedAuthNPolicySilo", + ["AuthenticationPolicySiloMembers"] = "msDS-AuthNPolicySiloMembers", + ["AuthNPolicyEnforce"] = "msDS-AuthNPolicyEnforced", + ["AuthNPolicySiloEnforce"] = "msDS-AuthNPolicySiloEnforced", + ["BadLogonCount"] = "badPwdCount", + ["BehaviorVersion"] = "msDS-Behavior-Version", + ["CanonicalName"] = "canonicalName", + ["Certificates"] = "userCertificate", + ["City"] = "l", + ["CNumConsecutiveSyncFailures"] = "cNumConsecutiveSyncFailures", + ["CNumFailures"] = "cNumFailures", + ["CommonName"] = "cn", + ["Company"] = "company", + ["ComplexityEnabled"] = "msDS-PasswordComplexityEnabled", + ["ComputerAllowedToAuthenticateTo"] = "msDS-ComputerAllowedToAuthenticateTo", + ["ComputerAuthenticationPolicy"] = "msDS-ComputerAuthNPolicy", + ["ComputerAuthNPolicyBL"] = "msDS-ComputerAuthNPolicyBL", + ["ComputerTGTLifetime"] = "msDS-ComputerTGTLifetime", + ["Cost"] = "cost", + ["Country"] = "c", + ["CreationTimeStamp"] = "createTimeStamp", + ["DefaultLockoutDuration"] = "lockoutDuration", + ["DefaultLockoutObservationWindow"] = "lockoutObservationWindow", + ["DefaultLockoutThreshold"] = "lockoutThreshold", + ["DefaultMaxPasswordAge"] = "maxPwdAge", + ["DefaultMinPasswordAge"] = "minPwdAge", + ["DefaultMinPasswordLength"] = "minPwdLength", + ["DefaultPasswordHistoryCount"] = "pwdHistoryLength", + ["DefaultPasswordProperties"] = "pwdProperties", + ["DeniedPasswordReplicationPolicy"] = "msDS-NeverRevealGroup", + ["Department"] = "department", + ["Description"] = "description", + ["DisplayName"] = "displayName", + ["DistinguishedName"] = "distinguishedName", + ["Division"] = "division", + ["DNSHostName"] = "dNSHostName", + ["DNSRoot"] = "dnsRoot", + ["DwLastResult"] = "dwLastResult", + ["DwLastSyncResult"] = "dwLastSyncResult", + ["DwReplicaFlags"] = "dwReplicaFlags", + ["DwVersion"] = "dwVersion", + ["EmailAddress"] = "mail", + ["EmployeeID"] = "employeeID", + ["EmployeeNumber"] = "employeeNumber", + ["Enabled"] = "Enabled", + ["EnabledScopes"] = "msDS-EnabledFeatureBL", + ["Fax"] = "facsimileTelephoneNumber", + ["FeatureGUID"] = "msDS-OptionalFeatureGUID", + ["FeatureScope"] = "msDS-OptionalFeatureFlags", + ["FTimeCreated"] = "ftimeCreated", + ["FTimeDeleted"] = "ftimeDeleted", + ["FTimeEnqueued"] = "ftimeEnqueued", + ["FTimeFirstFailure"] = "ftimeFirstFailure", + ["FTimeLastOriginatingChange"] = "ftimeLastOriginatingChange", + ["FTimeLastSyncAttempt"] = "ftimeLastSyncAttempt", + ["FTimeLastSyncSuccess"] = "ftimeLastSyncSuccess", + ["GivenName"] = "givenName", + ["GroupType"] = "groupType", + ["HomeDirectory"] = "homeDirectory", + ["HomeDrive"] = "homeDrive", + ["HomePhone"] = "homePhone", + ["HostComputers"] = "msDS-HostServiceAccountBL", + ["IncomingTrust"] = "msDS-TDOEgressBL", + ["Initials"] = "initials", + ["InstanceType"] = "instanceType", + ["InterSiteTopologyGenerator"] = "interSiteTopologyGenerator", + ["IsDefunct"] = "isDefunct", + ["IsDeleted"] = "isDeleted", + ["LastBadPasswordAttempt"] = "badPasswordTime", + ["LastKnownParent"] = "lastKnownParent", + ["LastKnownRDN"] = "msDS-LastKnownRDN", + ["LastLogonReplicationInterval"] = "msDS-LogonTimeSyncInterval", + ["LastLogonTimeStamp"] = "lastLogonTimestamp", + ["LdapDisplayName"] = "lDAPDisplayName", + ["LinkedGroupPolicyObjects"] = "gpLink", + ["Location"] = "location", + ["LockoutDuration"] = "msDS-LockoutDuration", + ["LockoutObservationWindow"] = "msDS-LockoutObservationWindow", + ["LockoutThreshold"] = "msDS-LockoutThreshold", + ["LogonWorkstations"] = "userWorkstations", + ["ManagedBy"] = "managedBy", + ["Manager"] = "manager", + ["MaxPasswordAge"] = "msDS-MaximumPasswordAge", + ["Member"] = "member", + ["MemberRulesInCAP"] = "msAuthz-MemberRulesInCentralAccessPolicy", + ["MemberRulesInCAPBL"] = "msAuthz-MemberRulesInCentralAccessPolicyBL", + ["MembersOfResourcePropertyList"] = "msDS-MembersOfResourcePropertyList", + ["MinPasswordAge"] = "msDS-MinimumPasswordAge", + ["MinPasswordLength"] = "msDS-MinimumPasswordLength", + ["MobilePhone"] = "mobile", + ["ModifiedTimeStamp"] = "modifyTimeStamp", + ["MsaGroupMembership"] = "msDS-GroupMSAMembership", + ["MsaManagedPasswordInterval"] = "msDS-ManagedPasswordInterval", + ["MSAuthzCentralAccessPolicyID"] = "msAuthz-CentralAccessPolicyID", + ["MSAuthzEffectiveDACL"] = "msAuthz-EffectiveSecurityPolicy", + ["MSAuthzLastEffectiveDACL"] = "msAuthz-LastEffectiveSecurityPolicy", + ["MSAuthzProposedDACL"] = "msAuthz-ProposedSecurityPolicy", + ["MSAuthzResourceCondition"] = "msAuthz-ResourceCondition", + ["MsDSAppliesToResourceTypes"] = "msDS-AppliesToResourceTypes", + ["MsDSClaimAttributeSource"] = "msDS-ClaimAttributeSource", + ["MsDSClaimIsSingleValued"] = "msDS-ClaimIsSingleValued", + ["MsDSClaimIsValueSpaceRestricted"] = "msDS-ClaimIsValueSpaceRestricted", + ["MsDSClaimPossibleValues"] = "msDS-ClaimPossibleValues", + ["MsDSClaimSharesPossibleValuesWithBL"] = "msDS-ClaimSharesPossibleValuesWithBL", + ["MsDSClaimSource"] = "msDS-ClaimSource", + ["MsDSClaimSourceType"] = "msDS-ClaimSourceType", + ["MsDSClaimTypeAppliesToClass"] = "msDS-ClaimTypeAppliesToClass", + ["MsDSClaimValueType"] = "msDS-ClaimValueType", + ["MSDShasFullReplicaNCs"] = "msDS-hasFullReplicaNCs", + ["MSDShasMasterNCs"] = "msDS-hasMasterNCs", + ["MSDSIsPossibleValuesPresent"] = "msDS-IsPossibleValuesPresent", + ["MsDSIsUsedAsResourceSecurityAttribute"] = "msDS-IsUsedAsResourceSecurityAttribute", + ["MSDSIsUserCachableAtRodc"] = "msDS-IsUserCachableAtRodc", + ["MsDSMembersOfResourcePropertyListBL"] = "msDS-MembersOfResourcePropertyListBL", + ["MSDSPortLDAP"] = "msDS-PortLDAP", + ["MsDSSClaimSharesPossibleValuesWith"] = "msDS-ClaimSharesPossibleValuesWith", + ["MsDSURI"] = "msDS-URI", + ["MSDSUserAccountControlComputed"] = "msDS-User-Account-Control-Computed", + ["MsDSValueTypeReference"] = "msDS-ValueTypeReference", + ["MSDSValueTypeReferenceBL"] = "msDS-ValueTypeReferenceBL", + ["Name"] = "name", + ["NCName"] = "nCName", + ["NETBIOSName"] = "nETBIOSName", + ["NTMixedDomainMode"] = "ntMixedDomain", + ["NTSecurityDescriptor"] = "nTSecurityDescriptor", + ["ObjectCategory"] = "objectCategory", + ["ObjectClass"] = "objectClass", + ["ObjectGUID"] = "objectGUID", + ["ObjectSid"] = "objectSid", + ["Office"] = "physicalDeliveryOfficeName", + ["OfficePhone"] = "telephoneNumber", + ["Options"] = "Options", + ["OpType"] = "OpType", + ["Organization"] = "o", + ["OS"] = "operatingSystem", + ["OSHotfix"] = "operatingSystemHotfix", + ["OSServicePack"] = "operatingSystemServicePack", + ["OSVersion"] = "operatingSystemVersion", + ["OtherName"] = "middleName", + ["OutgoingTrust"] = "msDS-TDOIngressBL", + ["PartiallyReplicatedNamingContexts"] = "hasPartialReplicaNCs", + ["PasswordHistoryCount"] = "msDS-PasswordHistoryLength", + ["PasswordLastSet"] = "pwdLastSet", + ["POBox"] = "postOfficeBox", + ["PostalCode"] = "postalCode", + ["Precedence"] = "msDS-PasswordSettingsPrecedence", + ["PrimaryGroup"] = "primaryGroupID", + ["ProfilePath"] = "profilePath", + ["PszAsyncIntersiteTransportDN"] = "pszAsyncIntersiteTransportDN", + ["PszAttributeName"] = "pszAttributeName", + ["PszDsaAddress"] = "pszDsaAddress", + ["PszDsaDN"] = "pszDsaDN", + ["PszLastOriginatingDsaDN"] = "pszLastOriginatingDsaDN", + ["PszNamingContext"] = "pszNamingContext", + ["PszObjectDn"] = "pszObjectDn", + ["PszSourceDsaAddress"] = "pszSourceDsaAddress", + ["PszSourceDsaDN"] = "pszSourceDsaDN", + ["PublicKeyRequiredPasswordRolling"] = "msDS-ExpirePasswordsOnSmartCardOnlyAccounts", + ["ReplicateFromDirectoryServer"] = "fromServer", + ["ReplicateSingleObject"] = "replicateSingleObject", + ["ReplicationAttributeMetadata"] = "msDS-ReplAttributeMetaData", + ["ReplicationAttributeMetadataObjectType"] = "DS_REPL_ATTR_META_DATA", + ["ReplicationAttributeValueMetadata"] = "msDS-ReplValueMetaData", + ["ReplicationAttributeValueMetadataObjectType"] = "DS_REPL_VALUE_META_DATA", + ["ReplicationConnectionFailures"] = "msDS-ReplConnectionFailures", + ["ReplicationFailuresObjectType"] = "DS_REPL_KCC_DSA_FAILURE", + ["ReplicationFrequency"] = "replInterval", + ["ReplicationInboundPartners"] = "msDS-NCReplInboundNeighbors", + ["ReplicationLinkFailures"] = "msDS-ReplLinkFailures", + ["ReplicationOutboundPartners"] = "msDS-NCReplOutboundNeighbors", + ["ReplicationPartnersObjectType"] = "DS_REPL_NEIGHBOR", + ["ReplicationQueue"] = "msDS-ReplPendingOps", + ["ReplicationQueueObjectType"] = "DS_REPL_OP", + ["ReplicationSchedule"] = "schedule", + ["ReplicationUpToDatenessVector"] = "msDS-NCReplCursors", + ["ReplicationUpToDatenessVectorObjectType"] = "DS_REPL_CURSOR", + ["RequiredDomainMode"] = "msDS-RequiredDomainBehaviorVersion", + ["RequiredForestMode"] = "msDS-RequiredForestBehaviorVersion", + ["ResultantPSO"] = "msDS-ResultantPSO", + ["RevealedAccounts"] = "msDS-RevealedList", + ["ReversibleEncryptionEnabled"] = "msDS-PasswordReversibleEncryptionEnabled", + ["RollingNTLMSecret"] = "msDS-StrongNTLMPolicy", + ["Rule"] = "msDS-TransformationRules", + ["SamAccountName"] = "sAMAccountName", + ["ScriptPath"] = "scriptPath", + ["SDRightsEffective"] = "sdRightsEffective", + ["SearchFlags"] = "searchFlags", + ["ServerReference"] = "serverReference", + ["ServerReferenceBL"] = "serverReferenceBL", + ["ServiceAccount"] = "msDS-HostServiceAccount", + ["ServiceAllowedNTLMNetworkAuthentication"] = "msDS-ServiceAllowedNTLMNetworkAuthentication", + ["ServiceAllowedToAuthenticateFrom"] = "msDS-ServiceAllowedToAuthenticateFrom", + ["ServiceAllowedToAuthenticateTo"] = "msDS-ServiceAllowedToAuthenticateTo", + ["ServiceAuthenticationPolicy"] = "msDS-ServiceAuthNPolicy", + ["ServiceAuthNPolicyBL"] = "msDS-ServiceAuthNPolicyBL", + ["ServicePrincipalNames"] = "servicePrincipalName", + ["ServiceTGTLifetime"] = "msDS-ServiceTGTLifetime", + ["Site"] = "siteObject", + ["SiteLinksIncluded"] = "siteLinkList", + ["SitesIncluded"] = "siteList", + ["SourceXmlAttribute"] = "sourceXmlAttribute", + ["SPNSuffixes"] = "msDS-SPNSuffixes", + ["State"] = "st", + ["Street"] = "street", + ["StreetAddress"] = "streetAddress", + ["Subnet"] = "siteObjectBL", + ["SupportedEncryptionTypes"] = "msDS-SupportedEncryptionTypes", + ["Surname"] = "sn", + ["SystemFlags"] = "systemFlags", + ["Target"] = "trustPartner", + ["Title"] = "title", + ["TransportType"] = "transportType", + ["TrustAttributes"] = "trustAttributes", + ["TrustDirection"] = "trustDirection", + ["TrustedPolicy"] = "msDS-EgressClaimsTransformationPolicy", + ["TrustingPolicy"] = "msDS-IngressClaimsTransformationPolicy", + ["TrustType"] = "trustType", + ["UlOptions"] = "ulOptions", + ["UlPriority"] = "ulPriority", + ["UlSerialNumber"] = "ulSerialNumber", + ["UnicodePwd"] = "unicodePwd", + ["UniversalGroupCachingRefreshSite"] = "msDS-Preferred-GC-Site", + ["UPNSuffixes"] = "uPNSuffixes", + ["UserAccountControl"] = "userAccountControl", + ["UserAccountControlComputed"] = "msDS-User-Account-Control-Computed", + ["UserAllowedNTLMNetworkAuthentication"] = "msDS-UserAllowedNTLMNetworkAuthentication", + ["UserAllowedToAuthenticateFrom"] = "msDS-UserAllowedToAuthenticateFrom", + ["UserAllowedToAuthenticateTo"] = "msDS-UserAllowedToAuthenticateTo", + ["UserAuthenticationPolicy"] = "msDS-UserAuthNPolicy", + ["UserAuthNPolicyBL"] = "msDS-UserAuthNPolicyBL", + ["UserPrincipalName"] = "userPrincipalName", + ["UserTGTLifetime"] = "msDS-UserTGTLifetime", + ["UsnAttributeFilter"] = "usnAttributeFilter", + ["UsnLastObjChangeSynced"] = "usnLastObjChangeSynced", + ["UsnLocalChange"] = "usnLocalChange", + ["UsnOriginatingChange"] = "usnOriginatingChange", + ["UuidAsyncIntersiteTransportObjGuid"] = "uuidAsyncIntersiteTransportObjGuid", + ["UuidDsaObjGuid"] = "uuidDsaObjGuid", + ["UuidLastOriginatingDsaInvocationID"] = "uuidLastOriginatingDsaInvocationID", + ["UuidSourceDsaInvocationID"] = "uuidSourceDsaInvocationID", + ["UuidSourceDsaObjGuid"] = "uuidSourceDsaObjGuid", + }; +} diff --git a/src/PSADTree/Nullable.cs b/src/PSADTree/Nullable.cs index 97c2603..ad74d3b 100644 --- a/src/PSADTree/Nullable.cs +++ b/src/PSADTree/Nullable.cs @@ -1,140 +1,150 @@ #if !NETCOREAPP -namespace System.Diagnostics.CodeAnalysis +namespace System.Diagnostics.CodeAnalysis; + +/// Specifies that null is allowed as an input even if the corresponding type disallows it. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class AllowNullAttribute : Attribute { } + +/// Specifies that null is disallowed as an input even if the corresponding type allows it. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class DisallowNullAttribute : Attribute { } + +/// Specifies that an output may be null even if the corresponding type disallows it. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class MaybeNullAttribute : Attribute { } + +/// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class NotNullAttribute : Attribute { } + +/// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class MaybeNullWhenAttribute : Attribute { - /// Specifies that null is allowed as an input even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] - internal sealed class AllowNullAttribute : Attribute { } + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } +} - /// Specifies that null is disallowed as an input even if the corresponding type allows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] - internal sealed class DisallowNullAttribute : Attribute { } +/// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class NotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } +} - /// Specifies that an output may be null even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] - internal sealed class MaybeNullAttribute : Attribute { } +/// Specifies that the output will be non-null if the named parameter is non-null. +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class NotNullIfNotNullAttribute : Attribute +{ + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } +} - /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] - internal sealed class NotNullAttribute : Attribute { } +/// Applied to a method that will never return under any circumstance. +[AttributeUsage(AttributeTargets.Method, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class DoesNotReturnAttribute : Attribute { } - /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] - internal sealed class MaybeNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter may be null. - /// - public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; - - /// Gets the return value condition. - public bool ReturnValue { get; } - } +/// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class DoesNotReturnIfAttribute : Attribute +{ + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } +} - /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] - internal sealed class NotNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// - public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; - - /// Gets the return value condition. - public bool ReturnValue { get; } - } +/// Specifies that the method or property will ensure that the listed field and property members have not-null values. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +[ExcludeFromCodeCoverage] +internal sealed class MemberNotNullAttribute : Attribute +{ + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = new[] { member }; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } +} - /// Specifies that the output will be non-null if the named parameter is non-null. - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] - internal sealed class NotNullIfNotNullAttribute : Attribute +/// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +[ExcludeFromCodeCoverage] +internal sealed class MemberNotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) { - /// Initializes the attribute with the associated parameter name. - /// - /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. - /// - public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; - - /// Gets the associated parameter name. - public string ParameterName { get; } + ReturnValue = returnValue; + Members = new[] { member }; } - /// Applied to a method that will never return under any circumstance. - [AttributeUsage(AttributeTargets.Method, Inherited = false)] - internal sealed class DoesNotReturnAttribute : Attribute { } - - /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] - internal sealed class DoesNotReturnIfAttribute : Attribute + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) { - /// Initializes the attribute with the specified parameter value. - /// - /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to - /// the associated parameter matches this value. - /// - public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; - - /// Gets the condition parameter value. - public bool ParameterValue { get; } + ReturnValue = returnValue; + Members = members; } - /// Specifies that the method or property will ensure that the listed field and property members have not-null values. - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] - internal sealed class MemberNotNullAttribute : Attribute - { - /// Initializes the attribute with a field or property member. - /// - /// The field or property member that is promised to be not-null. - /// - public MemberNotNullAttribute(string member) => Members = new[] { member }; - - /// Initializes the attribute with the list of field and property members. - /// - /// The list of field and property members that are promised to be not-null. - /// - public MemberNotNullAttribute(params string[] members) => Members = members; - - /// Gets field or property member names. - public string[] Members { get; } - } + /// Gets the return value condition. + public bool ReturnValue { get; } - /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] - internal sealed class MemberNotNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition and a field or property member. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// - /// - /// The field or property member that is promised to be not-null. - /// - public MemberNotNullWhenAttribute(bool returnValue, string member) - { - ReturnValue = returnValue; - Members = new[] { member }; - } - - /// Initializes the attribute with the specified return value condition and list of field and property members. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// - /// - /// The list of field and property members that are promised to be not-null. - /// - public MemberNotNullWhenAttribute(bool returnValue, params string[] members) - { - ReturnValue = returnValue; - Members = members; - } - - /// Gets the return value condition. - public bool ReturnValue { get; } - - /// Gets field or property member names. - public string[] Members { get; } - } + /// Gets field or property member names. + public string[] Members { get; } } #endif diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index 3651da5..da56bc0 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.ComponentModel; -using System.DirectoryServices; using System.DirectoryServices.AccountManagement; using System.Linq; using System.Management.Automation; @@ -71,6 +69,7 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable public string[]? Exclude { get; set; } [Parameter] + [ArgumentCompleter(typeof(LdapCompleter))] public string[]? Properties { get; set; } protected override void BeginProcessing() diff --git a/src/PSADTree/TreeObjectBase.cs b/src/PSADTree/TreeObjectBase.cs index 452f00c..b2a3e4d 100644 --- a/src/PSADTree/TreeObjectBase.cs +++ b/src/PSADTree/TreeObjectBase.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.DirectoryServices; using System.DirectoryServices.AccountManagement; +using System.Linq; using System.Security.Principal; using PSADTree.Extensions; @@ -57,6 +58,7 @@ protected TreeObjectBase( UserPrincipalName = treeObject.UserPrincipalName; Description = treeObject.Description; DisplayName = treeObject.DisplayName; + AdditionalProperties = treeObject.AdditionalProperties; } protected TreeObjectBase(string source, Principal principal, string[]? properties) @@ -97,21 +99,31 @@ protected TreeObjectBase( return null; } + if (Properties.Any(e => e == "*")) + { + return GetAllAttributes(entry); + } + Dictionary additionalProperties = new( capacity: Properties.Length, StringComparer.OrdinalIgnoreCase); foreach (string property in Properties) { - if (property.Equals("nTSecurityDescriptor", StringComparison.OrdinalIgnoreCase)) + if (!LdapMap.Instance.TryGetValue(property, out string? ldapDn)) + { + ldapDn = property; + } + + if (ldapDn.Equals("nTSecurityDescriptor", StringComparison.OrdinalIgnoreCase)) { additionalProperties[property] = GetAcl(entry); continue; } - if (entry.Properties.Contains(property)) + if (entry.Properties.Contains(ldapDn)) { - additionalProperties[property] = entry.Properties[property][0]; + additionalProperties[property] = entry.Properties[ldapDn].Value; } } @@ -120,6 +132,26 @@ protected TreeObjectBase( : new ReadOnlyDictionary(additionalProperties); } + private ReadOnlyDictionary GetAllAttributes(DirectoryEntry entry) + { + Dictionary additionalProperties = new( + capacity: entry.Properties.Count, + StringComparer.OrdinalIgnoreCase); + + foreach (string property in entry.Properties.PropertyNames) + { + if (property.Equals("nTSecurityDescriptor", StringComparison.OrdinalIgnoreCase)) + { + additionalProperties[property] = GetAcl(entry); + continue; + } + + additionalProperties[property] = entry.Properties[property].Value; + } + + return new ReadOnlyDictionary(additionalProperties); + } + private static ActiveDirectorySecurity? GetAcl(DirectoryEntry entry) { using DirectorySearcher searcher = new(entry, null, ["nTSecurityDescriptor"]) From 2844b0a4b0b1bf91864e21d1d4f948b33255d9cb Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Fri, 23 Jan 2026 06:31:13 -0800 Subject: [PATCH 04/14] more progress --- src/PSADTree/Extensions/MiscExtensions.cs | 89 +++++++++++++++++++-- src/PSADTree/PSADTreeCmdletBase.cs | 8 +- src/PSADTree/TreeComputer.cs | 4 +- src/PSADTree/TreeGroup.cs | 4 +- src/PSADTree/TreeObjectBase.cs | 95 +---------------------- src/PSADTree/TreeUser.cs | 4 +- 6 files changed, 100 insertions(+), 104 deletions(-) diff --git a/src/PSADTree/Extensions/MiscExtensions.cs b/src/PSADTree/Extensions/MiscExtensions.cs index a1a1171..67cf2ee 100644 --- a/src/PSADTree/Extensions/MiscExtensions.cs +++ b/src/PSADTree/Extensions/MiscExtensions.cs @@ -1,6 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.DirectoryServices; using System.DirectoryServices.AccountManagement; +using System.Linq; using System.Management.Automation; namespace PSADTree.Extensions; @@ -18,14 +22,89 @@ internal static bool TryGetProperty( [NotNullWhen(true)] out T? value) { value = default; - if (!search.Properties.Contains(property)) + ResultPropertyValueCollection? toConvert = search.Properties[property]; + return toConvert is not null and { Count: > 0 } + && LanguagePrimitives.TryConvertTo(toConvert, out value); + } + + internal static DirectoryEntry GetDirectoryEntry(this Principal principal) + => (DirectoryEntry)principal.GetUnderlyingObject(); + + internal static ReadOnlyDictionary? GetAdditionalProperties( + this Principal principal, + string[] properties) + { + DirectoryEntry entry = principal.GetDirectoryEntry(); + + if (properties.Length == 0) + return null; + + if (properties.Any(e => e == "*")) + return entry.GetAllAttributes(); + + Dictionary additionalProperties = new( + capacity: properties.Length, + StringComparer.OrdinalIgnoreCase); + + foreach (string property in properties) { - return false; + if (!LdapMap.Instance.TryGetValue(property, out string? ldapDn)) + { + ldapDn = property; + } + + if (IsSecurityDescriptor(property)) + { + additionalProperties[property] = GetAcl(entry); + continue; + } + + if (entry.Properties.Contains(ldapDn)) + { + additionalProperties[property] = entry.Properties[ldapDn].Value; + } } - return LanguagePrimitives.TryConvertTo(search.Properties[property][0], out value); + return additionalProperties is { Count: 0 } ? null : new(additionalProperties); } - internal static DirectoryEntry GetDirectoryEntry(this Principal principal) - => (DirectoryEntry)principal.GetUnderlyingObject(); + private static ReadOnlyDictionary GetAllAttributes(this DirectoryEntry entry) + { + Dictionary additionalProperties = new( + capacity: entry.Properties.Count, + StringComparer.OrdinalIgnoreCase); + + foreach (string property in entry.Properties.PropertyNames) + { + if (IsSecurityDescriptor(property)) + { + additionalProperties[property] = entry.GetAcl(); + continue; + } + + additionalProperties[property] = entry.Properties[property].Value; + } + + return new(additionalProperties); + } + + private static ActiveDirectorySecurity? GetAcl(this DirectoryEntry entry) + { + using DirectorySearcher searcher = new(entry, null, ["nTSecurityDescriptor"]) + { + SecurityMasks = SecurityMasks.Group | SecurityMasks.Owner | SecurityMasks.Dacl + }; + + SearchResult? result = searcher.FindOne(); + + if (result is null || !result.TryGetProperty("nTSecurityDescriptor", out byte[]? descriptor)) + return null; + + ActiveDirectorySecurity acl = new(); + acl.SetSecurityDescriptorBinaryForm(descriptor); + return acl; + } + + private static bool IsSecurityDescriptor(string ldapDn) + => ldapDn.Equals("nTSecurityDescriptor", StringComparison.OrdinalIgnoreCase); } diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index da56bc0..3a4855e 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -21,6 +21,8 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable private WildcardPattern[]? _exclusionPatterns; + private string[]? _properties; + private const WildcardOptions WildcardPatternOptions = WildcardOptions.Compiled | WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase; @@ -70,7 +72,11 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable [Parameter] [ArgumentCompleter(typeof(LdapCompleter))] - public string[]? Properties { get; set; } + public string[] Properties + { + get => _properties ??= []; + set => _properties = [.. _properties.Where(e => !string.IsNullOrWhiteSpace(e))]; + } protected override void BeginProcessing() { diff --git a/src/PSADTree/TreeComputer.cs b/src/PSADTree/TreeComputer.cs index a9217f3..09bf35d 100644 --- a/src/PSADTree/TreeComputer.cs +++ b/src/PSADTree/TreeComputer.cs @@ -16,12 +16,12 @@ internal TreeComputer( string source, TreeGroup? parent, ComputerPrincipal computer, - string[]? properties, + string[] properties, int depth) : base(source, parent, computer, properties, depth) { } - internal TreeComputer(string source, ComputerPrincipal computer, string[]? properties) + internal TreeComputer(string source, ComputerPrincipal computer, string[] properties) : base(source, computer, properties) { } diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index 8369673..dd11366 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -41,7 +41,7 @@ private TreeGroup( IsCircular = group.IsCircular; } - internal TreeGroup(string source, GroupPrincipal group, string[]? properties) + internal TreeGroup(string source, GroupPrincipal group, string[] properties) : base(source, group, properties) { _children = []; @@ -51,7 +51,7 @@ internal TreeGroup( string source, TreeGroup? parent, GroupPrincipal group, - string[]? properties, + string[] properties, int depth) : base(source, parent, group, properties, depth) { diff --git a/src/PSADTree/TreeObjectBase.cs b/src/PSADTree/TreeObjectBase.cs index b2a3e4d..1aba91c 100644 --- a/src/PSADTree/TreeObjectBase.cs +++ b/src/PSADTree/TreeObjectBase.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.DirectoryServices; using System.DirectoryServices.AccountManagement; -using System.Linq; using System.Security.Principal; using PSADTree.Extensions; @@ -61,7 +58,7 @@ protected TreeObjectBase( AdditionalProperties = treeObject.AdditionalProperties; } - protected TreeObjectBase(string source, Principal principal, string[]? properties) + protected TreeObjectBase(string source, Principal principal, string[] properties) { Source = source; SamAccountName = principal.SamAccountName; @@ -74,14 +71,14 @@ protected TreeObjectBase(string source, Principal principal, string[]? propertie UserPrincipalName = principal.UserPrincipalName; Description = principal.Description; DisplayName = principal.DisplayName; - AdditionalProperties = GetAdditionalProperties(principal.GetDirectoryEntry(), properties); + AdditionalProperties = principal.GetAdditionalProperties(properties); } protected TreeObjectBase( string source, TreeGroup? parent, Principal principal, - string[]? properties, + string[] properties, int depth) : this(source, principal, properties) { @@ -90,92 +87,6 @@ protected TreeObjectBase( Parent = parent; } - private ReadOnlyDictionary? GetAdditionalProperties( - DirectoryEntry entry, - string[]? Properties) - { - if (Properties is null or { Length: 0 }) - { - return null; - } - - if (Properties.Any(e => e == "*")) - { - return GetAllAttributes(entry); - } - - Dictionary additionalProperties = new( - capacity: Properties.Length, - StringComparer.OrdinalIgnoreCase); - - foreach (string property in Properties) - { - if (!LdapMap.Instance.TryGetValue(property, out string? ldapDn)) - { - ldapDn = property; - } - - if (ldapDn.Equals("nTSecurityDescriptor", StringComparison.OrdinalIgnoreCase)) - { - additionalProperties[property] = GetAcl(entry); - continue; - } - - if (entry.Properties.Contains(ldapDn)) - { - additionalProperties[property] = entry.Properties[ldapDn].Value; - } - } - - return additionalProperties.Count == 0 - ? null - : new ReadOnlyDictionary(additionalProperties); - } - - private ReadOnlyDictionary GetAllAttributes(DirectoryEntry entry) - { - Dictionary additionalProperties = new( - capacity: entry.Properties.Count, - StringComparer.OrdinalIgnoreCase); - - foreach (string property in entry.Properties.PropertyNames) - { - if (property.Equals("nTSecurityDescriptor", StringComparison.OrdinalIgnoreCase)) - { - additionalProperties[property] = GetAcl(entry); - continue; - } - - additionalProperties[property] = entry.Properties[property].Value; - } - - return new ReadOnlyDictionary(additionalProperties); - } - - private static ActiveDirectorySecurity? GetAcl(DirectoryEntry entry) - { - using DirectorySearcher searcher = new(entry, null, ["nTSecurityDescriptor"]) - { - SecurityMasks = SecurityMasks.Group | SecurityMasks.Owner | SecurityMasks.Dacl - }; - - SearchResult? result = searcher.FindOne(); - - if (result is null) - { - return null; - } - - if (!result.TryGetProperty("nTSecurityDescriptor", out byte[]? descriptor)) - { - return null; - } - - ActiveDirectorySecurity acl = new(); - acl.SetSecurityDescriptorBinaryForm(descriptor); - return acl; - } - public override string ToString() => DistinguishedName; internal abstract TreeObjectBase Clone(TreeGroup parent, string source, int depth); diff --git a/src/PSADTree/TreeUser.cs b/src/PSADTree/TreeUser.cs index 0f31aa0..be56563 100644 --- a/src/PSADTree/TreeUser.cs +++ b/src/PSADTree/TreeUser.cs @@ -25,12 +25,12 @@ internal TreeUser( string source, TreeGroup? parent, UserPrincipal user, - string[]? properties, + string[] properties, int depth) : base(source, parent, user, properties, depth) { } - internal TreeUser(string source, UserPrincipal user, string[]? properties) + internal TreeUser(string source, UserPrincipal user, string[] properties) : base(source, user, properties) { } From 872aa219e38bda9062b0332f7289008ca5ccdbca Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sat, 24 Jan 2026 08:06:41 -0800 Subject: [PATCH 05/14] some improvements --- module/PSADTree.Format.ps1xml | 10 - module/PSADTree.psd1 | 4 +- src/PSADTree/Extensions/MiscExtensions.cs | 74 ++- src/PSADTree/IADsLargeInteger.cs | 28 ++ src/PSADTree/LdapCompleter.cs | 4 +- src/PSADTree/LdapMap.cs | 539 +++++++++++----------- src/PSADTree/PSADTree.csproj | 16 +- src/PSADTree/PSADTreeCmdletBase.cs | 2 +- 8 files changed, 378 insertions(+), 299 deletions(-) create mode 100644 src/PSADTree/IADsLargeInteger.cs diff --git a/module/PSADTree.Format.ps1xml b/module/PSADTree.Format.ps1xml index 9eef827..22aa893 100644 --- a/module/PSADTree.Format.ps1xml +++ b/module/PSADTree.Format.ps1xml @@ -12,19 +12,12 @@ - - Left - Right @@ -35,9 +28,6 @@ - $_.Domain -replace '^DC=|(?<!\\),.+' diff --git a/module/PSADTree.psd1 b/module/PSADTree.psd1 index 0e1db7f..422dc16 100644 --- a/module/PSADTree.psd1 +++ b/module/PSADTree.psd1 @@ -9,14 +9,14 @@ @{ # Script module or binary module file associated with this manifest. RootModule = if ($PSEdition -eq 'Core') { - 'bin/net6.0/PSADTree.dll' + 'bin/net8.0-windows/PSADTree.dll' } else { 'bin/net472/PSADTree.dll' } # Version number of this module. - ModuleVersion = '1.1.6' + ModuleVersion = '1.2.0' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/src/PSADTree/Extensions/MiscExtensions.cs b/src/PSADTree/Extensions/MiscExtensions.cs index 67cf2ee..7a26d1d 100644 --- a/src/PSADTree/Extensions/MiscExtensions.cs +++ b/src/PSADTree/Extensions/MiscExtensions.cs @@ -6,6 +6,9 @@ using System.DirectoryServices.AccountManagement; using System.Linq; using System.Management.Automation; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Text; namespace PSADTree.Extensions; @@ -48,21 +51,27 @@ internal static DirectoryEntry GetDirectoryEntry(this Principal principal) foreach (string property in properties) { - if (!LdapMap.Instance.TryGetValue(property, out string? ldapDn)) + if (!LdapMap.TryGetValue(property, out string? ldapDn)) { ldapDn = property; } if (IsSecurityDescriptor(property)) { - additionalProperties[property] = GetAcl(entry); + additionalProperties[property] = entry.GetAcl(); continue; } - if (entry.Properties.Contains(ldapDn)) + object? value = entry.Properties[ldapDn]?.Value; + + if (value is null) continue; + if (IsIAdsLargeInteger(value, out long? fileTime)) { - additionalProperties[property] = entry.Properties[ldapDn].Value; + additionalProperties[property] = fileTime; + continue; } + + additionalProperties[property] = value; } return additionalProperties is { Count: 0 } ? null : new(additionalProperties); @@ -82,27 +91,60 @@ internal static DirectoryEntry GetDirectoryEntry(this Principal principal) continue; } - additionalProperties[property] = entry.Properties[property].Value; + object? value = entry.Properties[property]?.Value; + + if (value is null) continue; + if (IsIAdsLargeInteger(value, out long? fileTime)) + { + additionalProperties[property] = fileTime; + continue; + } + + additionalProperties[property] = value; } return new(additionalProperties); } - private static ActiveDirectorySecurity? GetAcl(this DirectoryEntry entry) + private static PSObject GetAcl(this DirectoryEntry entry) { - using DirectorySearcher searcher = new(entry, null, ["nTSecurityDescriptor"]) - { - SecurityMasks = SecurityMasks.Group | SecurityMasks.Owner | SecurityMasks.Dacl - }; + Type target = typeof(NTAccount); + ActiveDirectorySecurity acl = entry.ObjectSecurity; + AuthorizationRuleCollection rules = acl.GetAccessRules(true, true, target); + return PSObject.AsPSObject(acl) + .AddProperty("Path", entry.Path) + .AddProperty("Owner", acl.GetOwner(target)) + .AddProperty("Group", acl.GetGroup(target)) + .AddProperty("Sddl", acl.GetSecurityDescriptorSddlForm(AccessControlSections.All)) + .AddProperty("Access", rules) + .AddProperty("AccessToString", rules.GetAccessToString()); + } - SearchResult? result = searcher.FindOne(); + private static PSObject AddProperty(this PSObject pSObject, string name, object? value) + { + pSObject.Properties.Add(new PSNoteProperty(name, value)); + return pSObject; + } - if (result is null || !result.TryGetProperty("nTSecurityDescriptor", out byte[]? descriptor)) - return null; + private static string GetAccessToString(this AuthorizationRuleCollection rules) + { + StringBuilder builder = new(); + foreach (ActiveDirectoryAccessRule rule in rules) + builder.AppendLine($"{rule.IdentityReference} {rule.AccessControlType}"); + + return builder.ToString(); + } + + private static bool IsIAdsLargeInteger( + object value, + [NotNullWhen(true)] out long? fileTime) + { + fileTime = default; + if (value is not IAdsLargeInteger largeInt) + return false; - ActiveDirectorySecurity acl = new(); - acl.SetSecurityDescriptorBinaryForm(descriptor); - return acl; + fileTime = (largeInt.HighPart << 32) + largeInt.LowPart; + return true; } private static bool IsSecurityDescriptor(string ldapDn) diff --git a/src/PSADTree/IADsLargeInteger.cs b/src/PSADTree/IADsLargeInteger.cs new file mode 100644 index 0000000..99346e0 --- /dev/null +++ b/src/PSADTree/IADsLargeInteger.cs @@ -0,0 +1,28 @@ +using System.Runtime.InteropServices; +using System.Security; + +namespace PSADTree; + +[ComImport, + Guid("9068270b-0939-11d1-8be1-00c04fd8d503"), + InterfaceType(ComInterfaceType.InterfaceIsDual)] +public interface IAdsLargeInteger +{ + long HighPart + { + [SuppressUnmanagedCodeSecurity] + get; + + [SuppressUnmanagedCodeSecurity] + set; + } + + long LowPart + { + [SuppressUnmanagedCodeSecurity] + get; + + [SuppressUnmanagedCodeSecurity] + set; + } +} diff --git a/src/PSADTree/LdapCompleter.cs b/src/PSADTree/LdapCompleter.cs index 74942c0..030c4b7 100644 --- a/src/PSADTree/LdapCompleter.cs +++ b/src/PSADTree/LdapCompleter.cs @@ -14,7 +14,7 @@ public IEnumerable CompleteArgument( CommandAst commandAst, IDictionary fakeBoundParameters) { - foreach (string key in LdapMap.Instance.Keys) + foreach (string key in LdapMap.Keys) { if (key.StartsWith(wordToComplete, System.StringComparison.OrdinalIgnoreCase)) { @@ -22,7 +22,7 @@ public IEnumerable CompleteArgument( key, key, CompletionResultType.ParameterValue, - $"LDAP DisplayName: {LdapMap.Instance[key]}"); + $"LDAP DisplayName: {LdapMap.Get(key)}"); } } } diff --git a/src/PSADTree/LdapMap.cs b/src/PSADTree/LdapMap.cs index fd81289..b83045e 100644 --- a/src/PSADTree/LdapMap.cs +++ b/src/PSADTree/LdapMap.cs @@ -1,272 +1,287 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; namespace PSADTree; internal static class LdapMap { - internal static Dictionary Instance { get; } + internal static ReadOnlyDictionary Instance { get; } - static LdapMap() => Instance = new(StringComparer.OrdinalIgnoreCase) + internal static bool TryGetValue(string property, [NotNullWhen(true)] out string? ldapDn) + => Instance.TryGetValue(property, out ldapDn); + + internal static string Get(string property) => Instance[property]; + + internal static ReadOnlyDictionary.KeyCollection Keys { get => Instance.Keys; } + + + static LdapMap() { - ["AccountExpirationDate"] = "accountExpires", - ["AccountLockoutTime"] = "lockoutTime", - ["AccountPassword"] = "unicodePwd", - ["ADAMAllowReversiblePasswordEncryption"] = "ms-DS-UserEncryptedTextPasswordAllowed", - ["ADAMDisabled"] = "msDS-UserAccountDisabled", - ["ADAMLockedOut"] = "ms-DS-UserAccountAutoLocked", - ["ADAMPasswordExpired"] = "msDS-UserPasswordExpired", - ["ADAMPasswordNeverExpires"] = "msDS-UserDontExpirePassword", - ["ADAMPasswordNotRequired"] = "ms-DS-UserPasswordNotRequired", - ["AllowedDNSSuffixes"] = "msDS-AllowedDNSSuffixes", - ["AllowedPasswordReplicationPolicy"] = "msDS-RevealOnDemandGroup", - ["AllowedToActOnBehalfOf"] = "msDS-AllowedToActOnBehalfOfOtherIdentity", - ["AppliesTo"] = "msDS-PSOAppliesTo", - ["AssignedAuthNPolicyBL"] = "msDS-AssignedAuthNPolicyBL", - ["AssignedAuthNPolicySiloBL"] = "msDS-AssignedAuthNPolicySiloBL", - ["AttributeSyntax"] = "attributeSyntax", - ["AuthenticatedAccounts"] = "msDS-AuthenticatedToAccountlist", - ["AuthenticationPolicy"] = "msDS-AssignedAuthNPolicy", - ["AuthenticationPolicySilo"] = "msDS-AssignedAuthNPolicySilo", - ["AuthenticationPolicySiloMembers"] = "msDS-AuthNPolicySiloMembers", - ["AuthNPolicyEnforce"] = "msDS-AuthNPolicyEnforced", - ["AuthNPolicySiloEnforce"] = "msDS-AuthNPolicySiloEnforced", - ["BadLogonCount"] = "badPwdCount", - ["BehaviorVersion"] = "msDS-Behavior-Version", - ["CanonicalName"] = "canonicalName", - ["Certificates"] = "userCertificate", - ["City"] = "l", - ["CNumConsecutiveSyncFailures"] = "cNumConsecutiveSyncFailures", - ["CNumFailures"] = "cNumFailures", - ["CommonName"] = "cn", - ["Company"] = "company", - ["ComplexityEnabled"] = "msDS-PasswordComplexityEnabled", - ["ComputerAllowedToAuthenticateTo"] = "msDS-ComputerAllowedToAuthenticateTo", - ["ComputerAuthenticationPolicy"] = "msDS-ComputerAuthNPolicy", - ["ComputerAuthNPolicyBL"] = "msDS-ComputerAuthNPolicyBL", - ["ComputerTGTLifetime"] = "msDS-ComputerTGTLifetime", - ["Cost"] = "cost", - ["Country"] = "c", - ["CreationTimeStamp"] = "createTimeStamp", - ["DefaultLockoutDuration"] = "lockoutDuration", - ["DefaultLockoutObservationWindow"] = "lockoutObservationWindow", - ["DefaultLockoutThreshold"] = "lockoutThreshold", - ["DefaultMaxPasswordAge"] = "maxPwdAge", - ["DefaultMinPasswordAge"] = "minPwdAge", - ["DefaultMinPasswordLength"] = "minPwdLength", - ["DefaultPasswordHistoryCount"] = "pwdHistoryLength", - ["DefaultPasswordProperties"] = "pwdProperties", - ["DeniedPasswordReplicationPolicy"] = "msDS-NeverRevealGroup", - ["Department"] = "department", - ["Description"] = "description", - ["DisplayName"] = "displayName", - ["DistinguishedName"] = "distinguishedName", - ["Division"] = "division", - ["DNSHostName"] = "dNSHostName", - ["DNSRoot"] = "dnsRoot", - ["DwLastResult"] = "dwLastResult", - ["DwLastSyncResult"] = "dwLastSyncResult", - ["DwReplicaFlags"] = "dwReplicaFlags", - ["DwVersion"] = "dwVersion", - ["EmailAddress"] = "mail", - ["EmployeeID"] = "employeeID", - ["EmployeeNumber"] = "employeeNumber", - ["Enabled"] = "Enabled", - ["EnabledScopes"] = "msDS-EnabledFeatureBL", - ["Fax"] = "facsimileTelephoneNumber", - ["FeatureGUID"] = "msDS-OptionalFeatureGUID", - ["FeatureScope"] = "msDS-OptionalFeatureFlags", - ["FTimeCreated"] = "ftimeCreated", - ["FTimeDeleted"] = "ftimeDeleted", - ["FTimeEnqueued"] = "ftimeEnqueued", - ["FTimeFirstFailure"] = "ftimeFirstFailure", - ["FTimeLastOriginatingChange"] = "ftimeLastOriginatingChange", - ["FTimeLastSyncAttempt"] = "ftimeLastSyncAttempt", - ["FTimeLastSyncSuccess"] = "ftimeLastSyncSuccess", - ["GivenName"] = "givenName", - ["GroupType"] = "groupType", - ["HomeDirectory"] = "homeDirectory", - ["HomeDrive"] = "homeDrive", - ["HomePhone"] = "homePhone", - ["HostComputers"] = "msDS-HostServiceAccountBL", - ["IncomingTrust"] = "msDS-TDOEgressBL", - ["Initials"] = "initials", - ["InstanceType"] = "instanceType", - ["InterSiteTopologyGenerator"] = "interSiteTopologyGenerator", - ["IsDefunct"] = "isDefunct", - ["IsDeleted"] = "isDeleted", - ["LastBadPasswordAttempt"] = "badPasswordTime", - ["LastKnownParent"] = "lastKnownParent", - ["LastKnownRDN"] = "msDS-LastKnownRDN", - ["LastLogonReplicationInterval"] = "msDS-LogonTimeSyncInterval", - ["LastLogonTimeStamp"] = "lastLogonTimestamp", - ["LdapDisplayName"] = "lDAPDisplayName", - ["LinkedGroupPolicyObjects"] = "gpLink", - ["Location"] = "location", - ["LockoutDuration"] = "msDS-LockoutDuration", - ["LockoutObservationWindow"] = "msDS-LockoutObservationWindow", - ["LockoutThreshold"] = "msDS-LockoutThreshold", - ["LogonWorkstations"] = "userWorkstations", - ["ManagedBy"] = "managedBy", - ["Manager"] = "manager", - ["MaxPasswordAge"] = "msDS-MaximumPasswordAge", - ["Member"] = "member", - ["MemberRulesInCAP"] = "msAuthz-MemberRulesInCentralAccessPolicy", - ["MemberRulesInCAPBL"] = "msAuthz-MemberRulesInCentralAccessPolicyBL", - ["MembersOfResourcePropertyList"] = "msDS-MembersOfResourcePropertyList", - ["MinPasswordAge"] = "msDS-MinimumPasswordAge", - ["MinPasswordLength"] = "msDS-MinimumPasswordLength", - ["MobilePhone"] = "mobile", - ["ModifiedTimeStamp"] = "modifyTimeStamp", - ["MsaGroupMembership"] = "msDS-GroupMSAMembership", - ["MsaManagedPasswordInterval"] = "msDS-ManagedPasswordInterval", - ["MSAuthzCentralAccessPolicyID"] = "msAuthz-CentralAccessPolicyID", - ["MSAuthzEffectiveDACL"] = "msAuthz-EffectiveSecurityPolicy", - ["MSAuthzLastEffectiveDACL"] = "msAuthz-LastEffectiveSecurityPolicy", - ["MSAuthzProposedDACL"] = "msAuthz-ProposedSecurityPolicy", - ["MSAuthzResourceCondition"] = "msAuthz-ResourceCondition", - ["MsDSAppliesToResourceTypes"] = "msDS-AppliesToResourceTypes", - ["MsDSClaimAttributeSource"] = "msDS-ClaimAttributeSource", - ["MsDSClaimIsSingleValued"] = "msDS-ClaimIsSingleValued", - ["MsDSClaimIsValueSpaceRestricted"] = "msDS-ClaimIsValueSpaceRestricted", - ["MsDSClaimPossibleValues"] = "msDS-ClaimPossibleValues", - ["MsDSClaimSharesPossibleValuesWithBL"] = "msDS-ClaimSharesPossibleValuesWithBL", - ["MsDSClaimSource"] = "msDS-ClaimSource", - ["MsDSClaimSourceType"] = "msDS-ClaimSourceType", - ["MsDSClaimTypeAppliesToClass"] = "msDS-ClaimTypeAppliesToClass", - ["MsDSClaimValueType"] = "msDS-ClaimValueType", - ["MSDShasFullReplicaNCs"] = "msDS-hasFullReplicaNCs", - ["MSDShasMasterNCs"] = "msDS-hasMasterNCs", - ["MSDSIsPossibleValuesPresent"] = "msDS-IsPossibleValuesPresent", - ["MsDSIsUsedAsResourceSecurityAttribute"] = "msDS-IsUsedAsResourceSecurityAttribute", - ["MSDSIsUserCachableAtRodc"] = "msDS-IsUserCachableAtRodc", - ["MsDSMembersOfResourcePropertyListBL"] = "msDS-MembersOfResourcePropertyListBL", - ["MSDSPortLDAP"] = "msDS-PortLDAP", - ["MsDSSClaimSharesPossibleValuesWith"] = "msDS-ClaimSharesPossibleValuesWith", - ["MsDSURI"] = "msDS-URI", - ["MSDSUserAccountControlComputed"] = "msDS-User-Account-Control-Computed", - ["MsDSValueTypeReference"] = "msDS-ValueTypeReference", - ["MSDSValueTypeReferenceBL"] = "msDS-ValueTypeReferenceBL", - ["Name"] = "name", - ["NCName"] = "nCName", - ["NETBIOSName"] = "nETBIOSName", - ["NTMixedDomainMode"] = "ntMixedDomain", - ["NTSecurityDescriptor"] = "nTSecurityDescriptor", - ["ObjectCategory"] = "objectCategory", - ["ObjectClass"] = "objectClass", - ["ObjectGUID"] = "objectGUID", - ["ObjectSid"] = "objectSid", - ["Office"] = "physicalDeliveryOfficeName", - ["OfficePhone"] = "telephoneNumber", - ["Options"] = "Options", - ["OpType"] = "OpType", - ["Organization"] = "o", - ["OS"] = "operatingSystem", - ["OSHotfix"] = "operatingSystemHotfix", - ["OSServicePack"] = "operatingSystemServicePack", - ["OSVersion"] = "operatingSystemVersion", - ["OtherName"] = "middleName", - ["OutgoingTrust"] = "msDS-TDOIngressBL", - ["PartiallyReplicatedNamingContexts"] = "hasPartialReplicaNCs", - ["PasswordHistoryCount"] = "msDS-PasswordHistoryLength", - ["PasswordLastSet"] = "pwdLastSet", - ["POBox"] = "postOfficeBox", - ["PostalCode"] = "postalCode", - ["Precedence"] = "msDS-PasswordSettingsPrecedence", - ["PrimaryGroup"] = "primaryGroupID", - ["ProfilePath"] = "profilePath", - ["PszAsyncIntersiteTransportDN"] = "pszAsyncIntersiteTransportDN", - ["PszAttributeName"] = "pszAttributeName", - ["PszDsaAddress"] = "pszDsaAddress", - ["PszDsaDN"] = "pszDsaDN", - ["PszLastOriginatingDsaDN"] = "pszLastOriginatingDsaDN", - ["PszNamingContext"] = "pszNamingContext", - ["PszObjectDn"] = "pszObjectDn", - ["PszSourceDsaAddress"] = "pszSourceDsaAddress", - ["PszSourceDsaDN"] = "pszSourceDsaDN", - ["PublicKeyRequiredPasswordRolling"] = "msDS-ExpirePasswordsOnSmartCardOnlyAccounts", - ["ReplicateFromDirectoryServer"] = "fromServer", - ["ReplicateSingleObject"] = "replicateSingleObject", - ["ReplicationAttributeMetadata"] = "msDS-ReplAttributeMetaData", - ["ReplicationAttributeMetadataObjectType"] = "DS_REPL_ATTR_META_DATA", - ["ReplicationAttributeValueMetadata"] = "msDS-ReplValueMetaData", - ["ReplicationAttributeValueMetadataObjectType"] = "DS_REPL_VALUE_META_DATA", - ["ReplicationConnectionFailures"] = "msDS-ReplConnectionFailures", - ["ReplicationFailuresObjectType"] = "DS_REPL_KCC_DSA_FAILURE", - ["ReplicationFrequency"] = "replInterval", - ["ReplicationInboundPartners"] = "msDS-NCReplInboundNeighbors", - ["ReplicationLinkFailures"] = "msDS-ReplLinkFailures", - ["ReplicationOutboundPartners"] = "msDS-NCReplOutboundNeighbors", - ["ReplicationPartnersObjectType"] = "DS_REPL_NEIGHBOR", - ["ReplicationQueue"] = "msDS-ReplPendingOps", - ["ReplicationQueueObjectType"] = "DS_REPL_OP", - ["ReplicationSchedule"] = "schedule", - ["ReplicationUpToDatenessVector"] = "msDS-NCReplCursors", - ["ReplicationUpToDatenessVectorObjectType"] = "DS_REPL_CURSOR", - ["RequiredDomainMode"] = "msDS-RequiredDomainBehaviorVersion", - ["RequiredForestMode"] = "msDS-RequiredForestBehaviorVersion", - ["ResultantPSO"] = "msDS-ResultantPSO", - ["RevealedAccounts"] = "msDS-RevealedList", - ["ReversibleEncryptionEnabled"] = "msDS-PasswordReversibleEncryptionEnabled", - ["RollingNTLMSecret"] = "msDS-StrongNTLMPolicy", - ["Rule"] = "msDS-TransformationRules", - ["SamAccountName"] = "sAMAccountName", - ["ScriptPath"] = "scriptPath", - ["SDRightsEffective"] = "sdRightsEffective", - ["SearchFlags"] = "searchFlags", - ["ServerReference"] = "serverReference", - ["ServerReferenceBL"] = "serverReferenceBL", - ["ServiceAccount"] = "msDS-HostServiceAccount", - ["ServiceAllowedNTLMNetworkAuthentication"] = "msDS-ServiceAllowedNTLMNetworkAuthentication", - ["ServiceAllowedToAuthenticateFrom"] = "msDS-ServiceAllowedToAuthenticateFrom", - ["ServiceAllowedToAuthenticateTo"] = "msDS-ServiceAllowedToAuthenticateTo", - ["ServiceAuthenticationPolicy"] = "msDS-ServiceAuthNPolicy", - ["ServiceAuthNPolicyBL"] = "msDS-ServiceAuthNPolicyBL", - ["ServicePrincipalNames"] = "servicePrincipalName", - ["ServiceTGTLifetime"] = "msDS-ServiceTGTLifetime", - ["Site"] = "siteObject", - ["SiteLinksIncluded"] = "siteLinkList", - ["SitesIncluded"] = "siteList", - ["SourceXmlAttribute"] = "sourceXmlAttribute", - ["SPNSuffixes"] = "msDS-SPNSuffixes", - ["State"] = "st", - ["Street"] = "street", - ["StreetAddress"] = "streetAddress", - ["Subnet"] = "siteObjectBL", - ["SupportedEncryptionTypes"] = "msDS-SupportedEncryptionTypes", - ["Surname"] = "sn", - ["SystemFlags"] = "systemFlags", - ["Target"] = "trustPartner", - ["Title"] = "title", - ["TransportType"] = "transportType", - ["TrustAttributes"] = "trustAttributes", - ["TrustDirection"] = "trustDirection", - ["TrustedPolicy"] = "msDS-EgressClaimsTransformationPolicy", - ["TrustingPolicy"] = "msDS-IngressClaimsTransformationPolicy", - ["TrustType"] = "trustType", - ["UlOptions"] = "ulOptions", - ["UlPriority"] = "ulPriority", - ["UlSerialNumber"] = "ulSerialNumber", - ["UnicodePwd"] = "unicodePwd", - ["UniversalGroupCachingRefreshSite"] = "msDS-Preferred-GC-Site", - ["UPNSuffixes"] = "uPNSuffixes", - ["UserAccountControl"] = "userAccountControl", - ["UserAccountControlComputed"] = "msDS-User-Account-Control-Computed", - ["UserAllowedNTLMNetworkAuthentication"] = "msDS-UserAllowedNTLMNetworkAuthentication", - ["UserAllowedToAuthenticateFrom"] = "msDS-UserAllowedToAuthenticateFrom", - ["UserAllowedToAuthenticateTo"] = "msDS-UserAllowedToAuthenticateTo", - ["UserAuthenticationPolicy"] = "msDS-UserAuthNPolicy", - ["UserAuthNPolicyBL"] = "msDS-UserAuthNPolicyBL", - ["UserPrincipalName"] = "userPrincipalName", - ["UserTGTLifetime"] = "msDS-UserTGTLifetime", - ["UsnAttributeFilter"] = "usnAttributeFilter", - ["UsnLastObjChangeSynced"] = "usnLastObjChangeSynced", - ["UsnLocalChange"] = "usnLocalChange", - ["UsnOriginatingChange"] = "usnOriginatingChange", - ["UuidAsyncIntersiteTransportObjGuid"] = "uuidAsyncIntersiteTransportObjGuid", - ["UuidDsaObjGuid"] = "uuidDsaObjGuid", - ["UuidLastOriginatingDsaInvocationID"] = "uuidLastOriginatingDsaInvocationID", - ["UuidSourceDsaInvocationID"] = "uuidSourceDsaInvocationID", - ["UuidSourceDsaObjGuid"] = "uuidSourceDsaObjGuid", - }; + Dictionary map = new(StringComparer.OrdinalIgnoreCase) + { + ["AccountExpirationDate"] = "accountExpires", + ["AccountLockoutTime"] = "lockoutTime", + ["AccountPassword"] = "unicodePwd", + ["ADAMAllowReversiblePasswordEncryption"] = "ms-DS-UserEncryptedTextPasswordAllowed", + ["ADAMDisabled"] = "msDS-UserAccountDisabled", + ["ADAMLockedOut"] = "ms-DS-UserAccountAutoLocked", + ["ADAMPasswordExpired"] = "msDS-UserPasswordExpired", + ["ADAMPasswordNeverExpires"] = "msDS-UserDontExpirePassword", + ["ADAMPasswordNotRequired"] = "ms-DS-UserPasswordNotRequired", + ["AllowedDNSSuffixes"] = "msDS-AllowedDNSSuffixes", + ["AllowedPasswordReplicationPolicy"] = "msDS-RevealOnDemandGroup", + ["AllowedToActOnBehalfOf"] = "msDS-AllowedToActOnBehalfOfOtherIdentity", + ["AppliesTo"] = "msDS-PSOAppliesTo", + ["AssignedAuthNPolicyBL"] = "msDS-AssignedAuthNPolicyBL", + ["AssignedAuthNPolicySiloBL"] = "msDS-AssignedAuthNPolicySiloBL", + ["AttributeSyntax"] = "attributeSyntax", + ["AuthenticatedAccounts"] = "msDS-AuthenticatedToAccountlist", + ["AuthenticationPolicy"] = "msDS-AssignedAuthNPolicy", + ["AuthenticationPolicySilo"] = "msDS-AssignedAuthNPolicySilo", + ["AuthenticationPolicySiloMembers"] = "msDS-AuthNPolicySiloMembers", + ["AuthNPolicyEnforce"] = "msDS-AuthNPolicyEnforced", + ["AuthNPolicySiloEnforce"] = "msDS-AuthNPolicySiloEnforced", + ["BadLogonCount"] = "badPwdCount", + ["BehaviorVersion"] = "msDS-Behavior-Version", + ["CanonicalName"] = "canonicalName", + ["Certificates"] = "userCertificate", + ["City"] = "l", + ["CNumConsecutiveSyncFailures"] = "cNumConsecutiveSyncFailures", + ["CNumFailures"] = "cNumFailures", + ["CommonName"] = "cn", + ["Company"] = "company", + ["ComplexityEnabled"] = "msDS-PasswordComplexityEnabled", + ["ComputerAllowedToAuthenticateTo"] = "msDS-ComputerAllowedToAuthenticateTo", + ["ComputerAuthenticationPolicy"] = "msDS-ComputerAuthNPolicy", + ["ComputerAuthNPolicyBL"] = "msDS-ComputerAuthNPolicyBL", + ["ComputerTGTLifetime"] = "msDS-ComputerTGTLifetime", + ["Cost"] = "cost", + ["Country"] = "c", + ["CreationTimeStamp"] = "createTimeStamp", + ["DefaultLockoutDuration"] = "lockoutDuration", + ["DefaultLockoutObservationWindow"] = "lockoutObservationWindow", + ["DefaultLockoutThreshold"] = "lockoutThreshold", + ["DefaultMaxPasswordAge"] = "maxPwdAge", + ["DefaultMinPasswordAge"] = "minPwdAge", + ["DefaultMinPasswordLength"] = "minPwdLength", + ["DefaultPasswordHistoryCount"] = "pwdHistoryLength", + ["DefaultPasswordProperties"] = "pwdProperties", + ["DeniedPasswordReplicationPolicy"] = "msDS-NeverRevealGroup", + ["Department"] = "department", + ["Description"] = "description", + ["DisplayName"] = "displayName", + ["DistinguishedName"] = "distinguishedName", + ["Division"] = "division", + ["DNSHostName"] = "dNSHostName", + ["DNSRoot"] = "dnsRoot", + ["DwLastResult"] = "dwLastResult", + ["DwLastSyncResult"] = "dwLastSyncResult", + ["DwReplicaFlags"] = "dwReplicaFlags", + ["DwVersion"] = "dwVersion", + ["EmailAddress"] = "mail", + ["EmployeeID"] = "employeeID", + ["EmployeeNumber"] = "employeeNumber", + ["Enabled"] = "Enabled", + ["EnabledScopes"] = "msDS-EnabledFeatureBL", + ["Fax"] = "facsimileTelephoneNumber", + ["FeatureGUID"] = "msDS-OptionalFeatureGUID", + ["FeatureScope"] = "msDS-OptionalFeatureFlags", + ["FTimeCreated"] = "ftimeCreated", + ["FTimeDeleted"] = "ftimeDeleted", + ["FTimeEnqueued"] = "ftimeEnqueued", + ["FTimeFirstFailure"] = "ftimeFirstFailure", + ["FTimeLastOriginatingChange"] = "ftimeLastOriginatingChange", + ["FTimeLastSyncAttempt"] = "ftimeLastSyncAttempt", + ["FTimeLastSyncSuccess"] = "ftimeLastSyncSuccess", + ["GivenName"] = "givenName", + ["GroupType"] = "groupType", + ["HomeDirectory"] = "homeDirectory", + ["HomeDrive"] = "homeDrive", + ["HomePhone"] = "homePhone", + ["HostComputers"] = "msDS-HostServiceAccountBL", + ["IncomingTrust"] = "msDS-TDOEgressBL", + ["Initials"] = "initials", + ["InstanceType"] = "instanceType", + ["InterSiteTopologyGenerator"] = "interSiteTopologyGenerator", + ["IsDefunct"] = "isDefunct", + ["IsDeleted"] = "isDeleted", + ["LastBadPasswordAttempt"] = "badPasswordTime", + ["LastKnownParent"] = "lastKnownParent", + ["LastKnownRDN"] = "msDS-LastKnownRDN", + ["LastLogonReplicationInterval"] = "msDS-LogonTimeSyncInterval", + ["LastLogonTimeStamp"] = "lastLogonTimestamp", + ["LdapDisplayName"] = "lDAPDisplayName", + ["LinkedGroupPolicyObjects"] = "gpLink", + ["Location"] = "location", + ["LockoutDuration"] = "msDS-LockoutDuration", + ["LockoutObservationWindow"] = "msDS-LockoutObservationWindow", + ["LockoutThreshold"] = "msDS-LockoutThreshold", + ["LogonWorkstations"] = "userWorkstations", + ["ManagedBy"] = "managedBy", + ["Manager"] = "manager", + ["MaxPasswordAge"] = "msDS-MaximumPasswordAge", + ["Member"] = "member", + ["MemberRulesInCAP"] = "msAuthz-MemberRulesInCentralAccessPolicy", + ["MemberRulesInCAPBL"] = "msAuthz-MemberRulesInCentralAccessPolicyBL", + ["MembersOfResourcePropertyList"] = "msDS-MembersOfResourcePropertyList", + ["MinPasswordAge"] = "msDS-MinimumPasswordAge", + ["MinPasswordLength"] = "msDS-MinimumPasswordLength", + ["MobilePhone"] = "mobile", + ["ModifiedTimeStamp"] = "modifyTimeStamp", + ["MsaGroupMembership"] = "msDS-GroupMSAMembership", + ["MsaManagedPasswordInterval"] = "msDS-ManagedPasswordInterval", + ["MSAuthzCentralAccessPolicyID"] = "msAuthz-CentralAccessPolicyID", + ["MSAuthzEffectiveDACL"] = "msAuthz-EffectiveSecurityPolicy", + ["MSAuthzLastEffectiveDACL"] = "msAuthz-LastEffectiveSecurityPolicy", + ["MSAuthzProposedDACL"] = "msAuthz-ProposedSecurityPolicy", + ["MSAuthzResourceCondition"] = "msAuthz-ResourceCondition", + ["MsDSAppliesToResourceTypes"] = "msDS-AppliesToResourceTypes", + ["MsDSClaimAttributeSource"] = "msDS-ClaimAttributeSource", + ["MsDSClaimIsSingleValued"] = "msDS-ClaimIsSingleValued", + ["MsDSClaimIsValueSpaceRestricted"] = "msDS-ClaimIsValueSpaceRestricted", + ["MsDSClaimPossibleValues"] = "msDS-ClaimPossibleValues", + ["MsDSClaimSharesPossibleValuesWithBL"] = "msDS-ClaimSharesPossibleValuesWithBL", + ["MsDSClaimSource"] = "msDS-ClaimSource", + ["MsDSClaimSourceType"] = "msDS-ClaimSourceType", + ["MsDSClaimTypeAppliesToClass"] = "msDS-ClaimTypeAppliesToClass", + ["MsDSClaimValueType"] = "msDS-ClaimValueType", + ["MSDShasFullReplicaNCs"] = "msDS-hasFullReplicaNCs", + ["MSDShasMasterNCs"] = "msDS-hasMasterNCs", + ["MSDSIsPossibleValuesPresent"] = "msDS-IsPossibleValuesPresent", + ["MsDSIsUsedAsResourceSecurityAttribute"] = "msDS-IsUsedAsResourceSecurityAttribute", + ["MSDSIsUserCachableAtRodc"] = "msDS-IsUserCachableAtRodc", + ["MsDSMembersOfResourcePropertyListBL"] = "msDS-MembersOfResourcePropertyListBL", + ["MSDSPortLDAP"] = "msDS-PortLDAP", + ["MsDSSClaimSharesPossibleValuesWith"] = "msDS-ClaimSharesPossibleValuesWith", + ["MsDSURI"] = "msDS-URI", + ["MSDSUserAccountControlComputed"] = "msDS-User-Account-Control-Computed", + ["MsDSValueTypeReference"] = "msDS-ValueTypeReference", + ["MSDSValueTypeReferenceBL"] = "msDS-ValueTypeReferenceBL", + ["Name"] = "name", + ["NCName"] = "nCName", + ["NETBIOSName"] = "nETBIOSName", + ["NTMixedDomainMode"] = "ntMixedDomain", + ["NTSecurityDescriptor"] = "nTSecurityDescriptor", + ["ObjectCategory"] = "objectCategory", + ["ObjectClass"] = "objectClass", + ["ObjectGUID"] = "objectGUID", + ["ObjectSid"] = "objectSid", + ["Office"] = "physicalDeliveryOfficeName", + ["OfficePhone"] = "telephoneNumber", + ["Options"] = "Options", + ["OpType"] = "OpType", + ["Organization"] = "o", + ["OS"] = "operatingSystem", + ["OSHotfix"] = "operatingSystemHotfix", + ["OSServicePack"] = "operatingSystemServicePack", + ["OSVersion"] = "operatingSystemVersion", + ["OtherName"] = "middleName", + ["OutgoingTrust"] = "msDS-TDOIngressBL", + ["PartiallyReplicatedNamingContexts"] = "hasPartialReplicaNCs", + ["PasswordHistoryCount"] = "msDS-PasswordHistoryLength", + ["PasswordLastSet"] = "pwdLastSet", + ["POBox"] = "postOfficeBox", + ["PostalCode"] = "postalCode", + ["Precedence"] = "msDS-PasswordSettingsPrecedence", + ["PrimaryGroup"] = "primaryGroupID", + ["ProfilePath"] = "profilePath", + ["PszAsyncIntersiteTransportDN"] = "pszAsyncIntersiteTransportDN", + ["PszAttributeName"] = "pszAttributeName", + ["PszDsaAddress"] = "pszDsaAddress", + ["PszDsaDN"] = "pszDsaDN", + ["PszLastOriginatingDsaDN"] = "pszLastOriginatingDsaDN", + ["PszNamingContext"] = "pszNamingContext", + ["PszObjectDn"] = "pszObjectDn", + ["PszSourceDsaAddress"] = "pszSourceDsaAddress", + ["PszSourceDsaDN"] = "pszSourceDsaDN", + ["PublicKeyRequiredPasswordRolling"] = "msDS-ExpirePasswordsOnSmartCardOnlyAccounts", + ["ReplicateFromDirectoryServer"] = "fromServer", + ["ReplicateSingleObject"] = "replicateSingleObject", + ["ReplicationAttributeMetadata"] = "msDS-ReplAttributeMetaData", + ["ReplicationAttributeMetadataObjectType"] = "DS_REPL_ATTR_META_DATA", + ["ReplicationAttributeValueMetadata"] = "msDS-ReplValueMetaData", + ["ReplicationAttributeValueMetadataObjectType"] = "DS_REPL_VALUE_META_DATA", + ["ReplicationConnectionFailures"] = "msDS-ReplConnectionFailures", + ["ReplicationFailuresObjectType"] = "DS_REPL_KCC_DSA_FAILURE", + ["ReplicationFrequency"] = "replInterval", + ["ReplicationInboundPartners"] = "msDS-NCReplInboundNeighbors", + ["ReplicationLinkFailures"] = "msDS-ReplLinkFailures", + ["ReplicationOutboundPartners"] = "msDS-NCReplOutboundNeighbors", + ["ReplicationPartnersObjectType"] = "DS_REPL_NEIGHBOR", + ["ReplicationQueue"] = "msDS-ReplPendingOps", + ["ReplicationQueueObjectType"] = "DS_REPL_OP", + ["ReplicationSchedule"] = "schedule", + ["ReplicationUpToDatenessVector"] = "msDS-NCReplCursors", + ["ReplicationUpToDatenessVectorObjectType"] = "DS_REPL_CURSOR", + ["RequiredDomainMode"] = "msDS-RequiredDomainBehaviorVersion", + ["RequiredForestMode"] = "msDS-RequiredForestBehaviorVersion", + ["ResultantPSO"] = "msDS-ResultantPSO", + ["RevealedAccounts"] = "msDS-RevealedList", + ["ReversibleEncryptionEnabled"] = "msDS-PasswordReversibleEncryptionEnabled", + ["RollingNTLMSecret"] = "msDS-StrongNTLMPolicy", + ["Rule"] = "msDS-TransformationRules", + ["SamAccountName"] = "sAMAccountName", + ["ScriptPath"] = "scriptPath", + ["SDRightsEffective"] = "sdRightsEffective", + ["SearchFlags"] = "searchFlags", + ["ServerReference"] = "serverReference", + ["ServerReferenceBL"] = "serverReferenceBL", + ["ServiceAccount"] = "msDS-HostServiceAccount", + ["ServiceAllowedNTLMNetworkAuthentication"] = "msDS-ServiceAllowedNTLMNetworkAuthentication", + ["ServiceAllowedToAuthenticateFrom"] = "msDS-ServiceAllowedToAuthenticateFrom", + ["ServiceAllowedToAuthenticateTo"] = "msDS-ServiceAllowedToAuthenticateTo", + ["ServiceAuthenticationPolicy"] = "msDS-ServiceAuthNPolicy", + ["ServiceAuthNPolicyBL"] = "msDS-ServiceAuthNPolicyBL", + ["ServicePrincipalNames"] = "servicePrincipalName", + ["ServiceTGTLifetime"] = "msDS-ServiceTGTLifetime", + ["Site"] = "siteObject", + ["SiteLinksIncluded"] = "siteLinkList", + ["SitesIncluded"] = "siteList", + ["SourceXmlAttribute"] = "sourceXmlAttribute", + ["SPNSuffixes"] = "msDS-SPNSuffixes", + ["State"] = "st", + ["Street"] = "street", + ["StreetAddress"] = "streetAddress", + ["Subnet"] = "siteObjectBL", + ["SupportedEncryptionTypes"] = "msDS-SupportedEncryptionTypes", + ["Surname"] = "sn", + ["SystemFlags"] = "systemFlags", + ["Target"] = "trustPartner", + ["Title"] = "title", + ["TransportType"] = "transportType", + ["TrustAttributes"] = "trustAttributes", + ["TrustDirection"] = "trustDirection", + ["TrustedPolicy"] = "msDS-EgressClaimsTransformationPolicy", + ["TrustingPolicy"] = "msDS-IngressClaimsTransformationPolicy", + ["TrustType"] = "trustType", + ["UlOptions"] = "ulOptions", + ["UlPriority"] = "ulPriority", + ["UlSerialNumber"] = "ulSerialNumber", + ["UnicodePwd"] = "unicodePwd", + ["UniversalGroupCachingRefreshSite"] = "msDS-Preferred-GC-Site", + ["UPNSuffixes"] = "uPNSuffixes", + ["UserAccountControl"] = "userAccountControl", + ["UserAccountControlComputed"] = "msDS-User-Account-Control-Computed", + ["UserAllowedNTLMNetworkAuthentication"] = "msDS-UserAllowedNTLMNetworkAuthentication", + ["UserAllowedToAuthenticateFrom"] = "msDS-UserAllowedToAuthenticateFrom", + ["UserAllowedToAuthenticateTo"] = "msDS-UserAllowedToAuthenticateTo", + ["UserAuthenticationPolicy"] = "msDS-UserAuthNPolicy", + ["UserAuthNPolicyBL"] = "msDS-UserAuthNPolicyBL", + ["UserPrincipalName"] = "userPrincipalName", + ["UserTGTLifetime"] = "msDS-UserTGTLifetime", + ["UsnAttributeFilter"] = "usnAttributeFilter", + ["UsnLastObjChangeSynced"] = "usnLastObjChangeSynced", + ["UsnLocalChange"] = "usnLocalChange", + ["UsnOriginatingChange"] = "usnOriginatingChange", + ["UuidAsyncIntersiteTransportObjGuid"] = "uuidAsyncIntersiteTransportObjGuid", + ["UuidDsaObjGuid"] = "uuidDsaObjGuid", + ["UuidLastOriginatingDsaInvocationID"] = "uuidLastOriginatingDsaInvocationID", + ["UuidSourceDsaInvocationID"] = "uuidSourceDsaInvocationID", + ["UuidSourceDsaObjGuid"] = "uuidSourceDsaObjGuid", + }; + + Instance = new ReadOnlyDictionary(map); + } } diff --git a/src/PSADTree/PSADTree.csproj b/src/PSADTree/PSADTree.csproj index 4866383..0bf99c4 100644 --- a/src/PSADTree/PSADTree.csproj +++ b/src/PSADTree/PSADTree.csproj @@ -1,12 +1,12 @@ - net6.0;net472 + net8.0-windows;net472 enable true PSADTree latest - CA1416 + CA1416;NU1605 @@ -14,17 +14,21 @@ - - + + PrivateAssets="all" /> --> + + + + - + diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index 3a4855e..434c49f 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -75,7 +75,7 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable public string[] Properties { get => _properties ??= []; - set => _properties = [.. _properties.Where(e => !string.IsNullOrWhiteSpace(e))]; + set => _properties = [.. value.Where(e => !string.IsNullOrWhiteSpace(e))]; } protected override void BeginProcessing() From c87f2f8dc5f42bac7a974fee2116b0aec2bcbef4 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sat, 24 Jan 2026 15:12:57 -0300 Subject: [PATCH 06/14] ... --- src/PSADTree/Extensions/MiscExtensions.cs | 22 +++++-------------- src/PSADTree/Internal/_FormattingInternals.cs | 4 ++-- src/PSADTree/TreeComputer.cs | 20 ++++++++++++++--- src/PSADTree/TreeGroup.cs | 13 ----------- src/PSADTree/TreeUser.cs | 17 +++++++------- 5 files changed, 33 insertions(+), 43 deletions(-) diff --git a/src/PSADTree/Extensions/MiscExtensions.cs b/src/PSADTree/Extensions/MiscExtensions.cs index 7a26d1d..00685fe 100644 --- a/src/PSADTree/Extensions/MiscExtensions.cs +++ b/src/PSADTree/Extensions/MiscExtensions.cs @@ -14,22 +14,6 @@ namespace PSADTree.Extensions; internal static class MiscExtensions { - internal static T GetProperty( - this DirectoryEntry entry, - string property) - => LanguagePrimitives.ConvertTo(entry.Properties[property][0]); - - internal static bool TryGetProperty( - this SearchResult search, - string property, - [NotNullWhen(true)] out T? value) - { - value = default; - ResultPropertyValueCollection? toConvert = search.Properties[property]; - return toConvert is not null and { Count: > 0 } - && LanguagePrimitives.TryConvertTo(toConvert, out value); - } - internal static DirectoryEntry GetDirectoryEntry(this Principal principal) => (DirectoryEntry)principal.GetUnderlyingObject(); @@ -149,4 +133,10 @@ private static bool IsIAdsLargeInteger( private static bool IsSecurityDescriptor(string ldapDn) => ldapDn.Equals("nTSecurityDescriptor", StringComparison.OrdinalIgnoreCase); + + internal static UserAccountControl? GetUserAccountControl(this AuthenticablePrincipal principal) + { + DirectoryEntry entry = principal.GetDirectoryEntry(); + return (UserAccountControl?)entry.Properties["userAccountControl"]?.Value; + } } diff --git a/src/PSADTree/Internal/_FormattingInternals.cs b/src/PSADTree/Internal/_FormattingInternals.cs index 4cb3171..b14a36e 100644 --- a/src/PSADTree/Internal/_FormattingInternals.cs +++ b/src/PSADTree/Internal/_FormattingInternals.cs @@ -9,6 +9,6 @@ namespace PSADTree.Internal; public static class _FormattingInternals { [Hidden, EditorBrowsable(EditorBrowsableState.Never)] - public static string GetSource(TreeObjectBase treeObject) => - treeObject.Source; + public static string GetSource(TreeObjectBase treeObject) + => treeObject.Source; } diff --git a/src/PSADTree/TreeComputer.cs b/src/PSADTree/TreeComputer.cs index 09bf35d..282c77b 100644 --- a/src/PSADTree/TreeComputer.cs +++ b/src/PSADTree/TreeComputer.cs @@ -1,16 +1,24 @@ using System.DirectoryServices.AccountManagement; +using PSADTree.Extensions; namespace PSADTree; public sealed class TreeComputer : TreeObjectBase { + public UserAccountControl? UserAccountControl { get; } + + public bool? Enabled { get; } + private TreeComputer( TreeComputer computer, TreeGroup parent, string source, int depth) : base(computer, parent, source, depth) - { } + { + UserAccountControl = computer.UserAccountControl; + Enabled = computer.Enabled; + } internal TreeComputer( string source, @@ -19,11 +27,17 @@ internal TreeComputer( string[] properties, int depth) : base(source, parent, computer, properties, depth) - { } + { + UserAccountControl = computer.GetUserAccountControl(); + Enabled = !UserAccountControl?.HasFlag(PSADTree.UserAccountControl.ACCOUNTDISABLE); + } internal TreeComputer(string source, ComputerPrincipal computer, string[] properties) : base(source, computer, properties) - { } + { + UserAccountControl = computer.GetUserAccountControl(); + Enabled = !UserAccountControl?.HasFlag(PSADTree.UserAccountControl.ACCOUNTDISABLE); + } internal override TreeObjectBase Clone(TreeGroup parent, string source, int depth) => new TreeComputer(this, parent, source, depth); diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index dd11366..0f60d6c 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -2,7 +2,6 @@ using System.Collections.ObjectModel; using System.DirectoryServices; using System.DirectoryServices.AccountManagement; -using PSADTree.Extensions; namespace PSADTree; @@ -20,10 +19,6 @@ public sealed class TreeGroup : TreeObjectBase private List _children; - public UserAccountControl UserAccountControl { get; private set; } - - public bool Enabled { get; private set; } - public ReadOnlyCollection Children => new(_children); public bool IsCircular { get; private set; } @@ -35,8 +30,6 @@ private TreeGroup( int depth) : base(group, parent, source, depth) { - UserAccountControl = group.UserAccountControl; - Enabled = group.Enabled; _children = group._children; IsCircular = group.IsCircular; } @@ -93,12 +86,6 @@ internal void LinkCachedChildren(TreeCache cache) internal void AddChild(TreeObjectBase child) => _children.Add(child); - internal void SetUserAccountControl(DirectoryEntry entry) - { - UserAccountControl = entry.GetProperty("userAccountControl"); - Enabled = !UserAccountControl.HasFlag(UserAccountControl.ACCOUNTDISABLE); - } - internal override TreeObjectBase Clone(TreeGroup parent, string source, int depth) => new TreeGroup(this, parent, source, depth); } diff --git a/src/PSADTree/TreeUser.cs b/src/PSADTree/TreeUser.cs index be56563..1293cc4 100644 --- a/src/PSADTree/TreeUser.cs +++ b/src/PSADTree/TreeUser.cs @@ -1,4 +1,3 @@ -using System.DirectoryServices; using System.DirectoryServices.AccountManagement; using PSADTree.Extensions; @@ -6,9 +5,9 @@ namespace PSADTree; public sealed class TreeUser : TreeObjectBase { - public UserAccountControl UserAccountControl { get; private set; } + public UserAccountControl? UserAccountControl { get; } - public bool Enabled { get; private set; } + public bool? Enabled { get; private set; } private TreeUser( TreeUser user, @@ -28,16 +27,16 @@ internal TreeUser( string[] properties, int depth) : base(source, parent, user, properties, depth) - { } + { + UserAccountControl = user.GetUserAccountControl(); + Enabled = !UserAccountControl?.HasFlag(PSADTree.UserAccountControl.ACCOUNTDISABLE); + } internal TreeUser(string source, UserPrincipal user, string[] properties) : base(source, user, properties) - { } - - internal void SetUserAccountControl(DirectoryEntry entry) { - UserAccountControl = entry.GetProperty("userAccountControl"); - Enabled = !UserAccountControl.HasFlag(UserAccountControl.ACCOUNTDISABLE); + UserAccountControl = user.GetUserAccountControl(); + Enabled = !UserAccountControl?.HasFlag(PSADTree.UserAccountControl.ACCOUNTDISABLE); } internal override TreeObjectBase Clone(TreeGroup parent, string source, int depth) From d4097e027dd7cdb8f5631d00c1d86d96282606b2 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sat, 24 Jan 2026 10:44:47 -0800 Subject: [PATCH 07/14] almost done --- .../Commands/GetADTreeGroupMemberCommand.cs | 5 +- ...etADTreePrincipalGroupMembershipCommand.cs | 5 +- src/PSADTree/Extensions/MiscExtensions.cs | 7 ++- src/PSADTree/TreeComputer.cs | 4 +- src/PSADTree/TreeGroup.cs | 1 - src/PSADTree/TreeUser.cs | 6 +-- src/PSADTree/UserAccountControl.cs | 47 ++++++++++--------- 7 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index 2253aa2..17e4132 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -10,10 +10,7 @@ namespace PSADTree.Commands; VerbsCommon.Get, "ADTreeGroupMember", DefaultParameterSetName = DepthParameterSet)] [Alias("treegroupmember")] -[OutputType( - typeof(TreeGroup), - typeof(TreeUser), - typeof(TreeComputer))] +[OutputType(typeof(TreeGroup), typeof(TreeUser), typeof(TreeComputer))] public sealed class GetADTreeGroupMemberCommand : PSADTreeCmdletBase { [Parameter] diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index a1faed9..0236e35 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -10,10 +10,7 @@ namespace PSADTree.Commands; VerbsCommon.Get, "ADTreePrincipalGroupMembership", DefaultParameterSetName = DepthParameterSet)] [Alias("treeprincipalmembership")] -[OutputType( - typeof(TreeGroup), - typeof(TreeUser), - typeof(TreeComputer))] +[OutputType(typeof(TreeGroup), typeof(TreeUser), typeof(TreeComputer))] public sealed class GetADTreePrincipalGroupMembershipCommand : PSADTreeCmdletBase { protected override Principal GetFirstPrincipal() => Principal.FindByIdentity(Context, Identity); diff --git a/src/PSADTree/Extensions/MiscExtensions.cs b/src/PSADTree/Extensions/MiscExtensions.cs index 00685fe..43b5689 100644 --- a/src/PSADTree/Extensions/MiscExtensions.cs +++ b/src/PSADTree/Extensions/MiscExtensions.cs @@ -137,6 +137,11 @@ private static bool IsSecurityDescriptor(string ldapDn) internal static UserAccountControl? GetUserAccountControl(this AuthenticablePrincipal principal) { DirectoryEntry entry = principal.GetDirectoryEntry(); - return (UserAccountControl?)entry.Properties["userAccountControl"]?.Value; + object? uac = entry.Properties["userAccountControl"]?.Value; + if (uac is null) return null; + return (UserAccountControl)Convert.ToUInt32(uac); } + + internal static bool IsEnabled(this UserAccountControl uac) + => !uac.HasFlag(UserAccountControl.ACCOUNTDISABLE); } diff --git a/src/PSADTree/TreeComputer.cs b/src/PSADTree/TreeComputer.cs index 282c77b..0c128df 100644 --- a/src/PSADTree/TreeComputer.cs +++ b/src/PSADTree/TreeComputer.cs @@ -29,14 +29,14 @@ internal TreeComputer( : base(source, parent, computer, properties, depth) { UserAccountControl = computer.GetUserAccountControl(); - Enabled = !UserAccountControl?.HasFlag(PSADTree.UserAccountControl.ACCOUNTDISABLE); + Enabled = UserAccountControl?.IsEnabled(); } internal TreeComputer(string source, ComputerPrincipal computer, string[] properties) : base(source, computer, properties) { UserAccountControl = computer.GetUserAccountControl(); - Enabled = !UserAccountControl?.HasFlag(PSADTree.UserAccountControl.ACCOUNTDISABLE); + Enabled = UserAccountControl?.IsEnabled(); } internal override TreeObjectBase Clone(TreeGroup parent, string source, int depth) diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index 0f60d6c..27cce7a 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Collections.ObjectModel; -using System.DirectoryServices; using System.DirectoryServices.AccountManagement; namespace PSADTree; diff --git a/src/PSADTree/TreeUser.cs b/src/PSADTree/TreeUser.cs index 1293cc4..19d063c 100644 --- a/src/PSADTree/TreeUser.cs +++ b/src/PSADTree/TreeUser.cs @@ -7,7 +7,7 @@ public sealed class TreeUser : TreeObjectBase { public UserAccountControl? UserAccountControl { get; } - public bool? Enabled { get; private set; } + public bool? Enabled { get; } private TreeUser( TreeUser user, @@ -29,14 +29,14 @@ internal TreeUser( : base(source, parent, user, properties, depth) { UserAccountControl = user.GetUserAccountControl(); - Enabled = !UserAccountControl?.HasFlag(PSADTree.UserAccountControl.ACCOUNTDISABLE); + Enabled = UserAccountControl?.IsEnabled(); } internal TreeUser(string source, UserPrincipal user, string[] properties) : base(source, user, properties) { UserAccountControl = user.GetUserAccountControl(); - Enabled = !UserAccountControl?.HasFlag(PSADTree.UserAccountControl.ACCOUNTDISABLE); + Enabled = UserAccountControl?.IsEnabled(); } internal override TreeObjectBase Clone(TreeGroup parent, string source, int depth) diff --git a/src/PSADTree/UserAccountControl.cs b/src/PSADTree/UserAccountControl.cs index 28aa99f..ccfbcf1 100644 --- a/src/PSADTree/UserAccountControl.cs +++ b/src/PSADTree/UserAccountControl.cs @@ -5,27 +5,28 @@ namespace PSADTree; [Flags] public enum UserAccountControl : uint { - None = 0, - SCRIPT = 1, - ACCOUNTDISABLE = 2, - HOMEDIR_REQUIRED = 8, - LOCKOUT = 16, - PASSWD_NOTREQD = 32, - PASSWD_CANT_CHANGE = 64, - ENCRYPTED_TEXT_PWD_ALLOWED = 128, - TEMP_DUPLICATE_ACCOUNT = 256, - NORMAL_ACCOUNT = 512, - INTERDOMAIN_TRUST_ACCOUNT = 2048, - WORKSTATION_TRUST_ACCOUNT = 4096, - SERVER_TRUST_ACCOUNT = 8192, - DONT_EXPIRE_PASSWORD = 65536, - MNS_LOGON_ACCOUNT = 131072, - SMARTCARD_REQUIRED = 262144, - TRUSTED_FOR_DELEGATION = 524288, - NOT_DELEGATED = 1048576, - USE_DES_KEY_ONLY = 2097152, - DONT_REQ_PREAUTH = 4194304, - PASSWORD_EXPIRED = 8388608, - TRUSTED_TO_AUTH_FOR_DELEGATION = 16777216, - PARTIAL_SECRETS_ACCOUNT = 67108864 + None = 0, + SCRIPT = 0x00000001, // 1 + ACCOUNTDISABLE = 0x00000002, // 2 + // HOMEDIR_REQUIRED = 0x00000008, // Obsolete / ignored since ~2000 + LOCKOUT = 0x00000010, // 16 + PASSWD_NOTREQD = 0x00000020, // 32 + PASSWD_CANT_CHANGE = 0x00000040, // 64 (not stored, computed) + ENCRYPTED_TEXT_PWD_ALLOWED = 0x00000080, // 128 + TEMP_DUPLICATE_ACCOUNT = 0x00000100, // 256 + NORMAL_ACCOUNT = 0x00000200, // 512 + INTERDOMAIN_TRUST_ACCOUNT = 0x00000800, // 2048 + WORKSTATION_TRUST_ACCOUNT = 0x00001000, // 4096 + SERVER_TRUST_ACCOUNT = 0x00002000, // 8192 + DONT_EXPIRE_PASSWORD = 0x00010000, // 65536 + MNS_LOGON_ACCOUNT = 0x00020000, // 131072 + SMARTCARD_REQUIRED = 0x00040000, // 262144 + TRUSTED_FOR_DELEGATION = 0x00080000, // 524288 + NOT_DELEGATED = 0x00100000, // 1048576 + USE_DES_KEY_ONLY = 0x00200000, // 2097152 + DONT_REQ_PREAUTH = 0x00400000, // 4194304 + PASSWORD_EXPIRED = 0x00800000, // 8388608 (computed) + TRUSTED_TO_AUTH_FOR_DELEGATION = 0x01000000, // 16777216 + PARTIAL_SECRETS_ACCOUNT = 0x04000000, // 67108864 (RODC) + USE_AES_KEYS = 0x80000000U, // 2147483648 (AES Kerberos support) } From 4ba53fe3650dc9d51fffc0a2c7f94a667bee72d4 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sun, 25 Jan 2026 10:37:07 -0800 Subject: [PATCH 08/14] now its done --- module/PSADTree.psd1 | 6 +++--- src/PSADTree/Extensions/MiscExtensions.cs | 2 +- src/PSADTree/PSADTree.csproj | 13 +++---------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/module/PSADTree.psd1 b/module/PSADTree.psd1 index 422dc16..557e3a7 100644 --- a/module/PSADTree.psd1 +++ b/module/PSADTree.psd1 @@ -55,7 +55,7 @@ # ProcessorArchitecture = '' # Modules that must be imported into the global environment prior to importing this module - # RequiredModules = @() + # RequiredModules = @() # Assemblies that must be loaded prior to importing this module RequiredAssemblies = @('System.DirectoryServices.AccountManagement') @@ -134,14 +134,14 @@ # RequireLicenseAcceptance = $false # External dependent modules of this module - # ExternalModuleDependencies = @() + # ExternalModuleDependencies = @('Microsoft.PowerShell.Security') } # End of PSData hashtable } # End of PrivateData hashtable # HelpInfo URI of this module - # HelpInfoURI = '' + HelpInfoURI = 'https://github.com/santisq/PSADTree/blob/main/docs/en-US/PSADTree.md' # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. # DefaultCommandPrefix = '' diff --git a/src/PSADTree/Extensions/MiscExtensions.cs b/src/PSADTree/Extensions/MiscExtensions.cs index 43b5689..213849d 100644 --- a/src/PSADTree/Extensions/MiscExtensions.cs +++ b/src/PSADTree/Extensions/MiscExtensions.cs @@ -106,7 +106,7 @@ private static PSObject GetAcl(this DirectoryEntry entry) private static PSObject AddProperty(this PSObject pSObject, string name, object? value) { - pSObject.Properties.Add(new PSNoteProperty(name, value)); + pSObject.Members.Add(new PSNoteProperty(name, value), preValidated: true); return pSObject; } diff --git a/src/PSADTree/PSADTree.csproj b/src/PSADTree/PSADTree.csproj index 0bf99c4..c583673 100644 --- a/src/PSADTree/PSADTree.csproj +++ b/src/PSADTree/PSADTree.csproj @@ -6,7 +6,6 @@ true PSADTree latest - CA1416;NU1605 @@ -14,21 +13,15 @@ - - - + + - + From b2db5f705e22daca61df17b3a20c820821dba96b Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sun, 25 Jan 2026 13:22:29 -0800 Subject: [PATCH 09/14] now its done --- src/PSADTree/Extensions/MiscExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PSADTree/Extensions/MiscExtensions.cs b/src/PSADTree/Extensions/MiscExtensions.cs index 213849d..68bd3cd 100644 --- a/src/PSADTree/Extensions/MiscExtensions.cs +++ b/src/PSADTree/Extensions/MiscExtensions.cs @@ -106,7 +106,7 @@ private static PSObject GetAcl(this DirectoryEntry entry) private static PSObject AddProperty(this PSObject pSObject, string name, object? value) { - pSObject.Members.Add(new PSNoteProperty(name, value), preValidated: true); + pSObject.Properties.Add(new PSNoteProperty(name, value), preValidated: true); return pSObject; } From 1dba093e131d074063a135f81b057648b448cffc Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sun, 25 Jan 2026 15:39:37 -0800 Subject: [PATCH 10/14] changing noteproperties for codeproperties --- src/PSADTree/Extensions/MiscExtensions.cs | 42 ++-------- .../Internal/_SecurityDescriptorInternals.cs | 82 +++++++++++++++++++ 2 files changed, 87 insertions(+), 37 deletions(-) create mode 100644 src/PSADTree/Internal/_SecurityDescriptorInternals.cs diff --git a/src/PSADTree/Extensions/MiscExtensions.cs b/src/PSADTree/Extensions/MiscExtensions.cs index 68bd3cd..636394d 100644 --- a/src/PSADTree/Extensions/MiscExtensions.cs +++ b/src/PSADTree/Extensions/MiscExtensions.cs @@ -5,10 +5,7 @@ using System.DirectoryServices; using System.DirectoryServices.AccountManagement; using System.Linq; -using System.Management.Automation; -using System.Security.AccessControl; -using System.Security.Principal; -using System.Text; +using PSADTree.Internal; namespace PSADTree.Extensions; @@ -21,11 +18,11 @@ internal static DirectoryEntry GetDirectoryEntry(this Principal principal) this Principal principal, string[] properties) { - DirectoryEntry entry = principal.GetDirectoryEntry(); - if (properties.Length == 0) return null; + DirectoryEntry entry = principal.GetDirectoryEntry(); + if (properties.Any(e => e == "*")) return entry.GetAllAttributes(); @@ -42,7 +39,7 @@ internal static DirectoryEntry GetDirectoryEntry(this Principal principal) if (IsSecurityDescriptor(property)) { - additionalProperties[property] = entry.GetAcl(); + additionalProperties[property] = entry.GetSecurityDescriptorAsPSObject(); continue; } @@ -71,7 +68,7 @@ internal static DirectoryEntry GetDirectoryEntry(this Principal principal) { if (IsSecurityDescriptor(property)) { - additionalProperties[property] = entry.GetAcl(); + additionalProperties[property] = entry.GetSecurityDescriptorAsPSObject(); continue; } @@ -90,35 +87,6 @@ internal static DirectoryEntry GetDirectoryEntry(this Principal principal) return new(additionalProperties); } - private static PSObject GetAcl(this DirectoryEntry entry) - { - Type target = typeof(NTAccount); - ActiveDirectorySecurity acl = entry.ObjectSecurity; - AuthorizationRuleCollection rules = acl.GetAccessRules(true, true, target); - return PSObject.AsPSObject(acl) - .AddProperty("Path", entry.Path) - .AddProperty("Owner", acl.GetOwner(target)) - .AddProperty("Group", acl.GetGroup(target)) - .AddProperty("Sddl", acl.GetSecurityDescriptorSddlForm(AccessControlSections.All)) - .AddProperty("Access", rules) - .AddProperty("AccessToString", rules.GetAccessToString()); - } - - private static PSObject AddProperty(this PSObject pSObject, string name, object? value) - { - pSObject.Properties.Add(new PSNoteProperty(name, value), preValidated: true); - return pSObject; - } - - private static string GetAccessToString(this AuthorizationRuleCollection rules) - { - StringBuilder builder = new(); - foreach (ActiveDirectoryAccessRule rule in rules) - builder.AppendLine($"{rule.IdentityReference} {rule.AccessControlType}"); - - return builder.ToString(); - } - private static bool IsIAdsLargeInteger( object value, [NotNullWhen(true)] out long? fileTime) diff --git a/src/PSADTree/Internal/_SecurityDescriptorInternals.cs b/src/PSADTree/Internal/_SecurityDescriptorInternals.cs new file mode 100644 index 0000000..9a2e3cf --- /dev/null +++ b/src/PSADTree/Internal/_SecurityDescriptorInternals.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.DirectoryServices; +using System.Linq; +using System.Management.Automation; +using System.Reflection; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Text; + +namespace PSADTree.Internal; + +#pragma warning disable IDE1006 + +[EditorBrowsable(EditorBrowsableState.Never)] +public static class _SecurityDescriptorInternals +{ + private readonly static Type _target = typeof(NTAccount); + + private readonly static ReadOnlyDictionary _propertyGetters; + + static _SecurityDescriptorInternals() + { + _propertyGetters = new(typeof(_SecurityDescriptorInternals) + .GetMethods(BindingFlags.Public | BindingFlags.Static) + .ToDictionary(prop => prop.Name, prop => prop)); + } + + private static ActiveDirectorySecurity GetBaseObject(PSObject target) + => (ActiveDirectorySecurity)target.BaseObject; + + public static IdentityReference? GetOwner(PSObject target) + => GetBaseObject(target).GetOwner(_target); + + public static IdentityReference? GetGroup(PSObject target) + => GetBaseObject(target).GetGroup(_target); + + public static string GetSddlForm(PSObject target) + => GetBaseObject(target).GetSecurityDescriptorSddlForm(AccessControlSections.All); + + public static AuthorizationRuleCollection GetAccessRules(PSObject target) + => GetBaseObject(target).GetAccessRules(true, true, _target); + + public static string GetAccessToString(PSObject target) + { + StringBuilder builder = new(); + foreach (ActiveDirectoryAccessRule rule in GetAccessRules(target)) + builder.AppendLine($"{rule.IdentityReference} {rule.AccessControlType}"); + + return builder.ToString(); + } + + internal static PSObject GetSecurityDescriptorAsPSObject(this DirectoryEntry entry) + { + return PSObject.AsPSObject(entry.ObjectSecurity) + .AddProperty("Path", entry.Path) + .AddCodeProperty("Owner", _propertyGetters["GetOwner"]) + .AddCodeProperty("Group", _propertyGetters["GetGroup"]) + .AddCodeProperty("Sddl", _propertyGetters["GetSddlForm"]) + .AddCodeProperty("Access", _propertyGetters["GetAccessRules"]) + .AddCodeProperty("AccessToString", _propertyGetters["GetAccessToString"]); + } + + private static PSObject AddProperty( + this PSObject psObject, + string name, + object? value) + { + psObject.Properties.Add(new PSNoteProperty(name, value), preValidated: true); + return psObject; + } + + private static PSObject AddCodeProperty( + this PSObject psObject, + string name, + MethodInfo method) + { + psObject.Properties.Add(new PSCodeProperty(name, method), preValidated: true); + return psObject; + } +} From 84d8c19c2dfc6976a630ef8082debbc9b8f9c378 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Mon, 26 Jan 2026 04:34:07 -0800 Subject: [PATCH 11/14] missing docs --- module/PSADTree.Format.ps1xml | 2 +- src/PSADTree/Internal/_FormattingInternals.cs | 11 ++++-- .../Internal/_SecurityDescriptorInternals.cs | 34 ++++++++++--------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/module/PSADTree.Format.ps1xml b/module/PSADTree.Format.ps1xml index 22aa893..1c3a2af 100644 --- a/module/PSADTree.Format.ps1xml +++ b/module/PSADTree.Format.ps1xml @@ -29,7 +29,7 @@ - $_.Domain -replace '^DC=|(?<!\\),.+' + [PSADTree.Internal._FormattingInternals]::GetDomain($_) ObjectClass diff --git a/src/PSADTree/Internal/_FormattingInternals.cs b/src/PSADTree/Internal/_FormattingInternals.cs index b14a36e..75af3b5 100644 --- a/src/PSADTree/Internal/_FormattingInternals.cs +++ b/src/PSADTree/Internal/_FormattingInternals.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.Management.Automation; +using System.Text.RegularExpressions; namespace PSADTree.Internal; @@ -8,7 +9,13 @@ namespace PSADTree.Internal; [EditorBrowsable(EditorBrowsableState.Never)] public static class _FormattingInternals { + private static Regex s_getDomain = new( + @"^DC=|(? treeObject.Source; + [Hidden, EditorBrowsable(EditorBrowsableState.Never)] - public static string GetSource(TreeObjectBase treeObject) - => treeObject.Source; + public static string GetDomain(TreeObjectBase treeObject) => s_getDomain.Replace(treeObject.Domain, string.Empty); } diff --git a/src/PSADTree/Internal/_SecurityDescriptorInternals.cs b/src/PSADTree/Internal/_SecurityDescriptorInternals.cs index 9a2e3cf..b10a3a5 100644 --- a/src/PSADTree/Internal/_SecurityDescriptorInternals.cs +++ b/src/PSADTree/Internal/_SecurityDescriptorInternals.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.ObjectModel; using System.ComponentModel; using System.DirectoryServices; -using System.Linq; using System.Management.Automation; using System.Reflection; using System.Security.AccessControl; @@ -17,14 +15,20 @@ namespace PSADTree.Internal; public static class _SecurityDescriptorInternals { private readonly static Type _target = typeof(NTAccount); - - private readonly static ReadOnlyDictionary _propertyGetters; + private static readonly MethodInfo _getOwner; + private static readonly MethodInfo _getGroup; + private static readonly MethodInfo _getSddlForm; + private static readonly MethodInfo _getAccessRules; + private static readonly MethodInfo _getAccessToString; static _SecurityDescriptorInternals() { - _propertyGetters = new(typeof(_SecurityDescriptorInternals) - .GetMethods(BindingFlags.Public | BindingFlags.Static) - .ToDictionary(prop => prop.Name, prop => prop)); + Type type = typeof(_SecurityDescriptorInternals); + _getOwner = type.GetMethod(nameof(GetOwner))!; + _getGroup = type.GetMethod(nameof(GetGroup))!; + _getSddlForm = type.GetMethod(nameof(GetSddlForm))!; + _getAccessRules = type.GetMethod(nameof(GetAccessRules))!; + _getAccessToString = type.GetMethod(nameof(GetAccessToString))!; } private static ActiveDirectorySecurity GetBaseObject(PSObject target) @@ -52,15 +56,13 @@ public static string GetAccessToString(PSObject target) } internal static PSObject GetSecurityDescriptorAsPSObject(this DirectoryEntry entry) - { - return PSObject.AsPSObject(entry.ObjectSecurity) - .AddProperty("Path", entry.Path) - .AddCodeProperty("Owner", _propertyGetters["GetOwner"]) - .AddCodeProperty("Group", _propertyGetters["GetGroup"]) - .AddCodeProperty("Sddl", _propertyGetters["GetSddlForm"]) - .AddCodeProperty("Access", _propertyGetters["GetAccessRules"]) - .AddCodeProperty("AccessToString", _propertyGetters["GetAccessToString"]); - } + => PSObject.AsPSObject(entry.ObjectSecurity) + .AddProperty("Path", entry.Path) + .AddCodeProperty("Owner", _getOwner) + .AddCodeProperty("Group", _getGroup) + .AddCodeProperty("Sddl", _getSddlForm) + .AddCodeProperty("Access", _getAccessRules) + .AddCodeProperty("AccessToString", _getAccessToString); private static PSObject AddProperty( this PSObject psObject, From 845805aa31614ed74e91842d969871abc1949a0c Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Mon, 26 Jan 2026 14:06:17 -0300 Subject: [PATCH 12/14] working on docs --- docs/en-US/Get-ADTreeGroupMember.md | 65 +++++++++++++++---- .../Get-ADTreePrincipalGroupMembership.md | 65 +++++++++++++++---- .../Commands/GetADTreeGroupMemberCommand.cs | 6 +- ...etADTreePrincipalGroupMembershipCommand.cs | 6 +- src/PSADTree/Extensions/MiscExtensions.cs | 4 ++ src/PSADTree/Extensions/TreeExtensions.cs | 16 ++--- src/PSADTree/PSADTreeCmdletBase.cs | 9 ++- src/PSADTree/PSADTreeComparer.cs | 4 ++ 8 files changed, 133 insertions(+), 42 deletions(-) diff --git a/docs/en-US/Get-ADTreeGroupMember.md b/docs/en-US/Get-ADTreeGroupMember.md index 6390bf7..a1d1b8d 100644 --- a/docs/en-US/Get-ADTreeGroupMember.md +++ b/docs/en-US/Get-ADTreeGroupMember.md @@ -17,13 +17,14 @@ schema: 2.0.0 ```powershell Get-ADTreeGroupMember - [-Group] [-Identity] [-Server ] [-Credential ] [-Depth ] [-ShowAll] + [-Group] [-Exclude ] + [-Properties ] [] ``` @@ -31,13 +32,14 @@ Get-ADTreeGroupMember ```powershell Get-ADTreeGroupMember - [-Group] [-Identity] [-Server ] [-Credential ] [-Recursive] [-ShowAll] + [-Group] [-Exclude ] + [-Properties ] [] ``` @@ -68,7 +70,7 @@ PS ..\PSADTree\> Get-ADGroup -Filter * -SearchBase 'OU=myOU,DC=myDomain,DC=com' Get-ADTreeGroupMember ``` -You can pipe strings containing an identity to this cmdlet. [__`ADGroup`__](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adgroup?view=activedirectory-management-10.0) instances piped to this cmdlet are also supported. +You can pipe strings containing an identity to this cmdlet. [__`ADGroup`__](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adgroup) instances piped to this cmdlet are also supported. ### Example 4: Find any Circular Nested Groups from previous example @@ -110,7 +112,7 @@ Type a user name, such as __User01__ or __Domain01\User01__, or enter a __PSCred ```yaml Type: PSCredential Parameter Sets: (All) -Aliases: +Aliases: cred Required: False Position: Named @@ -127,7 +129,7 @@ By default, only 3 levels of recursion are included. `Get-ADTreeGroupMember` emi ```yaml Type: Int32 Parameter Sets: Depth -Aliases: +Aliases: d Required: False Position: Named @@ -150,7 +152,7 @@ Wildcard characters are accepted. ```yaml Type: String[] Parameter Sets: (All) -Aliases: +Aliases: ex Required: False Position: Named @@ -166,7 +168,7 @@ The `-Group` switch indicates that the cmdlet should display nested group member ```yaml Type: SwitchParameter Parameter Sets: (All) -Aliases: +Aliases: g Required: False Position: Named @@ -185,7 +187,7 @@ Specifies an Active Directory group by providing one of the following property v - A sAMAccountName - A UserPrincipalName -See [`IdentityType` Enum](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement.identitytype?view=dotnet-plat-ext-7.0) for more information. +See [`IdentityType` Enum](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement.identitytype) for more information. ```yaml Type: String @@ -199,6 +201,41 @@ Accept pipeline input: True (ByPropertyName, ByValue) Accept wildcard characters: False ``` +### -Properties + +Specifies one or more additional properties (LDAP attributes) to retrieve for each Active Directory object (user, group, computer, etc.) in the tree. Retrieved values are added to the read-only dictionary in the `.AdditionalProperties` property of each output object (`TreeUser`, `TreeGroup`, `TreeComputer`). + +__Behavior:__ + +- `*` → Retrieves __all__ available attributes from the object. +- One or more property names → Only properties that exist on the object and have a non-null value are included. + +__Supported input styles:__ + +- Friendly/PowerShell-style names (as in the Active Directory module), e.g., `City`, `Country`, `EmailAddress` +- Raw LDAP attribute names, e.g., `l`, `c`, `mail` + +When a friendly name is used, the key in `.AdditionalProperties` matches the friendly name (not the LDAP name). + +__Special handling:__ + +- `nTSecurityDescriptor` → Returned as a security descriptor object (similar to `Get-Acl` output) +- Large integer / FILETIME attributes (such as `pwdLastSet`, `accountExpires`, `lastLogonTimestamp`, `badPasswordTime`, etc.) → Converted to `long` (64-bit FileTime ticks) + +Non-existent properties (e.g. `Title` on a computer object) are silently ignored. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: prop, attrs, attributes + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -Recursive Specifies that the cmdlet should get all group members of the specified group. @@ -206,7 +243,7 @@ Specifies that the cmdlet should get all group members of the specified group. ```yaml Type: SwitchParameter Parameter Sets: Recursive -Aliases: +Aliases: rec Required: False Position: Named @@ -233,7 +270,7 @@ Directory server values: ```yaml Type: String Parameter Sets: (All) -Aliases: +Aliases: s, dc Required: False Position: Named @@ -256,7 +293,7 @@ This switch forces the cmdlet to display the full hierarchy including previously ```yaml Type: SwitchParameter Parameter Sets: (All) -Aliases: +Aliases: a Required: False Position: Named @@ -273,7 +310,7 @@ This cmdlet supports the common parameters. For more information, see [about_Com ### System.String -You can pipe strings containing an identity to this cmdlet. [__`ADGroup`__](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adgroup?view=activedirectory-management-10.0) instances piped to this cmdlet are also supported. +You can pipe strings containing an identity to this cmdlet. [__`ADGroup`__](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adgroup) instances piped to this cmdlet are also supported. ## OUTPUTS @@ -288,3 +325,7 @@ You can pipe strings containing an identity to this cmdlet. [__`ADGroup`__](http `treegroupmember` is the alias for this cmdlet. ## RELATED LINKS + +[__`Principal` Class__](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement.principal) + +[__`DirectoryEntry`__ Class](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.directoryentry) diff --git a/docs/en-US/Get-ADTreePrincipalGroupMembership.md b/docs/en-US/Get-ADTreePrincipalGroupMembership.md index 50d6033..d4f609a 100644 --- a/docs/en-US/Get-ADTreePrincipalGroupMembership.md +++ b/docs/en-US/Get-ADTreePrincipalGroupMembership.md @@ -23,6 +23,7 @@ Get-ADTreePrincipalGroupMembership [-Depth ] [-ShowAll] [-Exclude ] + [-Properties ] [] ``` @@ -36,6 +37,7 @@ Get-ADTreePrincipalGroupMembership [-Recursive] [-ShowAll] [-Exclude ] + [-Properties ] [] ``` @@ -66,12 +68,12 @@ PS ..\PSADTree\> Get-ADComputer -Filter * -SearchBase 'OU=myOU,DC=myDomain,DC=co Get-ADTreePrincipalGroupMembership ``` -You can pipe strings containing an identity to this cmdlet. [__`ADObject`__](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adobject?view=activedirectory-management-10.0) instances piped to this cmdlet are also supported. +You can pipe strings containing an identity to this cmdlet. [__`ADObject`__](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adobject) instances piped to this cmdlet are also supported. ### Example 4: Find any Circular Nested Groups from previous example ```powershell -PS ..\PSADTree\> Get-ADComputer -Filter * -SearchBase 'OU=myOU,DC=myDomain,DC=com' | +PS \> Get-ADComputer -Filter * -SearchBase 'OU=myOU,DC=myDomain,DC=com' | Get-ADTreePrincipalGroupMembership -Recursive | Where-Object IsCircular ``` @@ -79,13 +81,13 @@ PS ..\PSADTree\> Get-ADComputer -Filter * -SearchBase 'OU=myOU,DC=myDomain,DC=co ### Example 5: Get group memberships for a user in a different Domain ```powershell -PS ..\PSADTree\> Get-ADTreePrincipalGroupMembership john.doe -Server otherDomain +PS \> Get-ADTreePrincipalGroupMembership john.doe -Server otherDomain ``` ### Example 6: Get group memberships for a user, including processed groups ```powershell -PS ..\PSADTree\> Get-ADTreePrincipalGroupMembership john.doe -ShowAll +PS \> Get-ADTreePrincipalGroupMembership john.doe -ShowAll ``` By default, previously processed groups will be marked as _"Processed Group"_ and their hierarchy will not be displayed. @@ -106,7 +108,7 @@ Type a user name, such as __User01__ or __Domain01\User01__, or enter a __PSCred ```yaml Type: PSCredential Parameter Sets: (All) -Aliases: +Aliases: cred Required: False Position: Named @@ -123,7 +125,7 @@ By default, only 3 levels of recursion are included. `Get-ADTreePrincipalGroupMe ```yaml Type: Int32 Parameter Sets: Depth -Aliases: +Aliases: d Required: False Position: Named @@ -145,7 +147,7 @@ Wildcard characters are accepted. ```yaml Type: String[] Parameter Sets: (All) -Aliases: +Aliases: ex Required: False Position: Named @@ -164,7 +166,7 @@ Specifies an Active Directory principal by providing one of the following proper - A sAMAccountName - A UserPrincipalName -See [`IdentityType` Enum](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement.identitytype?view=dotnet-plat-ext-7.0) for more information. +See [`IdentityType` Enum](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement.identitytype) for more information. ```yaml Type: String @@ -178,6 +180,41 @@ Accept pipeline input: True (ByPropertyName, ByValue) Accept wildcard characters: False ``` +### -Properties + +Specifies one or more additional properties (LDAP attributes) to retrieve for each Active Directory object (user, group, computer, etc.) in the tree. Retrieved values are added to the read-only dictionary in the `.AdditionalProperties` property of each output object (`TreeUser`, `TreeGroup`, `TreeComputer`). + +__Behavior:__ + +- `*` → Retrieves __all__ available attributes from the object. +- One or more property names → Only properties that exist on the object and have a non-null value are included. + +__Supported input styles:__ + +- Friendly/PowerShell-style names (as in the Active Directory module), e.g., `City`, `Country`, `EmailAddress` +- Raw LDAP attribute names, e.g., `l`, `c`, `mail` + +When a friendly name is used, the key in `.AdditionalProperties` matches the friendly name (not the LDAP name). + +__Special handling:__ + +- `nTSecurityDescriptor` → Returned as a security descriptor object (similar to `Get-Acl` output) +- Large integer / FILETIME attributes (such as `pwdLastSet`, `accountExpires`, `lastLogonTimestamp`, `badPasswordTime`, etc.) → Converted to `long` (64-bit FileTime ticks) + +Non-existent properties (e.g. `Title` on a computer object) are silently ignored. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: prop, attrs, attributes + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -Recursive Specifies that the cmdlet should get all group membership of the specified principal. @@ -185,7 +222,7 @@ Specifies that the cmdlet should get all group membership of the specified princ ```yaml Type: SwitchParameter Parameter Sets: Recursive -Aliases: +Aliases: rec Required: False Position: Named @@ -212,7 +249,7 @@ Directory server values: ```yaml Type: String Parameter Sets: (All) -Aliases: +Aliases: s, dc Required: False Position: Named @@ -235,7 +272,7 @@ This switch forces the cmdlet to display the full hierarchy including previously ```yaml Type: SwitchParameter Parameter Sets: (All) -Aliases: +Aliases: a Required: False Position: Named @@ -252,7 +289,7 @@ This cmdlet supports the common parameters. For more information, see [about_Com ### System.String -You can pipe strings containing an identity to this cmdlet. [`ADObject`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adobject?view=activedirectory-management-10.0) instances piped to this cmdlet are also supported. +You can pipe strings containing an identity to this cmdlet. [`ADObject`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adobject) instances piped to this cmdlet are also supported. ## OUTPUTS @@ -267,3 +304,7 @@ You can pipe strings containing an identity to this cmdlet. [`ADObject`](https:/ `treeprincipalmembership` is the alias for this cmdlet. ## RELATED LINKS + +[__`Principal` Class__](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement.principal) + +[__`DirectoryEntry`__ Class](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.directoryentry) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index 17e4132..5b51134 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -14,6 +14,7 @@ namespace PSADTree.Commands; public sealed class GetADTreeGroupMemberCommand : PSADTreeCmdletBase { [Parameter] + [Alias("g")] public SwitchParameter Group { get; set; } protected override Principal GetFirstPrincipal() => GroupPrincipal.FindByIdentity(Context, Identity); @@ -35,12 +36,11 @@ protected override void BuildFromAD( { IEnumerable members = groupPrincipal.ToSafeSortedEnumerable( selector: group => group.GetMembers(), - cmdlet: this, - comparer: Comparer); + cmdlet: this); foreach (Principal member in members) { - IDisposable? disposable = null; + Principal? disposable = null; try { if (member is { DistinguishedName: null } || diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index 0236e35..13ea5f7 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -50,8 +50,7 @@ void HandleOther(TreeObjectBase treeObject, Principal principal) IEnumerable principalMembership = principal.ToSafeSortedEnumerable( selector: principal => principal.GetGroups(Context), - cmdlet: this, - comparer: Comparer); + cmdlet: this); foreach (Principal parent in principalMembership) { @@ -73,8 +72,7 @@ protected override void BuildFromAD( { IEnumerable principalMembership = groupPrincipal.ToSafeSortedEnumerable( selector: principal => principal.GetGroups(Context), - cmdlet: this, - comparer: Comparer); + cmdlet: this); foreach (Principal group in principalMembership) { diff --git a/src/PSADTree/Extensions/MiscExtensions.cs b/src/PSADTree/Extensions/MiscExtensions.cs index 636394d..29e0a8c 100644 --- a/src/PSADTree/Extensions/MiscExtensions.cs +++ b/src/PSADTree/Extensions/MiscExtensions.cs @@ -32,6 +32,10 @@ internal static DirectoryEntry GetDirectoryEntry(this Principal principal) foreach (string property in properties) { + // already processed + if (additionalProperties.ContainsKey(property)) + continue; + if (!LdapMap.TryGetValue(property, out string? ldapDn)) { ldapDn = property; diff --git a/src/PSADTree/Extensions/TreeExtensions.cs b/src/PSADTree/Extensions/TreeExtensions.cs index 28686ba..5ea2336 100644 --- a/src/PSADTree/Extensions/TreeExtensions.cs +++ b/src/PSADTree/Extensions/TreeExtensions.cs @@ -88,11 +88,8 @@ internal static TreeObjectBase[] Format( #if NETCOREAPP [SkipLocalsInit] -#endif - private static unsafe string ReplaceAt(this string input, int index, char newChar) - { -#if NETCOREAPP - return string.Create( + private static string ReplaceAt(this string input, int index, char newChar) + => string.Create( input.Length, (input, index, newChar), static (buffer, state) => { @@ -100,6 +97,8 @@ private static unsafe string ReplaceAt(this string input, int index, char newCha buffer[state.index] = state.newChar; }); #else + private static unsafe string ReplaceAt(this string input, int index, char newChar) + { if (input.Length > 0x200) { char[] chars = input.ToCharArray(); @@ -119,14 +118,13 @@ private static unsafe string ReplaceAt(this string input, int index, char newCha pChars[index] = newChar; return new string(pChars, 0, input.Length); -#endif } +#endif internal static IEnumerable ToSafeSortedEnumerable( this TPrincipal principal, Func> selector, - PSCmdlet cmdlet, - PSADTreeComparer comparer) + PSCmdlet cmdlet) where TPrincipal : Principal { List principals = []; @@ -156,7 +154,7 @@ internal static IEnumerable ToSafeSortedEnumerable( return principals .OrderBy(static e => e.StructuralObjectClass == "group") - .ThenBy(static e => e, comparer); + .ThenBy(static e => e, PSADTreeComparer.Value); } internal static string GetDefaultNamingContext(this string distinguishedName) => diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index 434c49f..aed33a0 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -39,8 +39,6 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable internal TreeBuilder Builder { get; } = new(); - internal PSADTreeComparer Comparer { get; } = new(); - [Parameter( Position = 0, Mandatory = true, @@ -50,28 +48,35 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable public string? Identity { get; set; } [Parameter] + [Alias("s", "dc")] public string Server { get; set; } = Environment.UserDomainName; [Parameter] [Credential] + [Alias("cred")] public PSCredential? Credential { get; set; } [Parameter(ParameterSetName = DepthParameterSet)] [ValidateRange(0, int.MaxValue)] + [Alias("d")] public int Depth { get; set; } = 3; [Parameter(ParameterSetName = RecursiveParameterSet)] + [Alias("rec")] public SwitchParameter Recursive { get; set; } [Parameter] + [Alias("a")] public SwitchParameter ShowAll { get; set; } [Parameter] [SupportsWildcards] + [Alias("ex")] public string[]? Exclude { get; set; } [Parameter] [ArgumentCompleter(typeof(LdapCompleter))] + [Alias("prop", "attrs", "attributes")] public string[] Properties { get => _properties ??= []; diff --git a/src/PSADTree/PSADTreeComparer.cs b/src/PSADTree/PSADTreeComparer.cs index 29ace4e..34a462d 100644 --- a/src/PSADTree/PSADTreeComparer.cs +++ b/src/PSADTree/PSADTreeComparer.cs @@ -6,6 +6,10 @@ namespace PSADTree; #pragma warning disable CS8767 internal sealed class PSADTreeComparer : IComparer { + internal static PSADTreeComparer Value { get; } + + static PSADTreeComparer() => Value = new(); + public int Compare(Principal lhs, Principal rhs) => lhs.StructuralObjectClass == "group" && rhs.StructuralObjectClass == "group" ? rhs.SamAccountName.CompareTo(lhs.SamAccountName) // Groups in descending order From ba3cde3bf6bbc0800f4f439cfbf5b94fd5db162b Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Mon, 26 Jan 2026 16:28:48 -0300 Subject: [PATCH 13/14] done with the damned docs... final is readme --- docs/en-US/Get-ADTreeGroupMember.md | 134 ++++++++++++------ .../Get-ADTreePrincipalGroupMembership.md | 130 +++++++++++------ docs/en-US/PSADTree.md | 46 ++++-- 3 files changed, 216 insertions(+), 94 deletions(-) diff --git a/docs/en-US/Get-ADTreeGroupMember.md b/docs/en-US/Get-ADTreeGroupMember.md index a1d1b8d..341ea2f 100644 --- a/docs/en-US/Get-ADTreeGroupMember.md +++ b/docs/en-US/Get-ADTreeGroupMember.md @@ -9,7 +9,7 @@ schema: 2.0.0 ## SYNOPSIS -`tree` like cmdlet for Active Directory group members. +Displays Active Directory group members in a tree-like structure, including nested members and circular group detection. ## SYNTAX @@ -45,61 +45,87 @@ Get-ADTreeGroupMember ## DESCRIPTION -The `Get-ADTreeGroupMember` cmdlet gets the Active Directory members of a specified group and displays them in a tree like structure. The members of a group can be users, groups, computers and service accounts. This cmdlet also helps identifying Circular Nested Groups. +The `Get-ADTreeGroupMember` cmdlet retrieves the members of an Active Directory group—including nested members—and displays them in a tree-like hierarchical structure. Group members can include users, groups, computers, and service accounts. + +This format makes it easy to visualize group nesting and quickly identify circular nested groups (infinite recursion loops caused by circular membership). ## EXAMPLES ### Example 1: Get the members of a group ```powershell -PS ..\PSADTree\> Get-ADTreeGroupMember TestGroup001 +PS> Get-ADTreeGroupMember TestGroup001 ``` -By default, this cmdlet uses `-Depth` with a default value of `3`. +By default, this cmdlet uses `-Depth 3`. ### Example 2: Get the members of a group recursively ```powershell -PS ..\PSADTree\> Get-ADTreeGroupMember TestGroup001 -Recursive +PS> Get-ADTreeGroupMember TestGroup001 -Recursive ``` +The `-Recursive` switch retrieves all nested members regardless of depth. + ### Example 3: Get the members of all groups under an Organizational Unit ```powershell -PS ..\PSADTree\> Get-ADGroup -Filter * -SearchBase 'OU=myOU,DC=myDomain,DC=com' | +PS> Get-ADGroup -Filter * -SearchBase 'OU=myOU,DC=myDomain,DC=com' | Get-ADTreeGroupMember ``` -You can pipe strings containing an identity to this cmdlet. [__`ADGroup`__](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adgroup) instances piped to this cmdlet are also supported. +You can pipe strings containing group identity to this cmdlet or [__`ADGroup`__](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adgroup) objects to this cmdlet. -### Example 4: Find any Circular Nested Groups from previous example +### Example 4: Find circular nested groups ```powershell -PS ..\PSADTree\> Get-ADComputer -Filter * -SearchBase 'OU=myOU,DC=myDomain,DC=com' | +PS> Get-ADComputer -Filter * -SearchBase 'OU=myOU,DC=myDomain,DC=com' | Get-ADTreeGroupMember -Recursive -Group | Where-Object IsCircular ``` -The `-Group` switch limits the members tree view to nested groups only. +The `-Group` switch limits output to nested groups only (excluding users, computers, etc.). -### Example 5: Get group members in a different Domain +### Example 5: Get group members in a different domain ```powershell -PS ..\PSADTree\> Get-ADTreeGroupMember TestGroup001 -Server otherDomain +PS> Get-ADTreeGroupMember TestGroup001 -Server otherDomain.com ``` -### Example 6: Get group members including processed groups +### Example 6: Display hierarchy even for previously processed groups ```powershell -PS ..\PSADTree\> Get-ADTreeGroupMember TestGroup001 -ShowAll +PS> Get-ADTreeGroupMember TestGroup001 -ShowAll ``` -By default, previously processed groups will be marked as _"Processed Group"_ and their hierarchy will not be displayed. -The `-ShowAll` switch indicates that the cmdlet should display the hierarchy of all previously processed groups. +By default, groups that have already been processed (to avoid redundant recursion) are marked as _"Processed Group"_ and their subtree is not expanded. +The `-ShowAll` switch forces full hierarchy display for all groups. > [!NOTE] +> Using `-ShowAll` does not incur a significant performance penalty because the cmdlet caches group data internally. + +### Example 7: Retrieve and inspect additional properties + +```powershell +# Retrieve specific properties (friendly names are preserved as keys) +PS> $tree = Get-ADTreeGroupMember TestGroup001 -Properties Country, City, nTSecurityDescriptor + +# View department for all user objects +PS> $tree | Where-Object ObjectClass -EQ 'user' | + Select-Object *, @{ Name='Country'; Expression={ $_.AdditionalProperties['Country'] }} + +# Retrieve all available attributes +PS> $tree = Get-ADTreeGroupMember TestGroup001 -Properties * + +# Inspect the full dictionary for the first object +PS> $tree[0].AdditionalProperties # ReadOnlyDictionary +``` + +>[!TIP] > -> The use of this switch should not infer in a great performance cost, for more details see the parameter details. +> - `-Properties *` retrieves __all__ available attributes from each object. +> - Use friendly names (e.g. `Country` → `c`, `City` → `l`, etc) or raw LDAP names — the key in `.AdditionalProperties` matches what you requested. +> - See the full list of supported friendly names in the [source code `LdapMap.cs`](https://github.com/santisq/PSADTree/tree/main/src/PSADTree/LdapMap.cs) ## PARAMETERS @@ -107,7 +133,7 @@ The `-ShowAll` switch indicates that the cmdlet should display the hierarchy of Specifies a user account that has permission to perform this action. The default is the current user. -Type a user name, such as __User01__ or __Domain01\User01__, or enter a __PSCredential__ object generated by the `Get-Credential` cmdlet. If you type a user name, you're prompted to enter the password. +Type a user name, such as __User01__ or __Domain01\User01__, or enter a __PSCredential__ object generated by the `Get-Credential` cmdlet. If you type a user name, you will be prompted to enter the password. ```yaml Type: PSCredential @@ -124,7 +150,7 @@ Accept wildcard characters: False ### -Depth Determines the number of nested groups and their members included in the recursion. -By default, only 3 levels of recursion are included. `Get-ADTreeGroupMember` emits a warning if the levels exceed this number. +By default, only 3 levels of recursion are included. `Get-ADTreeGroupMember` emits a warning if the actual nesting exceeds this number. ```yaml Type: Int32 @@ -147,7 +173,7 @@ Wildcard characters are accepted. > [!NOTE] > > - Patterns are tested against the principal's `.SamAccountName` property. -> - When the matched principal is of type `group`, all child principals are also excluded from the output. +> - When the matched principal is of type `group`, __all__ child principals are also excluded from the output. ```yaml Type: String[] @@ -179,15 +205,15 @@ Accept wildcard characters: False ### -Identity -Specifies an Active Directory group by providing one of the following property values: +Specifies the Active Directory group to retrieve members from. You can identify the group using one of the following values: -- A DistinguishedName -- A GUID -- A SID (Security Identifier) -- A sAMAccountName -- A UserPrincipalName +- DistinguishedName +- GUID +- SID (Security Identifier) +- sAMAccountName +- UserPrincipalName -See [`IdentityType` Enum](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement.identitytype) for more information. +For more information, see the [`IdentityType` enumeration](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement.identitytype). ```yaml Type: String @@ -212,8 +238,8 @@ __Behavior:__ __Supported input styles:__ -- Friendly/PowerShell-style names (as in the Active Directory module), e.g., `City`, `Country`, `EmailAddress` -- Raw LDAP attribute names, e.g., `l`, `c`, `mail` +- Friendly/PowerShell-style names (as in the Active Directory module), e.g., `City`, `Country`, `Department`, `EmailAddress`, `PasswordLastSet`, `LastBadPasswordAttempt` +- Raw LDAP attribute names, e.g., `l`, `c`, `department`, `mail`, `pwdLastSet`, `badPasswordTime`, `whenCreated` When a friendly name is used, the key in `.AdditionalProperties` matches the friendly name (not the LDAP name). @@ -238,7 +264,10 @@ Accept wildcard characters: False ### -Recursive -Specifies that the cmdlet should get all group members of the specified group. +Retrieves __all__ nested group members recursively (no depth limit). + +> [!NOTE] +> This switch and `-Depth` are mutually exclusive. If `-Recursive` is specified, `-Depth` is ignored. ```yaml Type: SwitchParameter @@ -254,18 +283,18 @@ Accept wildcard characters: False ### -Server -Specifies the AD DS instance to connect to by providing one of the following values for a corresponding domain name or directory server. +Specifies the Active Directory server (or domain) to bind to. Valid values include: -Domain name values: +__Domain name formats__: -- Fully qualified domain name +- Fully qualified domain name (FQDN) - NetBIOS name -Directory server values: +__Server formats__: -- Fully qualified directory server name +- Fully qualified server name - NetBIOS name -- Fully qualified directory server name and port +- Fully qualified server name with port (e.g. `dc01.contoso.com:3268`) ```yaml Type: String @@ -281,14 +310,15 @@ Accept wildcard characters: False ### -ShowAll -By default, previously processed groups will be marked as _"Processed Group"_ and their hierarchy will not be displayed. -This switch forces the cmdlet to display the full hierarchy including previously processed groups. +By default, groups that have already been processed are marked as _"Processed Group"_ and their hierarchy is not expanded (to avoid redundant output and recursion). + +The `-ShowAll` switch forces the cmdlet to display the full hierarchy of all groups, even those previously processed. > [!NOTE] > -> This cmdlet uses a caching mechanism to ensure that Active Directory Groups are only queried once per Identity. -> This caching mechanism is also used to reconstruct the pre-processed group's hierarchy when the `-ShowAll` switch is used, thus not incurring a performance cost. -> The intent behind this switch is to not clutter the cmdlet's output by default. +> This cmdlet caches group data to query each unique group only once. +> The `-ShowAll` switch reuses this cache to reconstruct hierarchies without additional AD queries, so it does not cause a significant performance penalty. +> The default behavior (hiding processed groups) keeps output clean and focused. ```yaml Type: SwitchParameter @@ -310,22 +340,40 @@ This cmdlet supports the common parameters. For more information, see [about_Com ### System.String -You can pipe strings containing an identity to this cmdlet. [__`ADGroup`__](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adgroup) instances piped to this cmdlet are also supported. +You can pipe strings that represent a group identity (DistinguishedName, GUID, SID, sAMAccountName, or UserPrincipalName) to this cmdlet. + +### Microsoft.ActiveDirectory.Management.ADGroup + +[__`ADGroup`__](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adgroup) objects are also accepted (typically piped from `Get-ADGroup` or similar cmdlets). ## OUTPUTS ### PSADTree.TreeGroup +Represents an Active Directory group in the tree structure. + ### PSADTree.TreeUser +Represents an Active Directory user in the tree structure. + ### PSADTree.TreeComputer +Represents an Active Directory computer in the tree structure. + ## NOTES `treegroupmember` is the alias for this cmdlet. +The cmdlet uses internal caching to avoid redundant queries to Active Directory and efficiently detect/handle circular group nesting. + ## RELATED LINKS [__`Principal` Class__](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement.principal) -[__`DirectoryEntry`__ Class](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.directoryentry) +[__`DirectoryEntry` Class__](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.directoryentry) + +[__`Get-ADGroupMember`__](https://learn.microsoft.com/en-us/powershell/module/activedirectory/get-adgroupmember) + +[__`Get-ADGroup`__](https://learn.microsoft.com/en-us/powershell/module/activedirectory/get-adgroup) + +[__`Get-ADUser`__](https://learn.microsoft.com/en-us/powershell/module/activedirectory/get-aduser) diff --git a/docs/en-US/Get-ADTreePrincipalGroupMembership.md b/docs/en-US/Get-ADTreePrincipalGroupMembership.md index d4f609a..2158337 100644 --- a/docs/en-US/Get-ADTreePrincipalGroupMembership.md +++ b/docs/en-US/Get-ADTreePrincipalGroupMembership.md @@ -9,7 +9,7 @@ schema: 2.0.0 ## SYNOPSIS -`tree` like cmdlet for Active Directory Principals Group Membership. +Displays the group membership of an Active Directory principal in a tree-like structure, including nested groups and circular membership detection. ## SYNTAX @@ -43,37 +43,41 @@ Get-ADTreePrincipalGroupMembership ## DESCRIPTION -The `Get-ADTreePrincipalGroupMembership` cmdlet gets the Active Directory groups that have a specified user, computer, group, or service account as a member and displays them in a tree like structure. This cmdlet also helps identifying Circular Nested Groups. +The `Get-ADTreePrincipalGroupMembership` cmdlet retrieves the group membership of a specified Active Directory principal (user, computer, group, or service account) and displays it in a tree-like hierarchical structure. + +This format makes it easy to visualize nested group membership and quickly identify circular nested groups (where a principal is indirectly a member of itself through a loop). ## EXAMPLES ### Example 1: Get group memberships for a user ```powershell -PS ..\PSADTree\> Get-ADTreePrincipalGroupMembership john.doe +PS> Get-ADTreePrincipalGroupMembership john.doe ``` -By default, this cmdlet uses `-Depth` with a default value of `3`. +By default, this cmdlet uses `-Depth 3`. ### Example 2: Get the recursive group memberships for a user ```powershell -PS ..\PSADTree\> Get-ADTreePrincipalGroupMembership john.doe -Recursive +PS> Get-ADTreePrincipalGroupMembership john.doe -Recursive ``` +The `-Recursive` switch retrieves all nested group memberships regardless of depth. + ### Example 3: Get group memberships for all computers under an Organizational Unit ```powershell -PS ..\PSADTree\> Get-ADComputer -Filter * -SearchBase 'OU=myOU,DC=myDomain,DC=com' | +PS> Get-ADComputer -Filter * -SearchBase 'OU=myOU,DC=myDomain,DC=com' | Get-ADTreePrincipalGroupMembership ``` -You can pipe strings containing an identity to this cmdlet. [__`ADObject`__](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adobject) instances piped to this cmdlet are also supported. +You can pipe strings containing a principal identity or [__`ADObject`__](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adobject) objects to this cmdlet. -### Example 4: Find any Circular Nested Groups from previous example +### Example 4: Find circular nested groups ```powershell -PS \> Get-ADComputer -Filter * -SearchBase 'OU=myOU,DC=myDomain,DC=com' | +PS> Get-ADComputer -Filter * -SearchBase 'OU=myOU,DC=myDomain,DC=com' | Get-ADTreePrincipalGroupMembership -Recursive | Where-Object IsCircular ``` @@ -81,21 +85,40 @@ PS \> Get-ADComputer -Filter * -SearchBase 'OU=myOU,DC=myDomain,DC=com' | ### Example 5: Get group memberships for a user in a different Domain ```powershell -PS \> Get-ADTreePrincipalGroupMembership john.doe -Server otherDomain +PS> Get-ADTreePrincipalGroupMembership john.doe -Server otherDomain.com ``` -### Example 6: Get group memberships for a user, including processed groups +### Example 6: Display hierarchy even for previously processed groups ```powershell -PS \> Get-ADTreePrincipalGroupMembership john.doe -ShowAll +PS> Get-ADTreePrincipalGroupMembership john.doe -ShowAll ``` -By default, previously processed groups will be marked as _"Processed Group"_ and their hierarchy will not be displayed. -The `-ShowAll` switch indicates that the cmdlet should display the hierarchy of all previously processed groups. +By default, groups that have already been processed (to avoid redundant recursion) are marked as _"Processed Group"_ and their subtree is not expanded. +The `-ShowAll` switch forces full hierarchy display for all groups. > [!NOTE] +> Using `-ShowAll` does not incur a significant performance penalty because the cmdlet caches group data internally. + +### Example 7: Retrieve and inspect additional properties + +```powershell +# Retrieve specific properties (friendly names become the keys) +PS> $tree = Get-ADTreePrincipalGroupMembership john.doe -Properties PasswordLastSet, Department, City, nTSecurityDescriptor + +# Show Department for the principal (if it's a user or has that property) +PS> $tree | Select-Object *, @{Name='Department'; Expression={ $_.AdditionalProperties['Department'] }} + +# Or get everything +PS> $tree = Get-ADTreePrincipalGroupMembership john.doe -Properties * +PS> $tree[0].AdditionalProperties # ReadOnlyDictionary +``` + +>[!TIP] > -> The use of this switch should not infer in a great performance cost, for more details see the parameter details. +> - `-Properties *` retrieves __all__ available attributes from each object. +> - Use friendly names (e.g. `Country` → `c`, `City` → `l`, etc) or raw LDAP names — the key in `.AdditionalProperties` matches what you requested. +> - See the full list of supported friendly names in the [source code `LdapMap.cs`](https://github.com/santisq/PSADTree/tree/main/src/PSADTree/LdapMap.cs) ## PARAMETERS @@ -103,7 +126,7 @@ The `-ShowAll` switch indicates that the cmdlet should display the hierarchy of Specifies a user account that has permission to perform this action. The default is the current user. -Type a user name, such as __User01__ or __Domain01\User01__, or enter a __PSCredential__ object generated by the `Get-Credential` cmdlet. If you type a user name, you're prompted to enter the password. +Type a user name, such as __User01__ or __Domain01\User01__, or enter a __PSCredential__ object generated by the `Get-Credential` cmdlet. If you type a user name, you will be prompted to enter the password. ```yaml Type: PSCredential @@ -119,8 +142,8 @@ Accept wildcard characters: False ### -Depth -Determines the number of nested group memberships included in the recursion. -By default, only 3 levels of recursion are included. `Get-ADTreePrincipalGroupMembership` emits a warning if the levels exceed this number. +Determines the number of nested groups and their members included in the recursion. +By default, only 3 levels of recursion are included. `Get-ADTreePrincipalGroupMembership` emits a warning if the actual nesting exceeds this number. ```yaml Type: Int32 @@ -142,7 +165,8 @@ Wildcard characters are accepted. > [!NOTE] > -> Patterns are tested against the principal's `.SamAccountName` property. +> - Patterns are tested against the principal's `.SamAccountName` property. +> - When the matched principal is of type `group`, __all__ child principals are also excluded from the output. ```yaml Type: String[] @@ -158,15 +182,15 @@ Accept wildcard characters: True ### -Identity -Specifies an Active Directory principal by providing one of the following property values: +Specifies the Active Directory principal to retrieve members from. You can identify the group using one of the following values: -- A DistinguishedName -- A GUID -- A SID (Security Identifier) -- A sAMAccountName -- A UserPrincipalName +- DistinguishedName +- GUID +- SID (Security Identifier) +- sAMAccountName +- UserPrincipalName -See [`IdentityType` Enum](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement.identitytype) for more information. +For more information, see the [`IdentityType` enumeration](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement.identitytype). ```yaml Type: String @@ -191,8 +215,8 @@ __Behavior:__ __Supported input styles:__ -- Friendly/PowerShell-style names (as in the Active Directory module), e.g., `City`, `Country`, `EmailAddress` -- Raw LDAP attribute names, e.g., `l`, `c`, `mail` +- Friendly/PowerShell-style names (as in the Active Directory module), e.g., `City`, `Country`, `Department`, `EmailAddress`, `PasswordLastSet`, `LastBadPasswordAttempt` +- Raw LDAP attribute names, e.g., `l`, `c`, `department`, `mail`, `pwdLastSet`, `badPasswordTime`, `whenCreated` When a friendly name is used, the key in `.AdditionalProperties` matches the friendly name (not the LDAP name). @@ -217,7 +241,10 @@ Accept wildcard characters: False ### -Recursive -Specifies that the cmdlet should get all group membership of the specified principal. +Retrieves __all__ nested group members recursively (no depth limit). + +> [!NOTE] +> This switch and `-Depth` are mutually exclusive. If `-Recursive` is specified, `-Depth` is ignored. ```yaml Type: SwitchParameter @@ -233,18 +260,18 @@ Accept wildcard characters: False ### -Server -Specifies the AD DS instance to connect to by providing one of the following values for a corresponding domain name or directory server. +Specifies the Active Directory server (or domain) to bind to. Valid values include: -Domain name values: +__Domain name formats__: -- Fully qualified domain name +- Fully qualified domain name (FQDN) - NetBIOS name -Directory server values: +__Server formats__: -- Fully qualified directory server name +- Fully qualified server name - NetBIOS name -- Fully qualified directory server name and port +- Fully qualified server name with port (e.g. `dc01.contoso.com:3268`) ```yaml Type: String @@ -260,14 +287,15 @@ Accept wildcard characters: False ### -ShowAll -By default, previously processed groups will be marked as _"Processed Group"_ and their hierarchy will not be displayed. -This switch forces the cmdlet to display the full hierarchy including previously processed groups. +By default, groups that have already been processed are marked as _"Processed Group"_ and their hierarchy is not expanded (to avoid redundant output and recursion). + +The `-ShowAll` switch forces the cmdlet to display the full hierarchy of all groups, even those previously processed. > [!NOTE] > -> This cmdlet uses a caching mechanism to ensure that Active Directory Groups are only queried once per Identity. -> This caching mechanism is also used to reconstruct the pre-processed group's hierarchy when the `-ShowAll` switch is used, thus not incurring a performance cost. -> The intent behind this switch is to not clutter the cmdlet's output by default. +> This cmdlet caches group data to query each unique group only once. +> The `-ShowAll` switch reuses this cache to reconstruct hierarchies without additional AD queries, so it does not cause a significant performance penalty. +> The default behavior (hiding processed groups) keeps output clean and focused. ```yaml Type: SwitchParameter @@ -289,22 +317,40 @@ This cmdlet supports the common parameters. For more information, see [about_Com ### System.String -You can pipe strings containing an identity to this cmdlet. [`ADObject`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adobject) instances piped to this cmdlet are also supported. +You can pipe strings that represent a principal identity (DistinguishedName, GUID, SID, sAMAccountName, or UserPrincipalName) to this cmdlet. + +### Microsoft.ActiveDirectory.Management.ADObject + +[__`ADObject`__](https://learn.microsoft.com/en-us/dotnet/api/microsoft.activedirectory.management.adobject) objects are also accepted (typically piped from `Get-ADUser`, `Get-ADComputer`, `Get-ADGroup`, etc.). ## OUTPUTS ### PSADTree.TreeGroup +Represents an Active Directory group in the tree structure. + ### PSADTree.TreeUser +Represents an Active Directory user in the tree structure. + ### PSADTree.TreeComputer +Represents an Active Directory computer in the tree structure. + ## NOTES `treeprincipalmembership` is the alias for this cmdlet. +The cmdlet uses internal caching to avoid redundant queries to Active Directory and efficiently detect/handle circular group nesting. + ## RELATED LINKS [__`Principal` Class__](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement.principal) -[__`DirectoryEntry`__ Class](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.directoryentry) +[__`DirectoryEntry` Class__](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.directoryentry) + +[__`Get-ADGroupMember`__](https://learn.microsoft.com/en-us/powershell/module/activedirectory/get-adgroupmember) + +[__`Get-ADGroup`__](https://learn.microsoft.com/en-us/powershell/module/activedirectory/get-adgroup) + +[__`Get-ADUser`__](https://learn.microsoft.com/en-us/powershell/module/activedirectory/get-aduser) diff --git a/docs/en-US/PSADTree.md b/docs/en-US/PSADTree.md index 82e6f4b..cd4bf55 100644 --- a/docs/en-US/PSADTree.md +++ b/docs/en-US/PSADTree.md @@ -1,23 +1,51 @@ --- Module Name: PSADTree Module Guid: e49013dc-4106-4a95-aebc-b2669cbadeab -Download Help Link: +Download Help Link: https://github.com/santisq/PSADTree Help Version: 1.0.0.0 Locale: en-US --- -# PSADTree Module +# about_PSADTree -## Description +## SHORT DESCRIPTION -PSADTree is a PowerShell Module with cmdlets that emulate the [`tree` command](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/tree) for Active Directory Principals. +Displays Active Directory group membership hierarchies in a tree-like format (similar to the Windows `tree` command). -## PSADTree Cmdlets +## LONG DESCRIPTION -### [Get-ADTreeGroupMember](Get-ADTreeGroupMember.md) +**PSADTree** is a lightweight PowerShell module that provides two cmdlets to visualize Active Directory group structures: -The `Get-ADTreeGroupMember` cmdlet displays members of an Active Directory Group in a tree structure. +- [`Get-ADTreeGroupMember`](Get-ADTreeGroupMember.md) – Shows the members (users, groups, computers, etc.) of a group, including nested membership, in a tree view. + Helps identify nested groups and detect circular references. -### [Get-ADTreePrincipalGroupMembership](Get-ADTreePrincipalGroupMembership.md) +- [`Get-ADTreePrincipalGroupMembership`](Get-ADTreePrincipalGroupMembership.md) – Shows all groups that a principal (user, computer, group, or service account) belongs to, including nested paths, in a tree view. + Useful for auditing effective permissions and spotting circular nesting. -The `Get-ADTreePrincipalGroupMembership` cmdlet displays an Active Directory Principal group membership in a tree structure. +Both cmdlets support: + +- Depth limiting and full recursion +- Additional property retrieval (`-Properties`) +- Caching to avoid redundant AD queries +- Exclusion patterns +- Cross-domain support (`-Server`) + +They are particularly useful for troubleshooting complex group nesting, security reviews, and understanding effective group membership without manually traversing AD. + +## REQUIREMENTS + +- PowerShell 7.4+ or later (or Windows PowerShell 5.1) +- Appropriate permissions to read AD objects + +## EXAMPLES + +```powershell +# View nested members of a group (default depth 3) +Get-ADTreeGroupMember "Domain Admins" + +# View all groups a user belongs to (recursive) +Get-ADTreePrincipalGroupMembership john.doe -Recursive + +# Get full membership tree with extra properties +Get-ADTreePrincipalGroupMembership john.doe -Properties Department, Title, PasswordLastSet -Recursive +``` From 64958b3a7edcb07d3bf15609ef52c24e194974ff Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Mon, 26 Jan 2026 17:07:56 -0300 Subject: [PATCH 14/14] done with readme --- README.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8fceeb6..4252620 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

PSADTree

-Tree like cmdlets for Active Directory Principals! +Tree-like cmdlets for Active Directory principals!

[![build](https://github.com/santisq/PSADTree/actions/workflows/ci.yml/badge.svg)](https://github.com/santisq/PSADTree/actions/workflows/ci.yml) @@ -10,7 +10,8 @@
-PSADTree is a PowerShell Module with cmdlets that emulate the [`tree` command](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/tree) for Active Directory Principals. +PSADTree is a PowerShell module that brings `tree`-like visualization to Active Directory group structures — perfect for spotting nested membership and circular references at a glance. + This Module currently includes two cmdlets: - [Get-ADTreeGroupMember](docs/en-US/Get-ADTreeGroupMember.md) for AD Group Members. @@ -42,7 +43,9 @@ Set-Location ./PSADTree ## Requirements -This Module uses the [`System.DirectoryServices.AccountManagement` Namespace](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement?view=dotnet-plat-ext-7.0) to query Active Directory, its System Requirement is __Windows OS__ and is compatible with __Windows PowerShell v5.1__ or [__PowerShell 7+__](https://github.com/PowerShell/PowerShell). +- Windows operating system (uses Windows-specific Active Directory .NET APIs) +- PowerShell 5.1 (Windows PowerShell) or PowerShell 7.4+ +- Read permissions on the Active Directory objects you want to query ## Usage @@ -110,9 +113,70 @@ ChildDomain group ├── TestGroup005 ↔ Processed G ChildDomain group └── TestGroup006 ↔ Processed Group ``` +### Retrieve and inspect additional properties + +```powershell +PS ..\PSADTree> $tree = Get-ADTreeGroupMember TestGroup001 -Properties * +PS ..\PSADTree> $user = $tree | Where-Object ObjectClass -EQ user | Select-Object -First 1 +PS ..\PSADTree> $user.AdditionalProperties + +Key Value +--- ----- +objectClass {top, person, organizationalPerson, user} +cn John Doe +sn Doe +c US +l Elizabethtown +st NC +title Accounting Specialist +postalCode 28337 +physicalDeliveryOfficeName Accounting Office +telephoneNumber 910-862-8720 +givenName John +initials B +distinguishedName CN=John Doe,OU=Accounting,OU=Mylab Users,DC=mylab,DC=local +instanceType 4 +whenCreated 9/18/2025 4:53:58 PM +whenChanged 9/18/2025 4:53:58 PM +displayName John Doe +uSNCreated 19664 +memberOf CN=TestGroup001,OU=Mylab Groups,DC=mylab,DC=local +uSNChanged 19668 +department Accounting +company Active Directory Pro +streetAddress 2628 Layman Avenue +nTSecurityDescriptor System.DirectoryServices.ActiveDirectorySecurity +name John Doe +objectGUID {225, 241, 160, 222…} +userAccountControl 512 +badPwdCount 0 +codePage 0 +countryCode 0 +badPasswordTime 0 +lastLogoff 0 +lastLogon 0 +pwdLastSet 0 +primaryGroupID 513 +objectSid {1, 5, 0, 0…} +accountExpires 9223372036854775807 +logonCount 0 +sAMAccountName john.doe +sAMAccountType 805306368 +userPrincipalName john.doe@mylab.com +objectCategory CN=Person,CN=Schema,CN=Configuration,DC=mylab,DC=local +dSCorePropagationData 1/1/1601 12:00:00 AM +mail john.doe@mylab.com +``` + +>[!TIP] +> +> - `-Properties *` retrieves __all__ available attributes from each object. +> - Use friendly names (e.g. `Country` → `c`, `City` → `l`, `PasswordLastSet` → `pwdLastSet`) or raw LDAP names — the key in `.AdditionalProperties` matches what you requested. +> - See the full list of supported friendly names in the [source code `LdapMap.cs`](https://github.com/santisq/PSADTree/tree/main/src/PSADTree/LdapMap.cs) + ### Get group members recursively, include only groups and display all processed groups -The `-Recursive` switch indicates that the cmdlet should traverse all the group hierarchy. +The `-Recursive` switch indicates that the cmdlet should traverse traverse the entire group hierarchy. The `-Group` switch limits the members tree view to nested groups only. By default, previously processed groups will be marked as _"Processed Group"_ and their hierarchy will not be displayed. The `-ShowAll` switch indicates that the cmdlet should display the hierarchy of all previously processed groups.