-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.
diff --git a/docs/en-US/Get-ADTreeGroupMember.md b/docs/en-US/Get-ADTreeGroupMember.md
index 6390bf7..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
@@ -17,13 +17,14 @@ schema: 2.0.0
```powershell
Get-ADTreeGroupMember
- [-Group]
[-Identity]
[-Server ]
[-Credential ]
[-Depth ]
[-ShowAll]
+ [-Group]
[-Exclude ]
+ [-Properties ]
[]
```
@@ -31,73 +32,100 @@ Get-ADTreeGroupMember
```powershell
Get-ADTreeGroupMember
- [-Group]
[-Identity]
[-Server ]
[-Credential ]
[-Recursive]
[-ShowAll]
+ [-Group]
[-Exclude ]
+ [-Properties ]
[]
```
## 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?view=activedirectory-management-10.0) 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
@@ -105,12 +133,12 @@ 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
Parameter Sets: (All)
-Aliases:
+Aliases: cred
Required: False
Position: Named
@@ -122,12 +150,12 @@ 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
Parameter Sets: Depth
-Aliases:
+Aliases: d
Required: False
Position: Named
@@ -145,12 +173,12 @@ 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[]
Parameter Sets: (All)
-Aliases:
+Aliases: ex
Required: False
Position: Named
@@ -166,7 +194,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
@@ -177,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?view=dotnet-plat-ext-7.0) 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
@@ -199,14 +227,52 @@ 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`, `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).
+
+__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.
+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
Parameter Sets: Recursive
-Aliases:
+Aliases: rec
Required: False
Position: Named
@@ -217,23 +283,23 @@ 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
Parameter Sets: (All)
-Aliases:
+Aliases: s, dc
Required: False
Position: Named
@@ -244,19 +310,20 @@ 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
Parameter Sets: (All)
-Aliases:
+Aliases: a
Required: False
Position: Named
@@ -273,18 +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?view=activedirectory-management-10.0) 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)
+
+[__`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 50d6033..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
@@ -23,6 +23,7 @@ Get-ADTreePrincipalGroupMembership
[-Depth ]
[-ShowAll]
[-Exclude ]
+ [-Properties ]
[]
```
@@ -36,42 +37,47 @@ Get-ADTreePrincipalGroupMembership
[-Recursive]
[-ShowAll]
[-Exclude ]
+ [-Properties ]
[]
```
## 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?view=activedirectory-management-10.0) 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 ..\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,21 +85,40 @@ 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.com
```
-### Example 6: Get group memberships for a user, including processed groups
+### Example 6: Display hierarchy even for previously 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.
-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
@@ -101,12 +126,12 @@ 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
Parameter Sets: (All)
-Aliases:
+Aliases: cred
Required: False
Position: Named
@@ -117,13 +142,13 @@ 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
Parameter Sets: Depth
-Aliases:
+Aliases: d
Required: False
Position: Named
@@ -140,12 +165,13 @@ 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[]
Parameter Sets: (All)
-Aliases:
+Aliases: ex
Required: False
Position: Named
@@ -156,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?view=dotnet-plat-ext-7.0) 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
@@ -178,14 +204,52 @@ 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`, `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).
+
+__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.
+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
Parameter Sets: Recursive
-Aliases:
+Aliases: rec
Required: False
Position: Named
@@ -196,23 +260,23 @@ 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
Parameter Sets: (All)
-Aliases:
+Aliases: s, dc
Required: False
Position: Named
@@ -223,19 +287,20 @@ 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
Parameter Sets: (All)
-Aliases:
+Aliases: a
Required: False
Position: Named
@@ -252,18 +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?view=activedirectory-management-10.0) 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)
+
+[__`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
+```
diff --git a/module/PSADTree.Format.ps1xml b/module/PSADTree.Format.ps1xml
index 9eef827..1c3a2af 100644
--- a/module/PSADTree.Format.ps1xml
+++ b/module/PSADTree.Format.ps1xml
@@ -12,19 +12,12 @@
-
-
Left
-
Right
@@ -35,11 +28,8 @@
-
- $_.Domain -replace '^DC=|(?<!\\),.+'
+ [PSADTree.Internal._FormattingInternals]::GetDomain($_)ObjectClass
diff --git a/module/PSADTree.psd1 b/module/PSADTree.psd1
index 0e1db7f..557e3a7 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 = @()
@@ -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/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs
index 068d268..5b51134 100644
--- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs
+++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs
@@ -10,13 +10,11 @@ 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]
+ [Alias("g")]
public SwitchParameter Group { get; set; }
protected override Principal GetFirstPrincipal() => GroupPrincipal.FindByIdentity(Context, Identity);
@@ -26,7 +24,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);
}
}
@@ -38,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 } ||
@@ -92,9 +89,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..13ea5f7 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);
@@ -24,15 +21,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:
@@ -53,15 +50,14 @@ 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)
{
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);
}
}
@@ -76,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/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
new file mode 100644
index 0000000..29e0a8c
--- /dev/null
+++ b/src/PSADTree/Extensions/MiscExtensions.cs
@@ -0,0 +1,119 @@
+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 PSADTree.Internal;
+
+namespace PSADTree.Extensions;
+
+internal static class MiscExtensions
+{
+ internal static DirectoryEntry GetDirectoryEntry(this Principal principal)
+ => (DirectoryEntry)principal.GetUnderlyingObject();
+
+ internal static ReadOnlyDictionary? GetAdditionalProperties(
+ this Principal principal,
+ string[] properties)
+ {
+ if (properties.Length == 0)
+ return null;
+
+ DirectoryEntry entry = principal.GetDirectoryEntry();
+
+ if (properties.Any(e => e == "*"))
+ return entry.GetAllAttributes();
+
+ Dictionary additionalProperties = new(
+ capacity: properties.Length,
+ StringComparer.OrdinalIgnoreCase);
+
+ foreach (string property in properties)
+ {
+ // already processed
+ if (additionalProperties.ContainsKey(property))
+ continue;
+
+ if (!LdapMap.TryGetValue(property, out string? ldapDn))
+ {
+ ldapDn = property;
+ }
+
+ if (IsSecurityDescriptor(property))
+ {
+ additionalProperties[property] = entry.GetSecurityDescriptorAsPSObject();
+ continue;
+ }
+
+ object? value = entry.Properties[ldapDn]?.Value;
+
+ if (value is null) continue;
+ if (IsIAdsLargeInteger(value, out long? fileTime))
+ {
+ additionalProperties[property] = fileTime;
+ continue;
+ }
+
+ additionalProperties[property] = value;
+ }
+
+ return additionalProperties is { Count: 0 } ? null : new(additionalProperties);
+ }
+
+ 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.GetSecurityDescriptorAsPSObject();
+ continue;
+ }
+
+ 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 bool IsIAdsLargeInteger(
+ object value,
+ [NotNullWhen(true)] out long? fileTime)
+ {
+ fileTime = default;
+ if (value is not IAdsLargeInteger largeInt)
+ return false;
+
+ fileTime = (largeInt.HighPart << 32) + largeInt.LowPart;
+ return true;
+ }
+
+ private static bool IsSecurityDescriptor(string ldapDn)
+ => ldapDn.Equals("nTSecurityDescriptor", StringComparison.OrdinalIgnoreCase);
+
+ internal static UserAccountControl? GetUserAccountControl(this AuthenticablePrincipal principal)
+ {
+ DirectoryEntry entry = principal.GetDirectoryEntry();
+ 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/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/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/Internal/_FormattingInternals.cs b/src/PSADTree/Internal/_FormattingInternals.cs
index 4cb3171..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
new file mode 100644
index 0000000..b10a3a5
--- /dev/null
+++ b/src/PSADTree/Internal/_SecurityDescriptorInternals.cs
@@ -0,0 +1,84 @@
+using System;
+using System.ComponentModel;
+using System.DirectoryServices;
+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 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()
+ {
+ 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)
+ => (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)
+ => 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,
+ 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;
+ }
+}
diff --git a/src/PSADTree/LdapCompleter.cs b/src/PSADTree/LdapCompleter.cs
new file mode 100644
index 0000000..030c4b7
--- /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.Keys)
+ {
+ if (key.StartsWith(wordToComplete, System.StringComparison.OrdinalIgnoreCase))
+ {
+ yield return new CompletionResult(
+ key,
+ key,
+ CompletionResultType.ParameterValue,
+ $"LDAP DisplayName: {LdapMap.Get(key)}");
+ }
+ }
+ }
+}
diff --git a/src/PSADTree/LdapMap.cs b/src/PSADTree/LdapMap.cs
new file mode 100644
index 0000000..b83045e
--- /dev/null
+++ b/src/PSADTree/LdapMap.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+
+namespace PSADTree;
+
+internal static class LdapMap
+{
+ internal static ReadOnlyDictionary Instance { get; }
+
+ 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()
+ {
+ 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/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/PSADTree.csproj b/src/PSADTree/PSADTree.csproj
index 4866383..c583673 100644
--- a/src/PSADTree/PSADTree.csproj
+++ b/src/PSADTree/PSADTree.csproj
@@ -1,12 +1,11 @@
- net6.0;net472
+ net8.0-windows;net472enabletruePSADTreelatest
- CA1416
@@ -14,12 +13,10 @@
-
-
-
-
+
+
+
+
diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs
index ecd17b3..aed33a0 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;
@@ -37,8 +39,6 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable
internal TreeBuilder Builder { get; } = new();
- internal PSADTreeComparer Comparer { get; } = new();
-
[Parameter(
Position = 0,
Mandatory = true,
@@ -48,26 +48,41 @@ 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 ??= [];
+ set => _properties = [.. value.Where(e => !string.IsNullOrWhiteSpace(e))];
+ }
+
protected override void BeginProcessing()
{
try
@@ -225,7 +240,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/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
diff --git a/src/PSADTree/TreeComputer.cs b/src/PSADTree/TreeComputer.cs
index ae7590d..0c128df 100644
--- a/src/PSADTree/TreeComputer.cs
+++ b/src/PSADTree/TreeComputer.cs
@@ -1,28 +1,43 @@
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,
TreeGroup? parent,
ComputerPrincipal computer,
+ string[] properties,
int depth)
- : base(source, parent, computer, depth)
- { }
+ : base(source, parent, computer, properties, depth)
+ {
+ UserAccountControl = computer.GetUserAccountControl();
+ Enabled = UserAccountControl?.IsEnabled();
+ }
- internal TreeComputer(string source, ComputerPrincipal computer)
- : base(source, computer)
- { }
+ internal TreeComputer(string source, ComputerPrincipal computer, string[] properties)
+ : base(source, computer, properties)
+ {
+ UserAccountControl = computer.GetUserAccountControl();
+ Enabled = UserAccountControl?.IsEnabled();
+ }
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 6aee386..27cce7a 100644
--- a/src/PSADTree/TreeGroup.cs
+++ b/src/PSADTree/TreeGroup.cs
@@ -33,8 +33,8 @@ private TreeGroup(
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 +43,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 = [];
}
diff --git a/src/PSADTree/TreeObjectBase.cs b/src/PSADTree/TreeObjectBase.cs
index b9d8680..1aba91c 100644
--- a/src/PSADTree/TreeObjectBase.cs
+++ b/src/PSADTree/TreeObjectBase.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.ObjectModel;
using System.DirectoryServices.AccountManagement;
using System.Security.Principal;
using PSADTree.Extensions;
@@ -33,6 +34,8 @@ public abstract class TreeObjectBase
public SecurityIdentifier ObjectSid { get; }
+ public ReadOnlyDictionary? AdditionalProperties { get; }
+
protected TreeObjectBase(
TreeObjectBase treeObject,
TreeGroup? parent,
@@ -52,9 +55,10 @@ protected TreeObjectBase(
UserPrincipalName = treeObject.UserPrincipalName;
Description = treeObject.Description;
DisplayName = treeObject.DisplayName;
+ AdditionalProperties = treeObject.AdditionalProperties;
}
- protected TreeObjectBase(string source, Principal principal)
+ protected TreeObjectBase(string source, Principal principal, string[] properties)
{
Source = source;
SamAccountName = principal.SamAccountName;
@@ -67,14 +71,16 @@ protected TreeObjectBase(string source, Principal principal)
UserPrincipalName = principal.UserPrincipalName;
Description = principal.Description;
DisplayName = principal.DisplayName;
+ AdditionalProperties = principal.GetAdditionalProperties(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);
diff --git a/src/PSADTree/TreeUser.cs b/src/PSADTree/TreeUser.cs
index 9abf626..19d063c 100644
--- a/src/PSADTree/TreeUser.cs
+++ b/src/PSADTree/TreeUser.cs
@@ -1,28 +1,43 @@
using System.DirectoryServices.AccountManagement;
+using PSADTree.Extensions;
namespace PSADTree;
public sealed class TreeUser : TreeObjectBase
{
+ public UserAccountControl? UserAccountControl { get; }
+
+ public bool? Enabled { get; }
+
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)
+ {
+ UserAccountControl = user.GetUserAccountControl();
+ Enabled = UserAccountControl?.IsEnabled();
+ }
- internal TreeUser(string source, UserPrincipal user)
- : base(source, user)
- { }
+ internal TreeUser(string source, UserPrincipal user, string[] properties)
+ : base(source, user, properties)
+ {
+ UserAccountControl = user.GetUserAccountControl();
+ Enabled = UserAccountControl?.IsEnabled();
+ }
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..ccfbcf1
--- /dev/null
+++ b/src/PSADTree/UserAccountControl.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace PSADTree;
+
+[Flags]
+public enum UserAccountControl : uint
+{
+ 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)
+}