diff --git a/README.md b/README.md index 4252620..a14b903 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,25 @@ -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. +PSADTree is a PowerShell module that brings intuitive `tree`-like visualization to Active Directory group structures. It helps administrators and security professionals quickly understand nested group memberships, identify effective permissions, and spot potential circular references at a glance. -This Module currently includes two cmdlets: +## Cmdlets -- [Get-ADTreeGroupMember](docs/en-US/Get-ADTreeGroupMember.md) for AD Group Members. -- [Get-ADTreePrincipalGroupMembership](docs/en-US/Get-ADTreePrincipalGroupMembership.md) for AD Principal Group Membership. +- **`Get-ADTreeGroupMember`** +Displays the members of an Active Directory group in a clear hierarchical tree view. It recursively shows nested groups, users, computers, and other principals, making it easy to visualize complex group nesting. -__Both cmdlets help with discovery of Circular Nested Groups.__ +- **`Get-ADTreePrincipalGroupMembership`** +Shows all groups that a given Active Directory principal (user, computer, group, etc.) belongs to, presented in a tree structure. This reverse view is especially useful for understanding effective membership and troubleshooting access issues. + +- **`Get-ADTreeStyle`** +Retrieves the singleton `TreeStyle` instance used to customize the colored, hierarchical output of `Get-ADTreeGroupMember` and `Get-ADTreePrincipalGroupMembership`. +Allows you to change colors for groups, users, computers, other principals, and apply accents. You can also control ANSI output rendering. ## Documentation -Check out [__the docs__](./docs/en-US/PSADTree.md) for information about how to use this Module. +- Learn how to use the cmdlets in the [official documentation](./docs/en-US/). + +- To Customize output rendering, see [about_TreeStyle](./docs/en-US/about_TreeStyle.md). ## Installation @@ -170,7 +177,7 @@ mail john.doe@mylab.com >[!TIP] > -> - `-Properties *` retrieves __all__ available attributes from each object. +> - `-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) @@ -274,4 +281,4 @@ ChildDomain group └── Users ## Contributing -Contributions are more than welcome, if you wish to contribute, fork this repository and submit a pull request with the changes. +Contributions are welcome, if you wish to contribute, fork this repository and submit a pull request with the changes. diff --git a/assets/EscapeSequence.png b/assets/EscapeSequence.png new file mode 100644 index 0000000..13f96e8 Binary files /dev/null and b/assets/EscapeSequence.png differ diff --git a/assets/Get-ADTreeGroupMember.After.png b/assets/Get-ADTreeGroupMember.After.png new file mode 100644 index 0000000..60bde94 Binary files /dev/null and b/assets/Get-ADTreeGroupMember.After.png differ diff --git a/assets/Get-ADTreeGroupMember.Before.png b/assets/Get-ADTreeGroupMember.Before.png new file mode 100644 index 0000000..b17a24f Binary files /dev/null and b/assets/Get-ADTreeGroupMember.Before.png differ diff --git a/assets/TreeStyle.png b/assets/TreeStyle.png new file mode 100644 index 0000000..2a2da50 Binary files /dev/null and b/assets/TreeStyle.png differ diff --git a/docs/en-US/Get-ADTreeStyle.md b/docs/en-US/Get-ADTreeStyle.md new file mode 100644 index 0000000..dab84b5 --- /dev/null +++ b/docs/en-US/Get-ADTreeStyle.md @@ -0,0 +1,63 @@ +--- +external help file: PSADTree.dll-Help.xml +Module Name: PSADTree +online version: https://github.com/santisq/PSADTree/blob/main/docs/en-US/Get-ADTreeStyle.md +schema: 2.0.0 +--- + +# Get-ADTreeStyle + +## SYNOPSIS + +Retrieves the `TreeStyle` instance used for output rendering. + +## SYNTAX + +```powershell +Get-ADTreeStyle + [] +``` + +## DESCRIPTION + +The `Get-ADTreeStyle` cmdlet provides access to the `TreeStyle` instance that controls the rendering and customization +of output for the `Get-ADTreeGroupMember` and `Get-ADTreePrincipalGroupMembership` cmdlets. + +To Customize output rendering, see [about_TreeStyle](about_TreeStyle.md). + +## EXAMPLES + +### Example 1 + +```powershell +PS ..\PSADTree> $style = Get-ADTreeStyle +``` + +Stores the `TreeStyle` instance in the `$style` variable. + +## PARAMETERS + +### CommonParameters + +This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None + +## OUTPUTS + +### TreeStyle + +## NOTES + +Modifying the properties of this object (such as colors for groups, users, computers, etc.) will immediately affect the +visual output of `Get-ADTreeGroupMember` and `Get-ADTreePrincipalGroupMembership` in the current PowerShell session. + +## RELATED LINKS + +[__`Get-ADTreeGroupMember`__](Get-ADTreeGroupMember.md) + +[__`Get-ADTreePrincipalGroupMembership`__](Get-ADTreePrincipalGroupMembership.md) + +[__about_TreeStyle__](about_TreeStyle.md) diff --git a/docs/en-US/about_TreeStyle.md b/docs/en-US/about_TreeStyle.md new file mode 100644 index 0000000..ec50462 --- /dev/null +++ b/docs/en-US/about_TreeStyle.md @@ -0,0 +1,102 @@ +# about_TreeStyle + +## TOPIC + +Customizing PSADTree Output with TreeStyle. + +## SHORT DESCRIPTION + +The `TreeStyle` class enables customization of the hierarchical output for `Get-ADTreeGroupMember` and `Get-ADTreePrincipalGroupMembership` cmdlets in the PSADTree module. + +## LONG DESCRIPTION + +PSADTree version 1.3.0 and later introduces support for coloring the hierarchical output of the `Get-ADTreeGroupMember` and `Get-ADTreePrincipalGroupMembership` cmdlets using the `TreeStyle` class. This class provides a subset of features similar to those in PowerShell’s built-in [PSStyle][1]. +You can access the singleton instance of `TreeStyle` through either the [Get-ADTreeStyle][2] cmdlet or the `[PSADTree.Style.TreeStyle]::Instance` property: + +
+     + TreeStyle +
+ +The `TreeStyle` class offers methods for combining escape sequences and applying text accents, such as bold or italic. See the next section for additional details. + +Here are its members: + +```powershell + TypeName: PSADTree.Style.TreeStyle + +Name MemberType Definition +---- ---------- ---------- +CombineSequence Method string CombineSequence(string left, string right) +Equals Method bool Equals(System.Object obj) +EscapeSequence Method string EscapeSequence(string vt) +GetHashCode Method int GetHashCode() +GetType Method type GetType() +ResetSettings Method void ResetSettings() +ToBold Method string ToBold(string vt) +ToItalic Method string ToItalic(string vt) +ToString Method string ToString() +OutputRendering Property PSADTree.Style.OutputRendering OutputRendering {get;set;} +Palette Property PSADTree.Style.Palette Palette {get;} +Principal Property PSADTree.Style.PrincipalStyle Principal {get;} +RenderingStyle Property PSADTree.Style.RenderingStyle RenderingStyle {get;set;} +Reset Property string Reset {get;} +``` + +The `.EscapeSequence()` method reveals the escape sequence applied to generate specific colors or accents. For example: + +
+     + EscapeSequence +
+ +## CUSTOMIZING OUTPUT + +You can customize the output by modifying the properties of the `TreeStyle` class, much like you would with PowerShell’s `PSStyle`. This allows you to update colors for computers, groups, and users, as well as the circular and processed tags. + +Consider the standard output of `Get-ADTreeGroupMember`: + +
+     + Get-ADTreeGroupMember.Before +
+ +You can adjust the appearance by modifying the `PSADTree.Style.TreeStyle` object. Here’s an example of how to apply customizations: + +```powershell +$style = Get-ADTreeStyle +$palette = $style.Palette + +# Update users to white text on a red background +$style.Principal.User = $style.CombineSequence($palette.Foreground.White, $palette.Background.Red) + +# Change the rendering style to use ASCII +$style.RenderingStyle = 'Classic' +``` + +> [!TIP] +> +> - PowerShell 6 and later support the `` `e `` escape character for VT sequences. For __Windows PowerShell 5.1__, use `[char] 27` instead. For example, replace ``"`e[45m"`` with `"$([char] 27)[45m"`. See [about_Special_Characters][3] for more details. +> - The `TreeStyle` class provides methods like `.ToItalic()`, `.ToBold()`, and `.CombineSequence()` to apply text accents or combine VT sequences. +> - To reset the `TreeStyle` instance to its default state, use `.ResetSettings()`. If stored in a variable, reassign it afterward, e.g., `$style.ResetSettings()` followed by `$style = Get-ADTreeStyle`. + +After applying these changes, re-running the same `Get-ADTreeGroupMember` command will display the updated styles: + +
+     + Get-ADTreeGroupMember.After +
+ +## DISABLING ANSI OUTPUT + +Just like PowerShell’s `PSStyle`, you can disable ANSI rendering in PSADTree’s output by modifying the `.OutputRendering` property of the `TreeStyle` instance. Simply set it to `'PlainText'` using the following command: + +```powershell +(Get-ADTreeStyle).OutputRendering = 'PlainText' +``` + +This disables all ANSI-based coloring and formatting, resulting in plain text output for commands like `Get-ADTreeGroupMember` and `Get-ADTreePrincipalGroupMembership`. It’s a straightforward way to simplify the display when you don’t need the extra visual styling. + +[1]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_ansi_terminals +[2]: ./Get-ADTreeStyle.md +[3]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_special_characters?view=powershell-7.4 diff --git a/module/PSADTree.Format.ps1xml b/module/PSADTree.Format.ps1xml deleted file mode 100644 index 1c3a2af..0000000 --- a/module/PSADTree.Format.ps1xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - treeview - - PSADTree.TreeObjectBase - - - [PSADTree.Internal._FormattingInternals]::GetSource($_) - - - - - - - Left - - - - Right - - - - Left - - - - - - - [PSADTree.Internal._FormattingInternals]::GetDomain($_) - - - ObjectClass - - - Hierarchy - - - - - - - - diff --git a/module/PSADTree.Formats.ps1xml b/module/PSADTree.Formats.ps1xml new file mode 100644 index 0000000..1c056f3 --- /dev/null +++ b/module/PSADTree.Formats.ps1xml @@ -0,0 +1,259 @@ + + + + + treeviewad + + PSADTree.TreeObjectBase + + + [PSADTree.Internal._FormattingInternals]::GetSource($_) + + + + + + + Left + + + + Right + + + + Left + + + + + + + [PSADTree.Internal._FormattingInternals]::GetDomain($_) + + + ObjectClass + + + Hierarchy + + + + + + + + PSADTree.Style.PrincipalStyle + + PSADTree.Style.PrincipalStyle + + + + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.Circular) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.Processed) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.Computer) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.Group) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.User) + + + + + + + + + PSADTree.Style.Palette + + PSADTree.Style.Palette + + + + + + + + Foreground + + + + Background + + + + + + + + PSADTree.Style.Palette.Palettes + + PSADTree.Style.Palette+ForegroundPalette + PSADTree.Style.Palette+BackgroundPalette + + + + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.Black) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.BrightBlack) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.White) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.BrightWhite) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.Red) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.BrightRed) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.Magenta) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.BrightMagenta) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.Blue) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.BrightBlue) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.Cyan) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.BrightCyan) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.Green) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.BrightGreen) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.Yellow) + + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.BrightYellow) + + + + + + + + + PSADTree.Style.TreeStyle + + PSADTree.Style.TreeStyle + + + + + + + + OutputRendering + + + + RenderingStyle + + + + + [PSADTree.Style.TreeStyle]::Instance.EscapeSequence($_.Reset) + + + + + Principal + + + + Palette + + + + + + + + diff --git a/module/PSADTree.psd1 b/module/PSADTree.psd1 index 557e3a7..19ef06c 100644 --- a/module/PSADTree.psd1 +++ b/module/PSADTree.psd1 @@ -16,7 +16,7 @@ } # Version number of this module. - ModuleVersion = '1.2.0' + ModuleVersion = '1.3.0' # Supported PSEditions # CompatiblePSEditions = @() @@ -67,7 +67,7 @@ # TypesToProcess = @() # Format files (.ps1xml) to be loaded when importing this module - FormatsToProcess = @('PSADTree.Format.ps1xml') + FormatsToProcess = @('PSADTree.Formats.ps1xml') # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess # NestedModules = @() @@ -77,6 +77,7 @@ # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @( + 'Get-ADTreeStyle' 'Get-ADTreeGroupMember' 'Get-ADTreePrincipalGroupMembership' ) @@ -86,6 +87,7 @@ # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. AliasesToExport = @( + 'treestyle' 'treegroupmember' 'treeprincipalmembership' ) diff --git a/src/PSADTree/Commands/GetADTreeStyleCommand.cs b/src/PSADTree/Commands/GetADTreeStyleCommand.cs new file mode 100644 index 0000000..1c157f7 --- /dev/null +++ b/src/PSADTree/Commands/GetADTreeStyleCommand.cs @@ -0,0 +1,12 @@ +using System.Management.Automation; +using PSADTree.Style; + +namespace PSADTree.Commands; + +[Cmdlet(VerbsCommon.Get, "ADTreeStyle")] +[OutputType(typeof(TreeStyle))] +[Alias("treestyle")] +public sealed class GetADTreeStyleCommand : PSCmdlet +{ + protected override void BeginProcessing() => WriteObject(TreeStyle.Instance); +} diff --git a/src/PSADTree/Extensions/ExceptionExtensions.cs b/src/PSADTree/Extensions/ExceptionExtensions.cs index 121dc38..de83a7e 100644 --- a/src/PSADTree/Extensions/ExceptionExtensions.cs +++ b/src/PSADTree/Extensions/ExceptionExtensions.cs @@ -6,22 +6,34 @@ namespace PSADTree.Extensions; internal static class ExceptionExtensions { - internal static ErrorRecord ToIdentityNotFound(this string? identity) => - new( - new NoMatchingPrincipalException($"Cannot find an object with identity: '{identity}'."), - "IdentityNotFound", - ErrorCategory.ObjectNotFound, - identity); + extension(string? identity) + { + internal ErrorRecord ToIdentityNotFound() => + new( + new NoMatchingPrincipalException($"Cannot find an object with identity: '{identity}'."), + "IdentityNotFound", + ErrorCategory.ObjectNotFound, + identity); + } - internal static ErrorRecord ToAmbiguousIdentity(this Exception exception, string? identity) => - new(exception, "AmbiguousIdentity", ErrorCategory.InvalidResult, identity); + extension(Exception exception) + { + internal ErrorRecord ToAmbiguousIdentity(string? identity) => + new(exception, "AmbiguousIdentity", ErrorCategory.InvalidResult, identity); - internal static ErrorRecord ToUnspecified(this Exception exception, string? identity) => - new(exception, "Unspecified", ErrorCategory.NotSpecified, identity); + internal ErrorRecord ToUnspecified(string? identity) => + new(exception, "Unspecified", ErrorCategory.NotSpecified, identity); - internal static ErrorRecord ToEnumerationFailure(this Exception exception, Principal? principal) => - new(exception, "EnumerationFailure", ErrorCategory.NotSpecified, principal); + internal ErrorRecord ToEnumerationFailure(Principal? principal) => + new(exception, "EnumerationFailure", ErrorCategory.NotSpecified, principal); - internal static ErrorRecord ToSetPrincipalContext(this Exception exception) => - new(exception, "SetPrincipalContext", ErrorCategory.ConnectionError, null); + internal ErrorRecord ToSetPrincipalContext() => + new(exception, "SetPrincipalContext", ErrorCategory.ConnectionError, null); + } + + extension(string vt) + { + internal void ThrowInvalidSequence() => throw new ArgumentException( + $"The specified string contains printable content when it should only contain ANSI escape sequences: '{vt}'."); + } } diff --git a/src/PSADTree/Extensions/MiscExtensions.cs b/src/PSADTree/Extensions/MiscExtensions.cs index 29e0a8c..239d0ea 100644 --- a/src/PSADTree/Extensions/MiscExtensions.cs +++ b/src/PSADTree/Extensions/MiscExtensions.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.DirectoryServices; using System.DirectoryServices.AccountManagement; +using System.Globalization; using System.Linq; using PSADTree.Internal; @@ -11,58 +12,74 @@ 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) + extension(Principal principal) { - 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); + internal DirectoryEntry GetDirectoryEntry() => (DirectoryEntry)principal.GetUnderlyingObject(); - foreach (string property in properties) + internal ReadOnlyDictionary? GetAdditionalProperties(string[] properties) { - // already processed - if (additionalProperties.ContainsKey(property)) - continue; + if (properties.Length == 0) + return null; - if (!LdapMap.TryGetValue(property, out string? ldapDn)) - { - ldapDn = property; - } + DirectoryEntry entry = principal.GetDirectoryEntry(); - if (IsSecurityDescriptor(property)) - { - additionalProperties[property] = entry.GetSecurityDescriptorAsPSObject(); - continue; - } + if (properties.Any(e => e == "*")) + return GetAllAttributes(entry); - object? value = entry.Properties[ldapDn]?.Value; + Dictionary additionalProperties = new( + capacity: properties.Length, + StringComparer.OrdinalIgnoreCase); - if (value is null) continue; - if (IsIAdsLargeInteger(value, out long? fileTime)) + foreach (string property in properties) { - additionalProperties[property] = fileTime; - continue; + // 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; } - additionalProperties[property] = value; + return additionalProperties is { Count: 0 } ? null : new(additionalProperties); } + } - return additionalProperties is { Count: 0 } ? null : new(additionalProperties); + extension(AuthenticablePrincipal principal) + { + internal UserAccountControl? GetUserAccountControl() + { + DirectoryEntry entry = principal.GetDirectoryEntry(); + object? uac = entry.Properties["userAccountControl"]?.Value; + if (uac is null) return null; + return (UserAccountControl)Convert.ToUInt32(uac, CultureInfo.InvariantCulture); + } } - private static ReadOnlyDictionary GetAllAttributes(this DirectoryEntry entry) + extension(UserAccountControl uac) + { + internal bool IsEnabled() => !uac.HasFlag(UserAccountControl.ACCOUNTDISABLE); + } + + private static ReadOnlyDictionary GetAllAttributes(DirectoryEntry entry) { Dictionary additionalProperties = new( capacity: entry.Properties.Count, @@ -79,6 +96,7 @@ internal static DirectoryEntry GetDirectoryEntry(this Principal principal) object? value = entry.Properties[property]?.Value; if (value is null) continue; + if (IsIAdsLargeInteger(value, out long? fileTime)) { additionalProperties[property] = fileTime; @@ -96,8 +114,7 @@ private static bool IsIAdsLargeInteger( [NotNullWhen(true)] out long? fileTime) { fileTime = default; - if (value is not IAdsLargeInteger largeInt) - return false; + if (value is not IAdsLargeInteger largeInt) return false; fileTime = (largeInt.HighPart << 32) + largeInt.LowPart; return true; @@ -105,15 +122,4 @@ 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(); - 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 5ea2336..90ad764 100644 --- a/src/PSADTree/Extensions/TreeExtensions.cs +++ b/src/PSADTree/Extensions/TreeExtensions.cs @@ -3,160 +3,176 @@ using System.DirectoryServices.AccountManagement; using System.Linq; using System.Management.Automation; -#if NETCOREAPP +#if NET8_0_OR_GREATER using System.Runtime.CompilerServices; #else using System.Text; #endif using System.Text.RegularExpressions; +using PSADTree.Style; namespace PSADTree.Extensions; -internal static class TreeExtensions +internal static partial class TreeExtensions { +#if NET8_0_OR_GREATER + [GeneratedRegex("(?<=,)DC=.+$", RegexOptions.Compiled)] + private static partial Regex GetDefaultNamingContextRegex(); + + private static readonly Regex s_reDefaultNamingContext = GetDefaultNamingContextRegex(); +#else private static readonly Regex s_reDefaultNamingContext = new( "(?<=,)DC=.+$", RegexOptions.Compiled); -#if !NETCOREAPP [ThreadStatic] private static StringBuilder? s_sb; #endif - internal static string Indent(this string inputString, int indentation) + + extension(string input) { - const string corner = "└── "; - int repeatCount = (4 * indentation) - 4; - int capacity = repeatCount + 4 + inputString.Length; - -#if NETCOREAPP - return string.Create( - capacity, (repeatCount, corner, inputString), - static (buffer, state) => + internal string GetDefaultNamingContext() => s_reDefaultNamingContext.Match(input).Value; + + internal string Indent(int indentation) { - int count = state.repeatCount; - buffer[..count].Fill(' '); - state.corner.AsSpan().CopyTo(buffer[count..]); - state.inputString.AsSpan().CopyTo(buffer[(count + 4)..]); - }); + string corner = TreeStyle.Instance.RenderingSet.Corner; + int repeatCount = (4 * indentation) - 4; + int capacity = repeatCount + 4 + input.Length; + +#if NET8_0_OR_GREATER + return string.Create( + capacity, (repeatCount, corner, input), + static (buffer, state) => + { + int count = state.repeatCount; + buffer[..count].Fill(' '); + state.corner.AsSpan().CopyTo(buffer[count..]); + state.input.AsSpan().CopyTo(buffer[(count + 4)..]); + }); #else - s_sb ??= new StringBuilder(64); - s_sb.Clear().EnsureCapacity(capacity); - - return s_sb - .Append(' ', repeatCount) - .Append(corner) - .Append(inputString) - .ToString(); + s_sb ??= new StringBuilder(64); + s_sb.Clear().EnsureCapacity(capacity); + + return s_sb + .Append(' ', repeatCount) + .Append(corner) + .Append(input) + .ToString(); #endif - } + } - internal static TreeObjectBase[] Format( - this TreeObjectBase[] tree) - { - int index; - for (int i = 0; i < tree.Length; i++) +#if NET8_0_OR_GREATER + [SkipLocalsInit] + private string ReplaceAt(int index, char newChar) + => string.Create( + input.Length, (input, index, newChar), + static (buffer, state) => + { + state.input.AsSpan().CopyTo(buffer); + buffer[state.index] = state.newChar; + }); +#else + private unsafe string ReplaceAt(int index, char newChar) { - TreeObjectBase current = tree[i]; + if (input.Length > 0x200) + { + char[] chars = input.ToCharArray(); + chars[index] = newChar; + return new string(chars); + } - if ((index = current.Hierarchy.IndexOf('└')) == -1) + char* pChars = stackalloc char[0x200]; + fixed (char* source = input) { - continue; + Buffer.MemoryCopy( + source, + pChars, + 0x200 * sizeof(char), + input.Length * sizeof(char)); } - for (int z = i - 1; z >= 0; z--) + pChars[index] = newChar; + return new string(pChars, 0, input.Length); + } +#endif + } + + extension(TreeObjectBase[] tree) + { + internal TreeObjectBase[] Format() + { + int index; + RenderingSet set = TreeStyle.Instance.RenderingSet; + for (int i = 0; i < tree.Length; i++) { - current = tree[z]; - string hierarchy = current.Hierarchy; + TreeObjectBase current = tree[i]; - if (char.IsWhiteSpace(hierarchy[index])) +#if NET8_0_OR_GREATER + index = current.Hierarchy.IndexOf(set.UpRight, StringComparison.Ordinal); +#else + index = current.Hierarchy.IndexOf(set.UpRight); +#endif + + if (index == -1) { - current.Hierarchy = hierarchy.ReplaceAt(index, '│'); continue; } - if (hierarchy[index] == '└') + for (int z = i - 1; z >= 0; z--) { - current.Hierarchy = hierarchy.ReplaceAt(index, '├'); - } + current = tree[z]; + string hierarchy = current.Hierarchy; - break; - } - } + if (char.IsWhiteSpace(hierarchy[index])) + { + current.Hierarchy = hierarchy.ReplaceAt(index, set.Vertical); + continue; + } - return tree; - } + if (hierarchy[index] == set.UpRight) + { + current.Hierarchy = hierarchy.ReplaceAt(index, set.VerticalRight); + } -#if NETCOREAPP - [SkipLocalsInit] - private static string ReplaceAt(this string input, int index, char newChar) - => string.Create( - input.Length, (input, index, newChar), - static (buffer, state) => - { - state.input.AsSpan().CopyTo(buffer); - 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(); - chars[index] = newChar; - return new string(chars); - } + break; + } + } - char* pChars = stackalloc char[0x200]; - fixed (char* source = input) - { - Buffer.MemoryCopy( - source, - pChars, - 0x200 * sizeof(char), - input.Length * sizeof(char)); + return tree; } - - pChars[index] = newChar; - return new string(pChars, 0, input.Length); } -#endif - internal static IEnumerable ToSafeSortedEnumerable( - this TPrincipal principal, - Func> selector, - PSCmdlet cmdlet) - where TPrincipal : Principal + extension(TPrincipal principal) where TPrincipal : Principal { - List principals = []; - using PrincipalSearchResult search = selector(principal); - using IEnumerator enumerator = search.GetEnumerator(); - - while (true) + internal IEnumerable ToSafeSortedEnumerable( + Func> selector, + PSCmdlet cmdlet) { - try + List principals = []; + using PrincipalSearchResult search = selector(principal); + using IEnumerator enumerator = search.GetEnumerator(); + + while (true) { - if (!enumerator.MoveNext()) + try { - break; - } + if (!enumerator.MoveNext()) break; - principals.Add(enumerator.Current); - } - catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) - { - throw; - } - catch (Exception exception) - { - cmdlet.WriteError(exception.ToEnumerationFailure(principal)); + principals.Add(enumerator.Current); + } + catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) + { + throw; + } + catch (Exception exception) + { + cmdlet.WriteError(exception.ToEnumerationFailure(principal)); + } } - } - return principals - .OrderBy(static e => e.StructuralObjectClass == "group") - .ThenBy(static e => e, PSADTreeComparer.Value); + return principals + .OrderBy(static e => e.StructuralObjectClass == "group") + .ThenBy(static e => e, PSADTreeComparer.Value); + } } - - internal static string GetDefaultNamingContext(this string distinguishedName) => - s_reDefaultNamingContext.Match(distinguishedName).Value; } diff --git a/src/PSADTree/Internal/_FormattingInternals.cs b/src/PSADTree/Internal/_FormattingInternals.cs index 9a11246..793c8cf 100644 --- a/src/PSADTree/Internal/_FormattingInternals.cs +++ b/src/PSADTree/Internal/_FormattingInternals.cs @@ -7,11 +7,17 @@ namespace PSADTree.Internal; #pragma warning disable IDE1006 [EditorBrowsable(EditorBrowsableState.Never)] -public static class _FormattingInternals +public static partial class _FormattingInternals { +#if NET8_0_OR_GREATER + [GeneratedRegex(@"^DC=|(? treeObject.Source; diff --git a/src/PSADTree/Internal/_SecurityDescriptorInternals.cs b/src/PSADTree/Internal/_SecurityDescriptorInternals.cs index 5b8393e..0c4834b 100644 --- a/src/PSADTree/Internal/_SecurityDescriptorInternals.cs +++ b/src/PSADTree/Internal/_SecurityDescriptorInternals.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.DirectoryServices; +using System.Globalization; using System.Management.Automation; using System.Reflection; using System.Security.AccessControl; @@ -14,22 +15,13 @@ namespace PSADTree.Internal; [EditorBrowsable(EditorBrowsableState.Never)] public static class _SecurityDescriptorInternals { - private readonly static Type s_target = typeof(NTAccount); - private static readonly MethodInfo s_getOwner; - private static readonly MethodInfo s_getGroup; - private static readonly MethodInfo s_getSddlForm; - private static readonly MethodInfo s_getAccessRules; - private static readonly MethodInfo s_getAccessToString; - - static _SecurityDescriptorInternals() - { - Type type = typeof(_SecurityDescriptorInternals); - s_getOwner = GetMethod(type, nameof(GetOwner)); - s_getGroup = GetMethod(type, nameof(GetGroup)); - s_getSddlForm = GetMethod(type, nameof(GetSddlForm)); - s_getAccessRules = GetMethod(type, nameof(GetAccessRules)); - s_getAccessToString = GetMethod(type, nameof(GetAccessToString)); - } + private static readonly Type s_type = typeof(_SecurityDescriptorInternals); + private static readonly Type s_target = typeof(NTAccount); + private static readonly MethodInfo s_getOwner = GetMethod(s_type, nameof(GetOwner)); + private static readonly MethodInfo s_getGroup = GetMethod(s_type, nameof(GetGroup)); + private static readonly MethodInfo s_getSddlForm = GetMethod(s_type, nameof(GetSddlForm)); + private static readonly MethodInfo s_getAccessRules = GetMethod(s_type, nameof(GetAccessRules)); + private static readonly MethodInfo s_getAccessToString = GetMethod(s_type, nameof(GetAccessToString)); private static MethodInfo GetMethod(Type type, string name) => type.GetMethod(name) @@ -55,7 +47,13 @@ public static string GetAccessToString(PSObject target) { StringBuilder builder = new(); foreach (ActiveDirectoryAccessRule rule in GetAccessRules(target)) + { +#if NET8_0_OR_GREATER + builder.AppendLine(CultureInfo.InvariantCulture, $"{rule.IdentityReference} {rule.AccessControlType}"); +#else builder.AppendLine($"{rule.IdentityReference} {rule.AccessControlType}"); +#endif + } return builder.ToString(); } diff --git a/src/PSADTree/Nullable.cs b/src/PSADTree/Nullable.cs index ad74d3b..73c7eea 100644 --- a/src/PSADTree/Nullable.cs +++ b/src/PSADTree/Nullable.cs @@ -1,4 +1,4 @@ -#if !NETCOREAPP +#if !NET8_0_OR_GREATER namespace System.Diagnostics.CodeAnalysis; diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index aed33a0..12c2ed4 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Management.Automation; using PSADTree.Extensions; +using PSADTree.Style; namespace PSADTree; @@ -87,6 +88,8 @@ protected override void BeginProcessing() { try { + TreeStyle.Instance.SupportsVirtualTerminal = Host.UI.SupportsVirtualTerminal; + if (Recursive.IsPresent) { Depth = int.MaxValue; diff --git a/src/PSADTree/PSADTreeComparer.cs b/src/PSADTree/PSADTreeComparer.cs index 34a462d..d0a3820 100644 --- a/src/PSADTree/PSADTreeComparer.cs +++ b/src/PSADTree/PSADTreeComparer.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.DirectoryServices.AccountManagement; @@ -12,6 +13,6 @@ internal sealed class PSADTreeComparer : IComparer public int Compare(Principal lhs, Principal rhs) => lhs.StructuralObjectClass == "group" && rhs.StructuralObjectClass == "group" - ? rhs.SamAccountName.CompareTo(lhs.SamAccountName) // Groups in descending order - : lhs.SamAccountName.CompareTo(rhs.SamAccountName); // Other in ascending order + ? string.Compare(rhs.SamAccountName, lhs.SamAccountName, StringComparison.Ordinal) // Groups in descending order + : string.Compare(lhs.SamAccountName, rhs.SamAccountName, StringComparison.Ordinal); // Other in ascending order } diff --git a/src/PSADTree/Style/OutputRendering.cs b/src/PSADTree/Style/OutputRendering.cs new file mode 100644 index 0000000..c8be2e1 --- /dev/null +++ b/src/PSADTree/Style/OutputRendering.cs @@ -0,0 +1,8 @@ +namespace PSADTree.Style; + +public enum OutputRendering +{ + Host = 0, + PlainText = 1, + Ansi = 2 +} diff --git a/src/PSADTree/Style/Palette.cs b/src/PSADTree/Style/Palette.cs new file mode 100644 index 0000000..6b096b4 --- /dev/null +++ b/src/PSADTree/Style/Palette.cs @@ -0,0 +1,98 @@ +using System; + +namespace PSADTree.Style; + +public sealed class Palette +{ + public sealed class ForegroundPalette + { + private string? _toString; + + public string Black { get; } = "\x1B[30m"; + + public string BrightBlack { get; } = "\x1B[90m"; + + public string White { get; } = "\x1B[37m"; + + public string BrightWhite { get; } = "\x1B[97m"; + + public string Red { get; } = "\x1B[31m"; + + public string BrightRed { get; } = "\x1B[91m"; + + public string Magenta { get; } = "\x1B[35m"; + + public string BrightMagenta { get; } = "\x1B[95m"; + + public string Blue { get; } = "\x1B[34m"; + + public string BrightBlue { get; } = "\x1B[94m"; + + public string Cyan { get; } = "\x1B[36m"; + + public string BrightCyan { get; } = "\x1B[96m"; + + public string Green { get; } = "\x1B[32m"; + + public string BrightGreen { get; } = "\x1B[92m"; + + public string Yellow { get; } = "\x1B[33m"; + + public string BrightYellow { get; } = "\x1B[93m"; + + public override string ToString() => + _toString ??= TreeStyle.FormatType(this); + } + + public sealed class BackgroundPalette + { + private string? _toString; + + public string Black { get; } = "\x1B[40m"; + + public string BrightBlack { get; } = "\x1B[100m"; + + public string White { get; } = "\x1B[47m"; + + public string BrightWhite { get; } = "\x1B[107m"; + + public string Red { get; } = "\x1B[41m"; + + public string BrightRed { get; } = "\x1B[101m"; + + public string Magenta { get; } = "\x1B[45m"; + + public string BrightMagenta { get; } = "\x1B[105m"; + + public string Blue { get; } = "\x1B[44m"; + + public string BrightBlue { get; } = "\x1B[104m"; + + public string Cyan { get; } = "\x1B[46m"; + + public string BrightCyan { get; } = "\x1B[106m"; + + public string Green { get; } = "\x1B[42m"; + + public string BrightGreen { get; } = "\x1B[102m"; + + public string Yellow { get; } = "\x1B[43m"; + + public string BrightYellow { get; } = "\x1B[103m"; + + public override string ToString() => + _toString ??= TreeStyle.FormatType(this); + } + + private string? _toString; + + public ForegroundPalette Foreground { get; } = new(); + + public BackgroundPalette Background { get; } = new(); + + internal Palette() + { } + + public override string ToString() => + _toString ??= $"{Foreground}{Environment.NewLine}{Environment.NewLine}{Background}"; +} diff --git a/src/PSADTree/Style/PrincipalStyle.cs b/src/PSADTree/Style/PrincipalStyle.cs new file mode 100644 index 0000000..82aa80d --- /dev/null +++ b/src/PSADTree/Style/PrincipalStyle.cs @@ -0,0 +1,97 @@ +using System.DirectoryServices.AccountManagement; + +namespace PSADTree.Style; + +public sealed class PrincipalStyle +{ + private static TreeStyle TreeStyle { get => TreeStyle.Instance; } + + public string Circular + { + get; + set => field = TreeStyle.ThrowIfInvalidSequence(value); + } = "\x1B[91m"; + + public string Processed + { + get; + set => field = TreeStyle.ThrowIfInvalidSequence(value); + } = "\x1B[93m"; + + public string Computer + { + get; + set => TreeStyle.ThrowIfInvalidSequence(value); + } = string.Empty; + + public string Group + { + get; + set => field = TreeStyle.ThrowIfInvalidSequence(value); + } = string.Empty; + + public string User + { + get; + set => field = TreeStyle.ThrowIfInvalidSequence(value); + } = string.Empty; + + internal PrincipalStyle() + { } + + internal string GetColoredName(TreeObjectBase treeObject) + { + if (TreeStyle.OutputRendering == OutputRendering.PlainText || + (TreeStyle.OutputRendering == OutputRendering.Host && !TreeStyle.SupportsVirtualTerminal)) + { + return treeObject.SamAccountName; + } + + return treeObject switch + { + TreeComputer treeComputer => $"{Computer}{treeComputer.SamAccountName}{TreeStyle.Reset}", + TreeGroup treeGroup => $"{Group}{treeGroup.SamAccountName}{TreeStyle.Reset}", + TreeUser treeUser => $"{User}{treeUser.SamAccountName}{TreeStyle.Reset}", + _ => treeObject.SamAccountName + }; + } + + internal string GetColoredName(Principal principal) + { + if (TreeStyle.OutputRendering == OutputRendering.PlainText || + (TreeStyle.OutputRendering == OutputRendering.Host && !TreeStyle.SupportsVirtualTerminal)) + { + return principal.SamAccountName; + } + + return principal switch + { + ComputerPrincipal computerPrincipal => $"{Computer}{computerPrincipal.SamAccountName}{TreeStyle.Reset}", + GroupPrincipal groupPrincipal => $"{Group}{groupPrincipal.SamAccountName}{TreeStyle.Reset}", + UserPrincipal userPrincipal => $"{User}{userPrincipal.SamAccountName}{TreeStyle.Reset}", + _ => principal.SamAccountName + }; + } + + internal string GetColoredCircular() + { + if (TreeStyle.OutputRendering == OutputRendering.PlainText || + (TreeStyle.OutputRendering == OutputRendering.Host && !TreeStyle.SupportsVirtualTerminal)) + { + return $"{TreeStyle.RenderingSet.Arrows} Circular Reference"; + } + + return $"{TreeStyle.RenderingSet.Arrows} {Circular}Circular Reference{TreeStyle.Reset}"; + } + + internal string GetColoredProcessed() + { + if (TreeStyle.OutputRendering == OutputRendering.PlainText || + (TreeStyle.OutputRendering == OutputRendering.Host && !TreeStyle.SupportsVirtualTerminal)) + { + return $"{TreeStyle.RenderingSet.Arrows} Processed Group"; + } + + return $"{TreeStyle.RenderingSet.Arrows} {Processed}Processed Group{TreeStyle.Reset}"; + } +} diff --git a/src/PSADTree/Style/RenderingSet.cs b/src/PSADTree/Style/RenderingSet.cs new file mode 100644 index 0000000..0ed90a3 --- /dev/null +++ b/src/PSADTree/Style/RenderingSet.cs @@ -0,0 +1,36 @@ +namespace PSADTree.Style; + +internal readonly struct RenderingSet +{ + internal static readonly RenderingSet Fancy = new("└── ", '└', '│', '├', "↔"); + + internal static readonly RenderingSet FancyRounded = new("╰── ", '╰', '│', '├', "↔"); + + internal static readonly RenderingSet Classic = new("+-- ", '+', '|', '|', "<>"); + + internal static readonly RenderingSet ClassicRounded = new("`-- ", '`', '|', '|', "<>"); + + internal string Corner { get; } + + internal char UpRight { get; } + + internal char Vertical { get; } + + internal char VerticalRight { get; } + + internal string Arrows { get; } + + private RenderingSet( + string corner, + char upRight, + char vertical, + char verticalRight, + string arrows) + { + Corner = corner; + UpRight = upRight; + Vertical = vertical; + VerticalRight = verticalRight; + Arrows = arrows; + } +} diff --git a/src/PSADTree/Style/RenderingStyle.cs b/src/PSADTree/Style/RenderingStyle.cs new file mode 100644 index 0000000..1ecfc2a --- /dev/null +++ b/src/PSADTree/Style/RenderingStyle.cs @@ -0,0 +1,9 @@ +namespace PSADTree.Style; + +public enum RenderingStyle +{ + Fancy = 0, + FancyRounded = 1, + Classic = 2, + ClassicRounded = 3 +} diff --git a/src/PSADTree/Style/TreeStyle.cs b/src/PSADTree/Style/TreeStyle.cs new file mode 100644 index 0000000..215c1ab --- /dev/null +++ b/src/PSADTree/Style/TreeStyle.cs @@ -0,0 +1,133 @@ +using System; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using PSADTree.Extensions; + +namespace PSADTree.Style; + +public sealed partial class TreeStyle +{ + private static TreeStyle? s_instance; + +#if NET8_0_OR_GREATER + [GeneratedRegex(@"^\x1B\[(?:[0-9]+;?){1,}m$", RegexOptions.Compiled)] + private static partial Regex ValidateRegex(); + + private static readonly Regex s_validate = ValidateRegex(); +#else + private static readonly Regex s_validate = new( + @"^\x1B\[(?:[0-9]+;?){1,}m$", + RegexOptions.Compiled); +#endif + + public static TreeStyle Instance { get => s_instance ??= new(); } + + public OutputRendering OutputRendering { get; set; } = OutputRendering.Host; + + public RenderingStyle RenderingStyle + { + get; + set + { + RenderingSet = value switch + { + RenderingStyle.Fancy => RenderingSet.Fancy, + RenderingStyle.FancyRounded => RenderingSet.FancyRounded, + RenderingStyle.Classic => RenderingSet.Classic, + RenderingStyle.ClassicRounded => RenderingSet.ClassicRounded, + _ => throw new ArgumentOutOfRangeException(nameof(RenderingStyle)) + }; + + field = value; + } + } = RenderingStyle.Fancy; + + public string Reset { get; } = "\x1B[0m"; + + public PrincipalStyle Principal { get; } = new(); + + public Palette Palette { get; } = new(); + + internal RenderingSet RenderingSet { get; private set; } = RenderingSet.Fancy; + + internal bool SupportsVirtualTerminal { get; set; } = true; + + internal TreeStyle() + { } + + public string CombineSequence(string left, string right) + { + ThrowIfInvalidSequence(left); + ThrowIfInvalidSequence(right); + return $"{left.TrimEnd('m')};{right.Substring(2)}"; + } + + public string ToItalic(string vt) + { + ThrowIfInvalidSequence(vt); + return $"{vt.TrimEnd('m')};3m"; + } + + public string ToBold(string vt) + { + ThrowIfInvalidSequence(vt); + return $"{vt.TrimEnd('m')};1m"; + } + + public string EscapeSequence(string vt) + { +#if NET8_0_OR_GREATER + return $"{vt}{vt.Replace("\x1B", "`e", StringComparison.Ordinal)}\x1B[0m"; +#else + return $"{vt}{vt.Replace("\x1B", "`e")}\x1B[0m"; +#endif + } + + public void ResetSettings() => s_instance = new(); + + internal static string FormatType(object instance) + { + PropertyInfo[] properties = instance.GetType().GetProperties(); + StringBuilder builder = new(properties.Length); + int i = 1; + + foreach (PropertyInfo property in properties) + { + string value = EscapeSequence( + vt: (string)property.GetValue(instance)!, + padding: 10); + + builder.Append(value); + + if (i++ % 4 == 0) + { + builder.AppendLine("\x1B[0m"); + continue; + } + + builder.Append("\x1B[0m"); + } + + return builder.ToString(); + } + + internal static string ThrowIfInvalidSequence(string vt) + { + if (!s_validate.IsMatch(vt)) + { + vt.ThrowInvalidSequence(); + } + + return vt; + } + + private static string EscapeSequence(string vt, int padding) + { +#if NET8_0_OR_GREATER + return $"{vt}{vt.Replace("\x1B", "`e", StringComparison.Ordinal).PadRight(padding)}\x1B[0m"; +#else + return $"{vt}{vt.Replace("\x1B", "`e").PadRight(padding)}\x1B[0m"; +#endif + } +} diff --git a/src/PSADTree/TreeCache.cs b/src/PSADTree/TreeCache.cs index 185fd7d..0f8dc52 100644 --- a/src/PSADTree/TreeCache.cs +++ b/src/PSADTree/TreeCache.cs @@ -11,7 +11,7 @@ internal sealed class TreeCache internal bool TryAdd(TreeGroup group) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER return _cache.TryAdd(group.DistinguishedName, group); #else if (_cache.ContainsKey(group.DistinguishedName)) diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index 27cce7a..5f07bb4 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -1,20 +1,13 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.DirectoryServices.AccountManagement; +using PSADTree.Style; namespace PSADTree; public sealed class TreeGroup : TreeObjectBase { - private const string Circular = $" ↔ {VTBrightRed}Circular Reference{VTReset}"; - - private const string Processed = $" ↔ {VTBrightYellow}Processed Group{VTReset}"; - - private const string VTBrightRed = "\x1B[91m"; - - private const string VTBrightYellow = "\x1B[93m"; - - private const string VTReset = "\x1B[0m"; + private static TreeStyle TreeStyle { get => TreeStyle.Instance; } private List _children; @@ -72,13 +65,16 @@ internal bool SetIfCircularNested() { if (IsCircular = IsCircularNested()) { - Hierarchy = $"{Hierarchy}{Circular}"; + Hierarchy = $"{Hierarchy} {TreeStyle.Principal.GetColoredCircular()}"; } return IsCircular; } - internal void SetProcessed() => Hierarchy = $"{Hierarchy}{Processed}"; + internal void SetProcessed() + { + Hierarchy = $"{Hierarchy} {TreeStyle.Principal.GetColoredProcessed()}"; + } internal void LinkCachedChildren(TreeCache cache) => _children = cache[DistinguishedName]._children; diff --git a/src/PSADTree/TreeObjectBase.cs b/src/PSADTree/TreeObjectBase.cs index 1aba91c..c820a8b 100644 --- a/src/PSADTree/TreeObjectBase.cs +++ b/src/PSADTree/TreeObjectBase.cs @@ -3,14 +3,15 @@ using System.DirectoryServices.AccountManagement; using System.Security.Principal; using PSADTree.Extensions; +using PSADTree.Style; namespace PSADTree; public abstract class TreeObjectBase { - public int Depth { get; } + private static TreeStyle TreeStyle { get => TreeStyle.Instance; } - internal string Source { get; } + public int Depth { get; } public TreeGroup? Parent { get; } @@ -36,6 +37,8 @@ public abstract class TreeObjectBase public ReadOnlyDictionary? AdditionalProperties { get; } + internal string Source { get; } + protected TreeObjectBase( TreeObjectBase treeObject, TreeGroup? parent, @@ -50,7 +53,7 @@ protected TreeObjectBase( DistinguishedName = treeObject.DistinguishedName; ObjectGuid = treeObject.ObjectGuid; ObjectSid = treeObject.ObjectSid; - Hierarchy = treeObject.SamAccountName.Indent(depth); + Hierarchy = TreeStyle.Principal.GetColoredName(treeObject).Indent(depth); Parent = parent; UserPrincipalName = treeObject.UserPrincipalName; Description = treeObject.Description; @@ -67,7 +70,7 @@ protected TreeObjectBase(string source, Principal principal, string[] properties DistinguishedName = principal.DistinguishedName; ObjectGuid = principal.Guid; ObjectSid = principal.Sid; - Hierarchy = SamAccountName; + Hierarchy = TreeStyle.Principal.GetColoredName(principal); UserPrincipalName = principal.UserPrincipalName; Description = principal.Description; DisplayName = principal.DisplayName; @@ -83,7 +86,7 @@ protected TreeObjectBase( : this(source, principal, properties) { Depth = depth; - Hierarchy = principal.SamAccountName.Indent(depth); + Hierarchy = TreeStyle.Principal.GetColoredName(principal).Indent(depth); Parent = parent; }