├── .gitattributes ├── .gitignore ├── Directory.Build.props ├── DitExplorer.Core ├── DitExplorer.Core.csproj ├── IAttributeSchema.cs ├── IAttributeSyntax.cs ├── IClassSchema.cs ├── IDirectory.cs ├── IDirectoryObject.cs ├── ISchemaObject.cs └── MultiValue.cs ├── DitExplorer.EseInterop ├── DitExplorer.EseInterop.csproj ├── JetColumnInfo.cs ├── JetColumnType.cs ├── JetCursor.cs ├── JetDatabase.cs ├── JetIndexColumnInfo.cs ├── JetInstance.cs ├── JetSession.cs ├── JetTableInfo.cs ├── Messages.Designer.cs ├── Messages.resx └── esent.dll ├── DitExplorer.Extensibility ├── DitExplorer.Extensibility.csproj ├── DitExtensionManifest.xsd ├── IDirectoryNode.cs ├── IItemActionContext.cs ├── IItemActionRegistry.cs └── ItemAction.cs ├── DitExplorer.Ntds ├── ADInstanceType.cs ├── ADSearchFlags.cs ├── ArgumentHelper.cs ├── AttributeSyntax.cs ├── AttributeSystemFlags.cs ├── CollectionHelpers.cs ├── DirectoryObjectNotFoundException.cs ├── DitExplorer.Ntds.csproj ├── Messages.Designer.cs ├── Messages.resx ├── NtdsAttributeSchema.cs ├── NtdsClassSchema.cs ├── NtdsDirectory.cs ├── NtdsDirectoryEnumerator.cs ├── NtdsDirectoryLinkEnumerator.cs ├── NtdsDirectoryObject.cs ├── NtdsDirectoryObjectReference.cs └── NtdsSchemaObject.cs ├── DitExplorer.UI.Core ├── AssemblyInfo.cs ├── Behaviors │ ├── CheckedList.cs │ ├── CommandFrame.cs │ ├── ContextCommandService.cs │ ├── FlexGrid.cs │ ├── FocusHelper.cs │ └── ListViewSorting.cs ├── CollectionHelpers.cs ├── DitExplorer.UI.Core.csproj ├── ICommandHandler.cs ├── IContextCommandProvider.cs ├── Messages.Designer.cs ├── Messages.resx ├── Notifier.cs └── ViewModel.cs ├── DitExplorer.Utilities ├── DitExplorer.Utilities.csproj ├── HexHelper.cs └── TabularTextWriter.cs ├── DitExplorer.WpfApp ├── Actions │ └── ViewMembersAction.cs ├── App.xaml ├── App.xaml.cs ├── AppViewModel.cs ├── ColumnChooserViewModel.cs ├── ColumnChooserWindow.xaml ├── ColumnChooserWindow.xaml.cs ├── ContextCommandInfo.cs ├── Controls │ └── MultiValueCell.cs ├── DatabaseRecord.cs ├── DatabaseSchemaViewModel.cs ├── DatabaseSchemaWindow.xaml ├── DatabaseSchemaWindow.xaml.cs ├── DirectoryListViewModel.cs ├── DirectoryNode.cs ├── DirectoryPropertyDescriptor.cs ├── DirectoryView.cs ├── DitExplorer.UI.WpfApp.csproj ├── ItemActionCommand.cs ├── ItemActivation.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── Messages.Designer.cs ├── Messages.resx ├── MyCommands.cs ├── ObjectInspectorViewModel.cs ├── ObjectInspectorWindow.xaml ├── ObjectInspectorWindow.xaml.cs ├── ObjectSearchViewModel.cs ├── Properties │ └── AssemblyInfo.cs ├── SearchWindow.xaml ├── SearchWindow.xaml.cs ├── Services │ ├── Interface1.cs │ └── ItemActionRegistry.cs └── themes │ └── generic.xaml ├── DitExplorer.sln ├── Extensions └── DitExplorer.CredentialExtraction │ ├── AssemblyInfo.cs │ ├── Credential.cs │ ├── CredentialExtractor.ditextmanifest │ ├── CredentialExtractorViewModel.cs │ ├── CredentialExtractorWindow.xaml │ ├── CredentialExtractorWindow.xaml.cs │ ├── DitExplorer.CredentialExtraction.csproj │ ├── ExtractCredentialsAction.cs │ ├── Messages.Designer.cs │ ├── Messages.resx │ ├── SidExtensions.cs │ └── Structs.cs ├── LICENSE.txt └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | DIT Explorer 4 | TrustedSec 5 | Alex Ball 6 | DIT Explorer 7 | Copyright © 2025 8 | 9 | https://www.github.com/trustedsec/DitExplorer 10 | https://www.github.com/trustedsec/DitExplorer 11 | 12 | -------------------------------------------------------------------------------- /DitExplorer.Core/DitExplorer.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | DitExplorer 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /DitExplorer.Core/IAttributeSchema.cs: -------------------------------------------------------------------------------- 1 | namespace DitExplorer; 2 | 3 | public interface IAttributeSchema : ISchemaObject 4 | { 5 | IAttributeSyntax? Syntax { get; } 6 | bool IsSingleValued { get; } 7 | bool IsLink { get; } 8 | } -------------------------------------------------------------------------------- /DitExplorer.Core/IAttributeSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace DitExplorer; 2 | 3 | public interface IAttributeSyntax 4 | { 5 | Type? AttributeType { get; } 6 | bool CanRetrieveValue { get; } 7 | } -------------------------------------------------------------------------------- /DitExplorer.Core/IClassSchema.cs: -------------------------------------------------------------------------------- 1 | namespace DitExplorer; 2 | 3 | public interface IClassSchema : ISchemaObject 4 | { 5 | IClassSchema? Superclass { get; } 6 | bool HasAttribute(string ldapName); 7 | IAttributeSchema[] GetAttributes(bool includeBaseClasses); 8 | } -------------------------------------------------------------------------------- /DitExplorer.Core/IDirectory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace DitExplorer; 8 | 9 | /// 10 | /// Exposes functionality for interacting with a directory. 11 | /// 12 | public interface IDirectory : IDisposable 13 | { 14 | /// 15 | /// Gets the root domain of the directory. 16 | /// 17 | IDirectoryObject RootDomain { get; } 18 | 19 | /// 20 | /// Gets a class corresponding to an encoded governsID value. 21 | /// 22 | /// Prefix-encoded governsID 23 | /// An with 24 | /// No class found with 25 | IClassSchema GetClassByGovernsId(int governsId); 26 | /// 27 | /// Gets a list of all object classes. 28 | /// 29 | /// An array of objects, one for 30 | /// each class defined in the directory.. 31 | IClassSchema[] GetClassSchemas(); 32 | /// 33 | /// Gets a list of all attribute in the schemae. 34 | /// 35 | /// An array of objects. 36 | IAttributeSchema[] GetAllAttributeSchemas(); 37 | /// 38 | /// Gets an attribute by its LDAP name. 39 | /// 40 | /// LDAP name of attribute 41 | /// The with the LDAP name , if found; 42 | /// otherwise, . 43 | IAttributeSchema? TryGetAttributeByLdapName(string name); 44 | } 45 | -------------------------------------------------------------------------------- /DitExplorer.Core/IDirectoryObject.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace DitExplorer; 3 | 4 | /// 5 | /// Provides functionality for interacting with an object in a directory. 6 | /// 7 | public interface IDirectoryObject 8 | { 9 | /// 10 | /// Gets the name of the object. 11 | /// 12 | public string Name { get; } 13 | string DistinguishedName { get; } 14 | /// 15 | /// Gets the directory containing the object. 16 | /// 17 | IDirectory Directory { get; } 18 | /// 19 | /// Gets the path of the object. 20 | /// 21 | /// 22 | /// The value of this property resembles a file path with ancestor names separated by a backslash. 23 | /// 24 | /// 25 | string ObjectPath { get; } 26 | 27 | IDirectoryObject? Parent { get; } 28 | IDirectoryObject GetChild(string name); 29 | IEnumerable GetChildren(); 30 | object? GetValueOf(IAttributeSchema attribute); 31 | MultiValue GetMultiValuesOf(IAttributeSchema attribute); 32 | object[] GetValueOfMultiple(IList attributes); 33 | IEnumerable SearchSubtree(string? searchName); 34 | IEnumerable SearchSubtree(string? searchName, IClassSchema? objectClass, bool includeSubclasses); 35 | 36 | /// 37 | /// Gets the object class schema. 38 | /// 39 | IClassSchema ObjectClass { get; } 40 | 41 | /// 42 | /// Gets a list of objects that are direct members of this object. 43 | /// 44 | /// 45 | IEnumerable GetMembers(); 46 | /// 47 | /// Gets a list of objects that this object is a member of. 48 | /// 49 | /// 50 | IEnumerable GetMemberOfGroups(); 51 | } -------------------------------------------------------------------------------- /DitExplorer.Core/ISchemaObject.cs: -------------------------------------------------------------------------------- 1 | namespace DitExplorer; 2 | 3 | public interface ISchemaObject : IDirectoryObject 4 | { 5 | string LdapDisplayName { get; } 6 | } -------------------------------------------------------------------------------- /DitExplorer.Core/MultiValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace DitExplorer; 9 | 10 | /// 11 | /// Represents the value of an attribute that can hold multiple values. 12 | /// 13 | public abstract class MultiValue : System.Collections.IEnumerable 14 | { 15 | /// 16 | /// Gets the element type; 17 | /// 18 | public abstract Type ElementType { get; } 19 | 20 | /// 21 | /// Gets the array holding the individual values. 22 | /// 23 | protected abstract Array ValueArray { get; } 24 | 25 | /// 26 | /// Gets the number of elements. 27 | /// 28 | public abstract int Count { get; } 29 | 30 | /// 31 | /// Gets an element at a 0-based index. 32 | /// 33 | /// 0-based index 34 | /// Value at 35 | public abstract object? GetElementAt(int index); 36 | 37 | /// 38 | /// Instantiates a from the values in an array. 39 | /// 40 | /// Array holding values 41 | /// A holding the values of 42 | /// is 43 | public static MultiValue Create(Array array) 44 | { 45 | if (array is null) throw new ArgumentNullException(nameof(array)); 46 | 47 | var elemType = array.GetType().GetElementType(); 48 | var multiType = typeof(MultiValue<>).MakeGenericType(elemType); 49 | var multi = (MultiValue)Activator.CreateInstance(multiType, array, array.Length)!; 50 | return multi; 51 | } 52 | 53 | /// 54 | /// Instantiates a from the values in an array. 55 | /// 56 | /// Array holding values 57 | /// Number of elements in to use 58 | /// A holding the values of 59 | /// is 60 | public static MultiValue Create(Array array, int count) 61 | { 62 | if (array is null) throw new ArgumentNullException(nameof(array)); 63 | 64 | var elemType = array.GetType().GetElementType(); 65 | var multiType = typeof(MultiValue<>).MakeGenericType(elemType); 66 | var multi = (MultiValue)Activator.CreateInstance(multiType, array, count)!; 67 | return multi; 68 | } 69 | 70 | /// 71 | /// Instantiates a from the values in an array. 72 | /// 73 | /// Type of element 74 | /// Array holding values 75 | /// A holding the values of 76 | /// is 77 | public static MultiValue Create(T[] elements) 78 | { 79 | if (elements is null) throw new ArgumentNullException(nameof(elements)); 80 | 81 | return new MultiValue(elements, elements.Length); 82 | } 83 | 84 | /// 85 | /// Gets the element type for a multivalue type. 86 | /// 87 | /// A closed multivalue . 88 | /// Element type of , if it is ; otherwise, 89 | /// is 90 | /// 91 | /// If is not a , this is 92 | /// indicated by a return value of and does not throw an exception. 93 | /// 94 | public static Type? GetElementType(Type multivalueType) 95 | { 96 | if (multivalueType is null) throw new ArgumentNullException(nameof(multivalueType)); 97 | 98 | if (multivalueType.IsGenericType && !multivalueType.IsGenericTypeDefinition) 99 | { 100 | var def = multivalueType.GetGenericTypeDefinition(); 101 | if (def == typeof(MultiValue<>)) 102 | { 103 | return def.GetGenericArguments()[0]; 104 | } 105 | } 106 | return null; 107 | } 108 | 109 | /// 110 | public IEnumerator GetEnumerator() => this.ValueArray.GetEnumerator(); 111 | } 112 | 113 | /// 114 | /// Represents the value of an attribute that can hold multiple values. 115 | /// 116 | /// The type of the attribute 117 | public sealed partial class MultiValue : MultiValue, IEnumerable 118 | { 119 | /// 120 | /// Initializes a new . 121 | /// 122 | /// Values contained in the attribute 123 | /// is 124 | public MultiValue(T[] values, int count) 125 | { 126 | if (values is null) throw new ArgumentNullException(nameof(values)); 127 | if (count > values.Length) throw new ArgumentOutOfRangeException(nameof(count)); 128 | 129 | if (count < values.Length) 130 | Array.Resize(ref values, count); 131 | 132 | this.Values = values; 133 | } 134 | /// 135 | /// Gets the individual values. 136 | /// 137 | public T[] Values { get; } 138 | 139 | /// 140 | protected sealed override Array ValueArray => this.Values; 141 | 142 | /// 143 | public sealed override Type ElementType => throw new NotImplementedException(); 144 | 145 | /// 146 | public sealed override int Count => this.Values.Length; 147 | /// 148 | public sealed override object? GetElementAt(int index) 149 | => this.Values[index]; 150 | 151 | private string? _cachedString; 152 | 153 | /// 154 | public sealed override string ToString() 155 | => (this._cachedString ?? this.BuildString()); 156 | private string BuildString() 157 | { 158 | StringBuilder sb = new StringBuilder(); 159 | foreach (var item in this.Values) 160 | { 161 | if (item != null) 162 | { 163 | var str = item.ToString(); 164 | if (sb.Length > 0) 165 | sb.Append("; "); 166 | sb.Append(str); 167 | } 168 | } 169 | return sb.ToString(); 170 | } 171 | 172 | IEnumerator IEnumerable.GetEnumerator() 173 | { 174 | return ((IEnumerable)Values).GetEnumerator(); 175 | } 176 | } 177 | 178 | partial class MultiValue : IComparable>, IComparable 179 | { 180 | /// 181 | public int CompareTo(MultiValue? other) 182 | { 183 | if (other is null) 184 | return -1; 185 | 186 | var count = Math.Min(this.Values.Length, other.Values.Length); 187 | for (int i = 0; i < count; i++) 188 | { 189 | var x = this.Values[i]; 190 | var y = this.Values[i]; 191 | 192 | int cmp = Comparer.Default.Compare(x, y); 193 | if (cmp != 0) 194 | return cmp; 195 | } 196 | 197 | { 198 | int cmp = this.Values.Length - other.Values.Length; 199 | return cmp; 200 | } 201 | } 202 | 203 | /// 204 | public int CompareTo(object? obj) 205 | { 206 | if (obj is MultiValue other) 207 | return this.CompareTo(other); 208 | else 209 | throw new ArgumentException("Other object is not of the same type.", nameof(other)); 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /DitExplorer.EseInterop/DitExplorer.EseInterop.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | True 16 | True 17 | Messages.resx 18 | 19 | 20 | 21 | 22 | 23 | ResXFileCodeGenerator 24 | Messages.Designer.cs 25 | 26 | 27 | 28 | 29 | 30 | Never 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /DitExplorer.EseInterop/JetColumnInfo.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Isam.Esent.Interop; 2 | using System.Runtime.InteropServices; 3 | using static Microsoft.Isam.Esent.Interop.EnumeratedColumn; 4 | 5 | namespace DitExplorer.EseInterop; 6 | 7 | public class JetColumnInfo 8 | { 9 | internal JetColumnInfo(string tableName, string columnName, JET_COLUMNDEF colInfo) 10 | : this(tableName, columnName, 11 | colInfo.columnid, 12 | (JetColumnType)colInfo.coltyp, 13 | colInfo.cbMax, 14 | colInfo.grbit) 15 | { 16 | } 17 | internal JetColumnInfo(string tableName, string columnName, 18 | JET_COLUMNID columnId, 19 | JetColumnType columnType, 20 | int maxSize, 21 | ColumndefGrbit flags) 22 | { 23 | this.TableName = tableName; 24 | this.ColumnName = columnName; 25 | this.ColumnId = columnId; 26 | this.ColumnType = columnType; 27 | this.MaxSize = maxSize; 28 | this.Flags = flags; 29 | } 30 | 31 | // UNDONE: Prefer MemoryMarshal 32 | //[StructLayout(LayoutKind.Explicit)] 33 | //struct ColumnIdUnion 34 | //{ 35 | // [FieldOffset(0)] 36 | // internal JET_COLUMNID colid; 37 | // [FieldOffset(0)] 38 | // internal int value; 39 | //} 40 | 41 | /// 42 | /// Creates a . 43 | /// 44 | /// Value of column ID 45 | /// A with value 46 | /// 47 | /// doesn't provide a public constructor. 48 | /// 49 | public static JET_COLUMNID MakeColumnId(int value) 50 | => MemoryMarshal.Cast(MemoryMarshal.CreateReadOnlySpan(ref value, 1))[0]; 51 | public static int ValueOf(JET_COLUMNID id) 52 | => MemoryMarshal.Cast(MemoryMarshal.CreateReadOnlySpan(ref id, 1))[0]; 53 | 54 | public string TableName { get; } 55 | public string ColumnName { get; } 56 | 57 | public JET_COLUMNID ColumnId { get; } 58 | public int ColumnIdValue => ValueOf(this.ColumnId); 59 | public JetColumnType ColumnType { get; } 60 | public int MaxSize { get; } 61 | public ColumndefGrbit Flags { get; } 62 | 63 | public bool IsFixedSize => 0 != (this.Flags & ColumndefGrbit.ColumnFixed); 64 | public bool IsTagged => 0 != (this.Flags & ColumndefGrbit.ColumnTagged); 65 | public bool IsNotNull => 0 != (this.Flags & ColumndefGrbit.ColumnNotNULL); 66 | public bool IsVersion => 0 != (this.Flags & ColumndefGrbit.ColumnVersion); 67 | public bool IsMultiValued => 0 != (this.Flags & ColumndefGrbit.ColumnMultiValued); 68 | } -------------------------------------------------------------------------------- /DitExplorer.EseInterop/JetColumnType.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Isam.Esent.Interop; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace DitExplorer.EseInterop; 9 | 10 | /// 11 | /// Specifies the data type of a column in an ESE database. 12 | /// 13 | /// 14 | /// Includes all values from the ESE documentation. 15 | /// doesn't include all values. 16 | /// 17 | public enum JetColumnType 18 | { 19 | Nil = 0, 20 | Bit = 1, 21 | UnsignedByte = 2, 22 | Short = 3, 23 | Long = 4, 24 | Currency = 5, 25 | Single = 6, 26 | Double = 7, 27 | DateTime = 8, 28 | Binary = 9, 29 | Text = 10, 30 | LongBinary = 11, 31 | LongText = 12, 32 | SLV = 13, 33 | UnsignedLong = 14, 34 | LongLong = 15, 35 | Guid = 16, 36 | UnsignedShort = 17, 37 | } 38 | -------------------------------------------------------------------------------- /DitExplorer.EseInterop/JetIndexColumnInfo.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Isam.Esent.Interop; 2 | using Microsoft.Isam.Esent.Interop.Windows8; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace DitExplorer.EseInterop; 10 | public class JetIndexColumnInfo 11 | { 12 | public JetIndexColumnInfo( 13 | string indexName, 14 | JetIndexColumnGrbit flags, 15 | int numberOfKeys, 16 | int entries, 17 | int pages, 18 | int columnCount, 19 | int columnIndex, 20 | JET_COLUMNID columnId, 21 | JetColumnType columnType, 22 | short country, 23 | short languageId, 24 | short codePage, 25 | short collate, 26 | int columnFlags, 27 | string columnName, 28 | int lCMapFlags) 29 | { 30 | IndexName = indexName; 31 | Flags = flags; 32 | NumberOfKeys = numberOfKeys; 33 | Entries = entries; 34 | Pages = pages; 35 | ColumnCount = columnCount; 36 | ColumnIndex = columnIndex; 37 | ColumnId = columnId; 38 | ColumnType = columnType; 39 | Country = country; 40 | LanguageId = languageId; 41 | CodePage = codePage; 42 | Collate = collate; 43 | ColumnFlags = columnFlags; 44 | ColumnName = columnName; 45 | LCMapFlags = lCMapFlags; 46 | } 47 | 48 | public string IndexName { get; } 49 | public JetIndexColumnGrbit Flags { get; } 50 | public int NumberOfKeys { get; } 51 | public int Entries { get; } 52 | public int Pages { get; } 53 | public int ColumnCount { get; } 54 | public int ColumnIndex { get; } 55 | public JET_COLUMNID ColumnId { get; } 56 | public JetColumnType ColumnType { get; } 57 | public short Country { get; } 58 | public short LanguageId { get; } 59 | public short CodePage { get; } 60 | public short Collate { get; } 61 | public int ColumnFlags { get; } 62 | public string ColumnName { get; } 63 | public int LCMapFlags { get; } 64 | } 65 | -------------------------------------------------------------------------------- /DitExplorer.EseInterop/JetInstance.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Isam.Esent.Interop; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace DitExplorer.EseInterop; 9 | public partial class JetInstance 10 | { 11 | private JET_INSTANCE _inst; 12 | 13 | public static JetInstance Initialize() 14 | { 15 | JetInstance instance = new JetInstance(); 16 | Api.JetInit(ref instance._inst); 17 | return instance; 18 | } 19 | 20 | public JetSession BeginSession() 21 | { 22 | Api.JetBeginSession(this._inst, out var sesid, null, null); 23 | return new JetSession(sesid); 24 | } 25 | } 26 | partial class JetInstance : IDisposable 27 | { 28 | private bool disposedValue; 29 | 30 | protected virtual void Dispose(bool disposing) 31 | { 32 | if (!disposedValue) 33 | { 34 | Api.JetTerm(this._inst); 35 | 36 | disposedValue = true; 37 | } 38 | } 39 | 40 | // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources 41 | // ~JetInstance() 42 | // { 43 | // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 44 | // Dispose(disposing: false); 45 | // } 46 | 47 | public void Dispose() 48 | { 49 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 50 | Dispose(disposing: true); 51 | GC.SuppressFinalize(this); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /DitExplorer.EseInterop/JetSession.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Isam.Esent.Interop; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using static System.Collections.Specialized.BitVector32; 8 | 9 | namespace DitExplorer.EseInterop; 10 | 11 | public enum OpenDatabaseOptions 12 | { 13 | None = 0, 14 | ReadOnly = 1, 15 | } 16 | 17 | public partial class JetSession 18 | { 19 | internal readonly JET_SESID sesid; 20 | 21 | internal JetSession(JET_SESID sesid) 22 | { 23 | this.sesid = sesid; 24 | } 25 | 26 | public static JetSession Begin() 27 | { 28 | Api.JetBeginSession(JET_INSTANCE.Nil, out var sesid, null, null); 29 | return new JetSession(sesid); 30 | } 31 | 32 | public JetDatabase AttachAndOpen(string fileName, OpenDatabaseOptions options) 33 | { 34 | this.VerifyNotDisposed(); 35 | 36 | AttachDatabaseGrbit attachFlags = (0 != (options & OpenDatabaseOptions.ReadOnly)) ? AttachDatabaseGrbit.ReadOnly : AttachDatabaseGrbit.None; 37 | Api.JetAttachDatabase(this.sesid, fileName, attachFlags); 38 | 39 | try 40 | { 41 | OpenDatabaseGrbit openFlags = (0 != (options & OpenDatabaseOptions.ReadOnly)) ? OpenDatabaseGrbit.ReadOnly : OpenDatabaseGrbit.None; 42 | Api.JetOpenDatabase(this.sesid, fileName, null, out var dbid, openFlags); 43 | var db = new JetDatabase(this.sesid, dbid, fileName); 44 | fileName = null; 45 | return db; 46 | } 47 | finally 48 | { 49 | if (fileName != null) 50 | Api.JetDetachDatabase(this.sesid, fileName); 51 | } 52 | } 53 | 54 | 55 | public JetCursor IntersectAll(JetCursor[] cursors) 56 | { 57 | this.VerifyNotDisposed(); 58 | 59 | if (cursors == null || cursors.Length == 0 || (Array.IndexOf(cursors, null) >= 0)) 60 | throw new ArgumentNullException(nameof(cursors)); 61 | if (Array.FindIndex(cursors, r => r.sesid != this.sesid) >= 0) 62 | throw new ArgumentException("One or more of the cursors belong to another session. This is not allowed."); 63 | if (Array.FindIndex(cursors, r => r.IsDisposed) >= 0) 64 | throw new ArgumentException("One or more of the cursors are disposed."); 65 | 66 | var ranges = Array.ConvertAll(cursors, r => new JET_INDEXRANGE() { tableid = r.tableId, grbit = IndexRangeGrbit.RecordInIndex }); 67 | Api.JetIntersectIndexes(this.sesid, ranges, ranges.Length, out var reclist, IntersectIndexesGrbit.None); 68 | using (JetCursor curIntersect = new JetCursor(this.sesid, reclist.tableid)) 69 | { 70 | throw new NotImplementedException(); 71 | } 72 | } 73 | } 74 | 75 | partial class JetSession : IDisposable 76 | { 77 | private bool disposedValue; 78 | protected void VerifyNotDisposed() 79 | { 80 | if (this.disposedValue) 81 | throw new ObjectDisposedException(Messages.JetObjectDisposedMessage); 82 | } 83 | 84 | protected virtual void Dispose(bool disposing) 85 | { 86 | if (!disposedValue) 87 | { 88 | Api.JetEndSession(this.sesid, EndSessionGrbit.None); 89 | disposedValue = true; 90 | } 91 | } 92 | 93 | // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources 94 | ~JetSession() 95 | { 96 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 97 | Dispose(disposing: false); 98 | } 99 | 100 | public void Dispose() 101 | { 102 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 103 | Dispose(disposing: true); 104 | GC.SuppressFinalize(this); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /DitExplorer.EseInterop/JetTableInfo.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Isam.Esent.Interop; 2 | 3 | namespace DitExplorer.EseInterop; 4 | 5 | /// 6 | /// Represents a table within an ESE database. 7 | /// 8 | public class JetTableInfo 9 | { 10 | private JetDatabase _db; 11 | 12 | internal JetTableInfo(string name, JetDatabase db, JET_OBJECTINFO objInfo) 13 | :this(name,db, 14 | recordCount: objInfo.cRecord, 15 | pagesUsed: objInfo.cPage, 16 | flags: objInfo.flags 17 | ) 18 | { 19 | } 20 | 21 | internal JetTableInfo(string name, JetDatabase db, 22 | int recordCount, 23 | int pagesUsed, 24 | ObjectInfoFlags flags 25 | ) 26 | { 27 | this.TableName = name; 28 | this._db = db; 29 | this.RecordCount = recordCount; 30 | this.PagesUsed = pagesUsed; 31 | this.Flags = flags; 32 | } 33 | 34 | /// 35 | /// Gets the name of the table. 36 | /// 37 | public string TableName { get; } 38 | 39 | /// 40 | /// Gets the number of records in the table. 41 | /// 42 | public int RecordCount { get; } 43 | 44 | /// 45 | /// Gets the number of pages used by the table. 46 | /// 47 | public int PagesUsed { get; } 48 | 49 | /// 50 | /// Gets a applied to the object. 51 | /// 52 | public ObjectInfoFlags Flags { get; } 53 | 54 | public JET_COLUMNID GetColumnId(string columnName) 55 | { 56 | return _db.GetColumnId(TableName, columnName); 57 | } 58 | 59 | public JetColumnInfo GetColumnInfo(string columnName) 60 | { 61 | return _db.GetColumnInfo(TableName, columnName); 62 | } 63 | } -------------------------------------------------------------------------------- /DitExplorer.EseInterop/Messages.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace DitExplorer.EseInterop { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Messages { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Messages() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DitExplorer.EseInterop.Messages", typeof(Messages).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to The cursor has no current record.. 65 | /// 66 | internal static string JetCursor_NoCurrentRecord { 67 | get { 68 | return ResourceManager.GetString("JetCursor_NoCurrentRecord", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to The value in the column was not the expected size.. 74 | /// 75 | internal static string JetCursor_UnexpectedValueSize { 76 | get { 77 | return ResourceManager.GetString("JetCursor_UnexpectedValueSize", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to The object is disposed and can no longer be used.. 83 | /// 84 | internal static string JetObjectDisposedMessage { 85 | get { 86 | return ResourceManager.GetString("JetObjectDisposedMessage", resourceCulture); 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /DitExplorer.EseInterop/Messages.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | The cursor has no current record. 122 | 123 | 124 | The value in the column was not the expected size. 125 | 126 | 127 | The object is disposed and can no longer be used. 128 | 129 | -------------------------------------------------------------------------------- /DitExplorer.EseInterop/esent.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustedsec/DitExplorer/693508e9fdd282fe6db73dd1cab571341e9cf733/DitExplorer.EseInterop/esent.dll -------------------------------------------------------------------------------- /DitExplorer.Extensibility/DitExplorer.Extensibility.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0-windows7.0 5 | enable 6 | enable 7 | true 8 | DitExplorer 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /DitExplorer.Extensibility/DitExtensionManifest.xsd: -------------------------------------------------------------------------------- 1 |  2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /DitExplorer.Extensibility/IDirectoryNode.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.Ntds; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace DitExplorer; 9 | 10 | public interface IDirectoryView 11 | { 12 | IDirectory Directory { get; } 13 | } 14 | 15 | public interface IDirectoryNode 16 | { 17 | IDirectoryObject Object { get; } 18 | IDirectoryView DirectoryView { get; } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /DitExplorer.Extensibility/IItemActionContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | 8 | namespace DitExplorer; 9 | public interface IItemActionContext 10 | { 11 | Window? Owner { get; } 12 | } 13 | -------------------------------------------------------------------------------- /DitExplorer.Extensibility/IItemActionRegistry.cs: -------------------------------------------------------------------------------- 1 | namespace DitExplorer; 2 | 3 | public interface IItemActionRegistry 4 | { 5 | IList GetActionsFor( 6 | object[] items, 7 | IItemActionContext actionContext 8 | ); 9 | void RegisterAction(ItemAction action); 10 | } -------------------------------------------------------------------------------- /DitExplorer.Extensibility/ItemAction.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Input; 2 | 3 | namespace DitExplorer 4 | { 5 | /// 6 | /// Represents an action that may be performed on one or more objects. 7 | /// 8 | /// 9 | /// If the action only applies to a single item at a time, derive from . 10 | /// 11 | public abstract class ItemAction 12 | { 13 | /// 14 | /// Gets the text displayed in the menu. 15 | /// 16 | /// 17 | /// The text should be localized for the current culture and may contain an underscore to denote the access key. 18 | /// 19 | public abstract string MenuText { get; } 20 | /// 21 | /// Determines whether the action can be executed on a set of items. 22 | /// 23 | /// Items to test 24 | /// if this action can execute on the provided items; otherwise, . 25 | public abstract bool CanExecute(object[] items, IItemActionContext context); 26 | public abstract void Execute(object[] items, IItemActionContext context); 27 | } 28 | 29 | public abstract class SingleItemAction : ItemAction 30 | { 31 | public sealed override bool CanExecute(object[] items, IItemActionContext context) 32 | { 33 | return (items is not null) && (items.Length == 1) && items[0] is T typed && CanExecute(typed, context); 34 | } 35 | 36 | public abstract bool CanExecute(T items, IItemActionContext context); 37 | 38 | public sealed override void Execute(object[] items, IItemActionContext context) 39 | { 40 | this.Execute((T)items[0], context); 41 | } 42 | 43 | public abstract void Execute(T item, IItemActionContext context); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /DitExplorer.Ntds/ADInstanceType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace DitExplorer.Ntds; 8 | 9 | /// 10 | /// Specifies instance type flags. 11 | /// 12 | /// 13 | /// These are documented in Active Directory schema. 14 | /// 15 | [Flags] 16 | public enum ADInstanceType 17 | { 18 | None = 0, 19 | 20 | /// 21 | /// Head of naming context 22 | /// 23 | HeadOfNamingContext = 1, 24 | /// 25 | /// Replica is not instantiated 26 | /// 27 | ReplicaNotInstantiated = 2, 28 | /// 29 | /// Object is writable on this directory 30 | /// 31 | Writable = 4, 32 | /// 33 | /// Naming context above this one on this directory is held 34 | /// 35 | ParentNCHeld = 8, 36 | /// 37 | /// Naming context is in the process of being constructed for the first time by using replication 38 | /// 39 | NCUnderConstruction = 0x10, 40 | /// 41 | /// Naming context is in the process of being removed from the local DSA 42 | /// 43 | NCDeleting = 0x20, 44 | }; 45 | -------------------------------------------------------------------------------- /DitExplorer.Ntds/ADSearchFlags.cs: -------------------------------------------------------------------------------- 1 | namespace DitExplorer.Ntds; 2 | 3 | // REF: https://learn.microsoft.com/en-us/windows/win32/adschema/a-searchflags 4 | [Flags] 5 | public enum ADSearchFlags 6 | { 7 | None = 0, 8 | 9 | Indexed = 1, 10 | IndexedPerContainer = 2, 11 | Anr = 4, 12 | TombstonePreserve = 8, 13 | CopyWithObject = 0x10, 14 | TupleIndex = 0x20, 15 | IndexedVlv = 0x40, 16 | Confidential = 0x80, 17 | } 18 | -------------------------------------------------------------------------------- /DitExplorer.Ntds/ArgumentHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace DitExplorer.Ntds; 9 | static class ArgumentHelper 10 | { 11 | public static T ThrowIfNot(object? value, [CallerArgumentExpression(nameof(value))] string? expression = null) 12 | where T : class 13 | => (value is T typed) ? typed : throw new ArgumentException("The argument is of the wrong type.", expression); 14 | public static T? ThrowIfNotNullAndNot(object? value, [CallerArgumentExpression(nameof(value))] string? expression = null) 15 | where T : class 16 | => (value is null) ? null 17 | : (value is T typed) ? typed : throw new ArgumentException("The argument is of the wrong type.", expression); 18 | public static IList ThrowIfNot(System.Collections.IList value, [CallerArgumentExpression(nameof(value))] string? expression = null) 19 | where T : class 20 | { 21 | if (!value.OfType().All(r => r is T)) 22 | throw new ArgumentException("Not all values in the list are of the required type.", expression); 23 | 24 | return value.OfType().ToArray(); 25 | } 26 | public static IList ThrowIfNot(this IList value, [CallerArgumentExpression(nameof(value))] string? expression = null) 27 | where TSource : class 28 | where TRequired : TSource 29 | { 30 | if (!value.All(r => r is TRequired)) 31 | throw new ArgumentException("Not all values in the list are of the required type.", expression); 32 | 33 | return value.Cast().ToArray(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /DitExplorer.Ntds/AttributeSyntax.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.EseInterop; 2 | using Microsoft.Isam.Esent.Interop; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace DitExplorer.Ntds; 10 | 11 | /// 12 | /// Represents an attribute syntax. 13 | /// 14 | /// 15 | /// The syntax of an attribute determines how its value is encoded and interpreted. 16 | /// 17 | /// For more information, see the documentation. 18 | /// 19 | /// 20 | public class AttributeSyntax : IAttributeSyntax 21 | { 22 | /// 23 | /// Function that retrieves the raw value from a cursor. 24 | /// 25 | internal readonly Func retrieveFunc; 26 | /// 27 | /// Function that converts the raw value to the presentable value. 28 | /// 29 | /// 30 | /// If no function is present, this means the raw value doesn't require conversion and is returned as-is. 31 | /// 32 | internal readonly Func? decodeFunc; 33 | /// 34 | /// Function to convert from 35 | /// 36 | private readonly Action setFunc; 37 | 38 | internal AttributeSyntax( 39 | Type attributeType, 40 | string ldapName, 41 | string syntaxId, 42 | int prefixEncodedId, 43 | int omSyntax, 44 | string? omObjectClass, 45 | Func retrieveFunc, 46 | Func? decodeFunc, 47 | Action setFunc 48 | ) 49 | { 50 | this.AttributeType = attributeType; 51 | LdapName = ldapName; 52 | SyntaxId = syntaxId; 53 | PrefixEncodedId = prefixEncodedId; 54 | OmSyntax = omSyntax; 55 | OmObjectClass = omObjectClass; 56 | this.retrieveFunc = retrieveFunc; 57 | this.decodeFunc = decodeFunc; 58 | this.setFunc = setFunc; 59 | } 60 | 61 | /// 62 | /// Gets a value indicating whether the implementation can retrieve a value with this syntax. 63 | /// 64 | public bool CanRetrieveValue => (this.retrieveFunc != null); 65 | 66 | /// 67 | /// Gets the type of value held by this attribute. 68 | /// 69 | public Type AttributeType { get; } 70 | /// 71 | /// Gets encoded ID of the syntax. 72 | /// 73 | public int RawId { get; } 74 | /// 75 | /// Gets the LDAP name of the syntax 76 | /// 77 | public string LdapName { get; } 78 | /// 79 | /// Gets the syntax ID. 80 | /// 81 | public string SyntaxId { get; } 82 | /// 83 | /// Gets the prefix-encoded form of the syntax ID. 84 | /// 85 | public int PrefixEncodedId { get; } 86 | 87 | /// 88 | /// Gets the omSyntax value. 89 | /// 90 | public int OmSyntax { get; } 91 | /// 92 | /// Gets the omObjectClass, if any. 93 | /// 94 | public string? OmObjectClass { get; } 95 | 96 | internal void SetRawValueOn(JetCursor cursor, JET_COLUMNID colid, object? value) 97 | { 98 | if (value is null) 99 | { 100 | // TODO: Check if the schema should allow this 101 | cursor.SetNull(colid); 102 | } 103 | else 104 | { 105 | 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /DitExplorer.Ntds/AttributeSystemFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace DitExplorer.Ntds; 8 | 9 | // REF: https://learn.microsoft.com/en-us/windows/win32/adschema/a-systemflags 10 | [Flags] 11 | public enum AttributeSystemFlags : uint 12 | { 13 | None = 0, 14 | 15 | NotReplicated = 1, 16 | ReplicatedToGC = 2, 17 | Constructed = 4, 18 | BaseSchema = 0x10, 19 | DeletedImmediately = 0x02000000, 20 | Unmovable = 0x04000000, 21 | Unrenamable = 0x08000000, 22 | ConfCanMoveWithRestrictions = 0x10000000, 23 | ConfCanMove = 0x20000000, 24 | ConfCanRenameWithRestrictions = 0x40000000, 25 | CannotDelete = 0x80000000, 26 | } 27 | -------------------------------------------------------------------------------- /DitExplorer.Ntds/CollectionHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace DitExplorer.Ntds; 9 | internal static class CollectionHelpers 10 | { 11 | public static void AddRange(this HashSet hashset, IEnumerable values) 12 | { 13 | foreach (var item in values) 14 | { 15 | hashset.Add(item); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DitExplorer.Ntds/DirectoryObjectNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace DitExplorer.Ntds; 8 | 9 | [Serializable] 10 | public class DirectoryObjectNotFoundException : Exception 11 | { 12 | public DirectoryObjectNotFoundException() { } 13 | public DirectoryObjectNotFoundException(string message) : base(message) { } 14 | public DirectoryObjectNotFoundException(string message, Exception inner) : base(message, inner) { } 15 | protected DirectoryObjectNotFoundException( 16 | System.Runtime.Serialization.SerializationInfo info, 17 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 18 | } -------------------------------------------------------------------------------- /DitExplorer.Ntds/DitExplorer.Ntds.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | True 17 | True 18 | Messages.resx 19 | 20 | 21 | 22 | 23 | 24 | ResXFileCodeGenerator 25 | Messages.Designer.cs 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /DitExplorer.Ntds/Messages.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace DitExplorer.Ntds { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Messages { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Messages() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DitExplorer.Ntds.Messages", typeof(Messages).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Cannot retrieve the attribute because it does not exist in the database.. 65 | /// 66 | internal static string Attribute_NotInDatabase { 67 | get { 68 | return ResourceManager.GetString("Attribute_NotInDatabase", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to The attribute cannot be retrieved because the syntax is not supported.. 74 | /// 75 | internal static string Attribute_SyntaxNotSupported { 76 | get { 77 | return ResourceManager.GetString("Attribute_SyntaxNotSupported", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to Unable to open the directory. No root domain was provided, and detection of the root domain failed.. 83 | /// 84 | internal static string Directory_NoRootFound { 85 | get { 86 | return ResourceManager.GetString("Directory_NoRootFound", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to The attribute cannot be used with this method because it is a single-valued attribute.. 92 | /// 93 | internal static string DirectoryObject_AttrIsSingleValued { 94 | get { 95 | return ResourceManager.GetString("DirectoryObject_AttrIsSingleValued", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to The object is disposed and can no longer be used.. 101 | /// 102 | internal static string ObjectDisposedMessage { 103 | get { 104 | return ResourceManager.GetString("ObjectDisposedMessage", resourceCulture); 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /DitExplorer.Ntds/Messages.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Cannot retrieve the attribute because it does not exist in the database. 122 | 123 | 124 | The attribute cannot be retrieved because the syntax is not supported. 125 | 126 | 127 | The attribute cannot be used with this method because it is a single-valued attribute. 128 | 129 | 130 | Unable to open the directory. No root domain was provided, and detection of the root domain failed. 131 | 132 | 133 | The object is disposed and can no longer be used. 134 | 135 | -------------------------------------------------------------------------------- /DitExplorer.Ntds/NtdsAttributeSchema.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Isam.Esent.Interop; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace DitExplorer.Ntds; 9 | 10 | /// 11 | /// Represents an attribute in the directory schema. 12 | /// 13 | public class NtdsAttributeSchema : NtdsSchemaObject, IAttributeSchema 14 | { 15 | internal NtdsAttributeSchema(NtdsDirectory dir, int dnt, int parentDnt) : base(dir, dnt, parentDnt) 16 | { 17 | } 18 | 19 | /// 20 | /// Gets the encoded attribute ID. 21 | /// 22 | /// 23 | /// For more information on how attributes are encoded, see the documentation. 24 | /// 25 | public int AttributeIdRaw { get; internal set; } 26 | 27 | private string? _rdnToken; 28 | /// 29 | /// Gets the name of this token when it appears in a DN or RDN. 30 | /// 31 | public string RdnToken => (this._rdnToken ??= this.LdapDisplayName?.ToUpper()); 32 | 33 | /// 34 | /// Gets the name of the column for this attribute in NTDS.dit 35 | /// 36 | public string? ColumnName { get; internal set; } 37 | /// 38 | /// Gets the ID of the column for this attribute in NTDS.dit 39 | /// 40 | internal JET_COLUMNID ColumnId { get; set; } 41 | /// 42 | /// Gets a value indicating whether the column exists in the database. 43 | /// 44 | public bool ExistsInDatabase => (this.ColumnId != JET_COLUMNID.Nil); 45 | 46 | /// 47 | /// Gets the prefix-encoded attribute syntax ID. 48 | /// 49 | public int AttributeSyntaxIdPrefixEncoded { get; internal set; } 50 | /// 51 | /// Gets the OM syntax. 52 | /// 53 | public int OmSyntax { get; internal set; } 54 | 55 | /// 56 | /// Gets the syntax of the attribute. 57 | /// 58 | /// 59 | /// The syntax of the attribute determines how the value is encoded. 60 | /// 61 | public AttributeSyntax Syntax { get; internal set; } 62 | /// 63 | IAttributeSyntax IAttributeSchema.Syntax => this.Syntax; 64 | 65 | /// 66 | /// Gets a value indicating whether the attribute only holds a single value. 67 | /// 68 | public bool IsSingleValued { get; internal set; } 69 | /// 70 | /// Gets the link ID used for this attribute. 71 | /// 72 | public int LinkId { get; internal set; } 73 | /// 74 | /// Gets a value indicating whether this attribute holds links to other objects. 75 | /// 76 | public bool IsLink => this.LinkId != 0; 77 | /// 78 | /// Gets the search flags of this attribute. 79 | /// 80 | public ADSearchFlags SearchFlags { get; internal set; } 81 | /// 82 | /// Gets a value indicating whether this attribute participates in Ambiguous Name Resolution. 83 | /// 84 | public bool UsedForAnr => (0 != (this.SearchFlags & ADSearchFlags.Anr)); 85 | } 86 | -------------------------------------------------------------------------------- /DitExplorer.Ntds/NtdsClassSchema.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | namespace DitExplorer.Ntds; 4 | 5 | /// 6 | /// Represents an object class in the directory. 7 | /// 8 | public sealed class NtdsClassSchema : NtdsSchemaObject, IClassSchema 9 | { 10 | internal NtdsClassSchema(NtdsDirectory dir, int dnt, int parentDnt) : base(dir, dnt, parentDnt) 11 | { 12 | } 13 | 14 | /// 15 | /// Gets the encoded form of the governsID attribute. 16 | /// 17 | public int GovernsIdRaw { get; internal set; } 18 | /// 19 | /// Gets the DNT of the superclass of this class. 20 | /// 21 | internal int SubclassOfId { get; set; } 22 | 23 | internal int[] AuxiliaryClassIds { get; set; } 24 | internal int[] SystemAuxiliaryClassIds { get; set; } 25 | internal int[] MustContainIds { get; set; } 26 | internal int[] SystemMustContainIds { get; set; } 27 | internal int[] MayContainIds { get; set; } 28 | internal int[] SystemMayContainIds { get; set; } 29 | 30 | public NtdsClassSchema? SuperClass { get; internal set; } 31 | IClassSchema? IClassSchema.Superclass => this.SuperClass; 32 | 33 | public IAttributeSchema[] GetAttributes(bool includeBaseClasses) 34 | { 35 | var attrIds = this.GetAttributeIds(includeBaseClasses); 36 | var attrs = Array.ConvertAll(attrIds, r => this.Directory.GetAttributeById(r)); 37 | return attrs; 38 | } 39 | public int[] GetAttributeIds(bool includeBaseClasses) 40 | { 41 | // TODO: Cache for later 42 | 43 | HashSet attributes = new HashSet(); 44 | if (!includeBaseClasses) 45 | { 46 | GetAttributesInto(attributes); 47 | return attributes.ToArray(); 48 | } 49 | 50 | HashSet classDnts = new HashSet(); 51 | Queue baseClassQueue = new Queue(); 52 | baseClassQueue.Enqueue(this); 53 | 54 | while (baseClassQueue.Count > 0) 55 | { 56 | var objcls = baseClassQueue.Dequeue(); 57 | if (objcls != null && classDnts.Add(objcls.Dnt)) 58 | { 59 | objcls.GetAttributesInto(attributes); 60 | baseClassQueue.Enqueue(objcls.SuperClass); 61 | 62 | foreach (var auxClassId in this.AuxiliaryClassIds) 63 | { 64 | var auxcls = this.Directory.GetClassByGovernsId(auxClassId); 65 | if (auxcls != null) 66 | baseClassQueue.Enqueue(auxcls); 67 | } 68 | } 69 | } 70 | 71 | return attributes.ToArray(); 72 | } 73 | 74 | private void GetAttributesInto(HashSet attributes) 75 | { 76 | attributes.AddRange(this.SystemMustContainIds); 77 | attributes.AddRange(this.MustContainIds); 78 | attributes.AddRange(this.SystemMayContainIds); 79 | attributes.AddRange(this.MayContainIds); 80 | } 81 | 82 | public bool HasAttribute(string ldapName) 83 | { 84 | var attr = this.Directory.TryGetAttributeByLdapName(ldapName); 85 | if (attr == null) 86 | return false; 87 | 88 | var objcls = this; 89 | HashSet attrIds = new HashSet(); 90 | do 91 | { 92 | objcls.GetAttributesInto(attrIds); 93 | if (attrIds.Contains(attr.AttributeIdRaw)) 94 | return true; 95 | 96 | objcls = objcls.SuperClass; 97 | } while (objcls is not null); 98 | 99 | return false; 100 | } 101 | } -------------------------------------------------------------------------------- /DitExplorer.Ntds/NtdsDirectoryEnumerator.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.EseInterop; 2 | using Microsoft.Isam.Esent.Interop; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace DitExplorer.Ntds; 11 | 12 | public class DirectoryEnumerable : IEnumerable 13 | { 14 | private readonly NtdsDirectory _dir; 15 | private readonly Func _cursorFactory; 16 | 17 | internal DirectoryEnumerable(NtdsDirectory dir, Func cursorFactory) 18 | { 19 | _dir = dir; 20 | _cursorFactory = cursorFactory; 21 | } 22 | 23 | public IEnumerator GetEnumerator() 24 | { 25 | return new NtdsDirectoryEnumerator(_dir, _cursorFactory(_dir)); 26 | } 27 | 28 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 29 | } 30 | 31 | public class NtdsDirectoryEnumerator : IEnumerator 32 | { 33 | private readonly NtdsDirectory _dir; 34 | private readonly JetCursor _cursor; 35 | 36 | internal NtdsDirectoryEnumerator(NtdsDirectory dir, JetCursor cursor) 37 | { 38 | _dir = dir; 39 | _cursor = cursor; 40 | Current = null; 41 | } 42 | 43 | /// 44 | public NtdsDirectoryObject? Current { get; private set; } 45 | 46 | /// 47 | object IEnumerator.Current => Current; 48 | 49 | /// 50 | public void Dispose() 51 | { 52 | _cursor.Dispose(); 53 | } 54 | /// 55 | public bool MoveNext() 56 | { 57 | // This method is called before retrieving an item. However, 58 | // The cursor is already positioned at the "next" item. 59 | 60 | if (!_cursor.HasCurrentRecord) 61 | { 62 | Current = null; 63 | return false; 64 | } 65 | 66 | Current = _dir.GetObjectFromRecord(_cursor); 67 | _cursor.MoveNext(); 68 | 69 | return true; 70 | } 71 | 72 | /// 73 | public void Reset() 74 | => throw new NotSupportedException(); 75 | } -------------------------------------------------------------------------------- /DitExplorer.Ntds/NtdsDirectoryLinkEnumerator.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.EseInterop; 2 | using Microsoft.Isam.Esent.Interop; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace DitExplorer.Ntds; 11 | 12 | public class NtdsDirectoryLinkEnumerable : IEnumerable 13 | { 14 | private readonly NtdsDirectory _dir; 15 | private readonly Func _cursorFactory; 16 | private readonly JET_COLUMNID _dntColid; 17 | 18 | internal NtdsDirectoryLinkEnumerable(NtdsDirectory dir, Func cursorFactory, JET_COLUMNID dntColid) 19 | { 20 | _dir = dir; 21 | _cursorFactory = cursorFactory; 22 | _dntColid = dntColid; 23 | } 24 | 25 | public IEnumerator GetEnumerator() 26 | { 27 | return new NtdsDirectoryLinkEnumerator(_dir, _cursorFactory(_dir), this._dntColid); 28 | } 29 | 30 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 31 | } 32 | 33 | public class NtdsDirectoryLinkEnumerator : IEnumerator 34 | { 35 | private readonly NtdsDirectory _dir; 36 | private readonly JetCursor _cursor; 37 | private readonly JET_COLUMNID _dntColid; 38 | 39 | internal NtdsDirectoryLinkEnumerator(NtdsDirectory dir, JetCursor cursor, JET_COLUMNID dntColid) 40 | { 41 | _dir = dir; 42 | _cursor = cursor; 43 | _dntColid = dntColid; 44 | Current = null; 45 | } 46 | 47 | /// 48 | public NtdsDirectoryObject? Current { get; private set; } 49 | 50 | /// 51 | object IEnumerator.Current => Current; 52 | 53 | /// 54 | public void Dispose() 55 | { 56 | _cursor.Dispose(); 57 | } 58 | /// 59 | public bool MoveNext() 60 | { 61 | // This method is called before retrieving an item. However, 62 | // The cursor is already positioned at the "next" item. 63 | 64 | if (!_cursor.HasCurrentRecord) 65 | { 66 | Current = null; 67 | return false; 68 | } 69 | 70 | var dnt = _cursor.ReadInt32(this._dntColid).Value; 71 | Current = _dir.GetByDnt(dnt); 72 | _cursor.MoveNext(); 73 | 74 | return true; 75 | } 76 | 77 | /// 78 | public void Reset() 79 | => throw new NotSupportedException(); 80 | } -------------------------------------------------------------------------------- /DitExplorer.Ntds/NtdsDirectoryObjectReference.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace DitExplorer.Ntds; 8 | public class NtdsDirectoryObjectReference 9 | { 10 | internal NtdsDirectoryObjectReference(NtdsDirectory directory, int dnt) 11 | { 12 | Directory = directory; 13 | Dnt = dnt; 14 | } 15 | 16 | public NtdsDirectory Directory { get; } 17 | public int Dnt { get; } 18 | 19 | private NtdsDirectoryObject? _target; 20 | public NtdsDirectoryObject Target => (this._target ??= this.Directory.GetByDnt(this.Dnt)); 21 | 22 | public override string ToString() => this.Target?.DistinguishedName; 23 | } 24 | -------------------------------------------------------------------------------- /DitExplorer.Ntds/NtdsSchemaObject.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace DitExplorer.Ntds; 3 | 4 | /// 5 | /// Represents a schema object. 6 | /// 7 | public abstract class NtdsSchemaObject : NtdsDirectoryObject, ISchemaObject 8 | { 9 | internal NtdsSchemaObject(NtdsDirectory dir, int dnt, int parentDnt) : base(dir, dnt, parentDnt) 10 | { 11 | } 12 | 13 | /// 14 | public sealed override string ToString() => this.LdapDisplayName; 15 | 16 | /// 17 | /// Gets the LDAP display name of the object. 18 | /// 19 | public string? LdapDisplayName { get; internal set; } 20 | 21 | /// 22 | /// Gets the admin description. 23 | /// 24 | public string? AdminDescription { get; internal set; } 25 | } 26 | -------------------------------------------------------------------------------- /DitExplorer.UI.Core/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /DitExplorer.UI.Core/Behaviors/CheckedList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | using System.Windows.Input; 10 | 11 | namespace DitExplorer.UI.Behaviors; 12 | public static class CheckedList 13 | { 14 | #region IsCheckingEnabled 15 | public static bool GetIsCheckingEnabled(DependencyObject obj) 16 | => (bool)obj.GetValue(InCheckingEnabledProperty); 17 | public static void SetIsCheckingEnabled(DependencyObject obj, bool value) 18 | => obj.SetValue(InCheckingEnabledProperty, value); 19 | public static readonly DependencyProperty InCheckingEnabledProperty = 20 | DependencyProperty.RegisterAttached("IsCheckingEnabled", typeof(bool), typeof(CheckedList), new PropertyMetadata(false, OnEnabledChanged)); 21 | 22 | private static readonly object? NoValue = new object(); 23 | private static object? TryGetPropertyOnItem(object item, string propertyName) 24 | { 25 | PropertyDescriptor? prop = TryGetProperty(item, propertyName); 26 | if (prop != null) 27 | { 28 | var value = prop.GetValue(item); 29 | return value; 30 | } 31 | else 32 | return NoValue; 33 | } 34 | 35 | private static PropertyDescriptor? TryGetProperty(object item, string propertyName) 36 | { 37 | var props = TypeDescriptor.GetProperties(item); 38 | var prop = props.Find(propertyName, false); 39 | return prop; 40 | } 41 | 42 | private static void TrySetPropertyOnItem(object item, string propertyName, object? value) 43 | { 44 | PropertyDescriptor? prop = TryGetProperty(item, propertyName); 45 | if (prop != null) 46 | prop.SetValue(item, value); 47 | } 48 | 49 | private static void ListView_KeyDown(object sender, KeyEventArgs e) 50 | { 51 | if (e.Key == Key.Space) 52 | { 53 | ListView lvw = (ListView)sender; 54 | var propName = GetIsCheckedProperty(lvw); 55 | 56 | if (propName != null) 57 | { 58 | // Check the current state 59 | 60 | bool anyChecked = false; 61 | bool anyUnchecked = false; 62 | 63 | foreach (var item in lvw.SelectedItems) 64 | if (item != null) 65 | { 66 | var value = TryGetPropertyOnItem(item, propName) as bool?; 67 | if (value.HasValue) 68 | { 69 | anyChecked |= value.Value; 70 | anyUnchecked |= !value.Value; 71 | } 72 | } 73 | 74 | // If any items are unchecked, the action is to check the selected items 75 | bool newValue = anyUnchecked; 76 | foreach (var item in lvw.SelectedItems) 77 | if (item != null) 78 | TrySetPropertyOnItem(item, propName, newValue); 79 | 80 | e.Handled = true; 81 | } 82 | } 83 | } 84 | 85 | private static void ListViewItem_DoubleClick(object sender, MouseButtonEventArgs e) 86 | { 87 | ListView lvw = (ListView)sender; 88 | var propName = GetIsCheckedProperty(lvw); 89 | 90 | if (propName != null) 91 | { 92 | // Check the current state 93 | 94 | object item = lvw.SelectedItem; 95 | if (item != null) 96 | { 97 | var isChecked = TryGetPropertyOnItem(item, propName) as bool?; 98 | if (isChecked.HasValue) 99 | TrySetPropertyOnItem(item, propName, !isChecked.Value); 100 | } 101 | 102 | e.Handled = true; 103 | } 104 | } 105 | 106 | private static readonly KeyEventHandler keyDownHandler = new KeyEventHandler(ListView_KeyDown); 107 | private static readonly MouseButtonEventHandler doubleClickHandler = new MouseButtonEventHandler(ListViewItem_DoubleClick); 108 | 109 | private static void OnEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 110 | { 111 | var lvw = d as ListView; 112 | if (lvw != null) 113 | if ((bool)e.NewValue) 114 | { // If the list also has text searching enabled, it'll eat this event first 115 | lvw.AddHandler(UIElement.PreviewKeyDownEvent, keyDownHandler); 116 | lvw.AddHandler(Control.MouseDoubleClickEvent, doubleClickHandler); 117 | } 118 | else 119 | { 120 | lvw.RemoveHandler(UIElement.PreviewKeyDownEvent, keyDownHandler); 121 | lvw.RemoveHandler(Control.MouseDoubleClickEvent, doubleClickHandler); 122 | } 123 | } 124 | #endregion 125 | 126 | #region CheckBoxElementName 127 | public static string? GetIsCheckedProperty(DependencyObject obj) 128 | => (string?)obj.GetValue(IsCheckedPropertyProperty); 129 | public static void SetIsCheckedProperty(DependencyObject obj, string? value) 130 | => obj.SetValue(IsCheckedPropertyProperty, value); 131 | public static readonly DependencyProperty IsCheckedPropertyProperty = 132 | DependencyProperty.RegisterAttached("IsCheckedProperty", typeof(string), typeof(CheckedList), new PropertyMetadata(null)); 133 | #endregion 134 | } 135 | -------------------------------------------------------------------------------- /DitExplorer.UI.Core/Behaviors/CommandFrame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | using System.Windows.Input; 9 | 10 | namespace DitExplorer.UI.Behaviors; 11 | public static class CommandFrame 12 | { 13 | public static ICommandHandler GetCommandHandler(DependencyObject obj) 14 | { 15 | return (ICommandHandler)obj.GetValue(CommandHandlerProperty); 16 | } 17 | 18 | public static void SetCommandHandler(DependencyObject obj, ICommandHandler value) 19 | { 20 | obj.SetValue(CommandHandlerProperty, value); 21 | } 22 | 23 | // Using a DependencyProperty as the backing store for CommandHandler. This enables animation, styling, binding, etc... 24 | public static readonly DependencyProperty CommandHandlerProperty = 25 | DependencyProperty.RegisterAttached("CommandHandler", typeof(ICommandHandler), typeof(CommandFrame), new PropertyMetadata(null, OnHandlerChanged)); 26 | 27 | private static void OnHandlerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 28 | { 29 | var elem = d as FrameworkElement; 30 | if (elem != null && e.NewValue != null) 31 | { 32 | var handler = (ICommandHandler)e.NewValue; 33 | CommandManager.AddCanExecuteHandler(elem, HandleCanExecute); 34 | CommandManager.AddExecutedHandler(elem, HandleExecuted); 35 | 36 | if (elem.IsLoaded) 37 | handler.OnLoaded(elem); 38 | else 39 | elem.Loaded += Elem_Loaded; 40 | 41 | elem.Unloaded += Elem_Unloaded; 42 | } 43 | // TODO: Detach when set to null 44 | } 45 | 46 | private static void Elem_Loaded(object sender, RoutedEventArgs e) 47 | { 48 | var elem = (FrameworkElement)sender; 49 | var handler = GetCommandHandler(elem); 50 | handler?.OnLoaded(elem); 51 | } 52 | 53 | private static void Elem_Unloaded(object sender, RoutedEventArgs e) 54 | { 55 | var elem = (FrameworkElement)sender; 56 | var handler = GetCommandHandler(elem); 57 | handler?.OnUnloaded(); 58 | } 59 | 60 | private static List _globalHooks = new List(); 61 | public static void AddGlobalCommandHandler(ICommandHandler handler) 62 | { 63 | if (handler is null) throw new ArgumentNullException(nameof(handler)); 64 | _globalHooks.Add(handler); 65 | } 66 | 67 | private static void HandleCanExecute(object sender, CanExecuteRoutedEventArgs e) 68 | { 69 | if (e.Command == ApplicationCommands.Close && sender is Window wnd) 70 | { 71 | var handler = GetCommandHandler((DependencyObject)sender); 72 | e.CanExecute = handler?.CanClose() ?? false; 73 | e.Handled = true; 74 | } 75 | else 76 | { 77 | var handler = GetCommandHandler((DependencyObject)sender); 78 | handler?.HandleCanExecute(e); 79 | } 80 | 81 | if (!e.Handled) 82 | foreach (var hook in _globalHooks) 83 | { 84 | hook.HandleCanExecute(e); 85 | if (e.Handled) 86 | break; 87 | } 88 | } 89 | 90 | private static void HandleExecuted(object sender, ExecutedRoutedEventArgs e) 91 | { 92 | if (e.Command == ApplicationCommands.Close && sender is Window wnd) 93 | { 94 | var handler = GetCommandHandler((DependencyObject)sender); 95 | var canClose = handler?.CanClose() ?? false; 96 | if (canClose) 97 | wnd.Close(); 98 | } 99 | else 100 | { 101 | var handler = GetCommandHandler((DependencyObject)sender); 102 | handler?.HandleExecute(e); 103 | } 104 | 105 | if (!e.Handled) 106 | foreach (var hook in _globalHooks) 107 | { 108 | hook.HandleExecute(e); 109 | if (e.Handled) 110 | break; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /DitExplorer.UI.Core/Behaviors/FlexGrid.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | 9 | namespace DitExplorer.UI.Behaviors; 10 | 11 | public static class FlexGrid 12 | { 13 | 14 | 15 | public static PropertyDescriptor? GetDisplayProperty(DependencyObject obj) 16 | => (PropertyDescriptor?)obj.GetValue(DisplayPropertyProperty); 17 | public static void SetDisplayProperty(DependencyObject obj, PropertyDescriptor? value) 18 | => obj.SetValue(DisplayPropertyProperty, value); 19 | public static readonly DependencyProperty DisplayPropertyProperty = 20 | DependencyProperty.RegisterAttached("DisplayProperty", typeof(PropertyDescriptor), typeof(FlexGrid), new PropertyMetadata(null)); 21 | } 22 | -------------------------------------------------------------------------------- /DitExplorer.UI.Core/Behaviors/FocusHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Input; 8 | using System.Windows.Threading; 9 | 10 | namespace DitExplorer.UI.Behaviors; 11 | 12 | public static class FocusHelper 13 | { 14 | 15 | 16 | public static IInputElement? GetDeferredFocusElement(DependencyObject obj) 17 | { 18 | return (IInputElement?)obj.GetValue(DeferredFocusElementProperty); 19 | } 20 | 21 | public static void SetDeferredFocusElement(DependencyObject obj, IInputElement? value) 22 | { 23 | obj.SetValue(DeferredFocusElementProperty, value); 24 | } 25 | 26 | // Using a DependencyProperty as the backing store for DeferredFocusElement. This enables animation, styling, binding, etc... 27 | public static readonly DependencyProperty DeferredFocusElementProperty = 28 | DependencyProperty.RegisterAttached("DeferredFocusElement", typeof(IInputElement), typeof(FocusHelper), new PropertyMetadata(null, OnDeferredFocusElementChanged)); 29 | 30 | private static void OnDeferredFocusElementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 31 | { 32 | var elem = e.NewValue as FrameworkElement; 33 | if (elem != null) 34 | if (elem.IsLoaded) 35 | FocusManager.SetFocusedElement(d, elem); 36 | else 37 | elem.Loaded += Elem_Loaded; 38 | } 39 | 40 | private static void Elem_Loaded(object sender, RoutedEventArgs e) 41 | { 42 | var obj = (DependencyObject)sender; 43 | var scope = FocusManager.GetFocusScope(obj); 44 | if (scope != null) 45 | Dispatcher.CurrentDispatcher.BeginInvoke(() => 46 | { 47 | FocusManager.SetFocusedElement(scope, (IInputElement)sender); 48 | }, DispatcherPriority.Input); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /DitExplorer.UI.Core/Behaviors/ListViewSorting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | using System.Windows.Data; 10 | 11 | namespace DitExplorer.UI.Behaviors; 12 | 13 | /// 14 | /// Implements list sort behavior for a . 15 | /// 16 | /// 17 | /// This behavior attaches an event handler to listen for clicks on 18 | /// elements. When clicked, the list is sorted by the column whose header was clicked. 19 | /// 20 | public static class ListViewSorting 21 | { 22 | #region SortProperty 23 | public static string GetSortProperty(DependencyObject obj) 24 | => (string)obj?.GetValue(SortPropertyProperty); 25 | public static void SetSortProperty(DependencyObject obj, string value) 26 | => obj.SetValue(SortPropertyProperty, value); 27 | public static readonly DependencyProperty SortPropertyProperty = 28 | DependencyProperty.RegisterAttached("SortProperty", typeof(string), typeof(ListViewSorting), new PropertyMetadata(null)); 29 | #endregion 30 | #region IsSortingEnabled 31 | public static bool GetIsSortingEnabled(DependencyObject obj) 32 | => (bool)obj.GetValue(IsSortingEnabledProperty); 33 | public static void SetIsSortingEnabled(DependencyObject obj, bool value) 34 | => obj.SetValue(IsSortingEnabledProperty, value); 35 | public static readonly DependencyProperty IsSortingEnabledProperty = 36 | DependencyProperty.RegisterAttached("IsSortingEnabled", typeof(bool), typeof(ListViewSorting), new PropertyMetadata(false, OnIsSortingEnabledChanged)); 37 | #endregion 38 | 39 | private static void ColumnHeader_Click(object o, RoutedEventArgs e) 40 | { 41 | var lvw = (ListView)o; 42 | ListSortInfo sortInfo = GetSortInfo(lvw); 43 | 44 | var col = e.OriginalSource as GridViewColumnHeader; 45 | if (col != null) 46 | { 47 | var sortProp = GetSortProperty(col.Column); 48 | if (sortProp == null) 49 | { 50 | // Try to infer from binding 51 | var binding = col.Column?.DisplayMemberBinding as Binding; 52 | sortProp = binding?.Path?.Path; 53 | } 54 | 55 | if (sortProp != null) 56 | { 57 | // sortProp isn't verified. If it's invalid, no error occurs, but sorting won't work 58 | 59 | if (sortInfo.sortPropName == sortProp) 60 | sortInfo.descending = !sortInfo.descending; 61 | else 62 | { 63 | sortInfo.sortPropName = sortProp; 64 | sortInfo.descending = false; 65 | } 66 | 67 | var view = lvw.ItemsSource as ICollectionView; 68 | if (view == null) 69 | view = CollectionViewSource.GetDefaultView(lvw.ItemsSource); 70 | 71 | if (view != null) 72 | { 73 | view.SortDescriptions.Clear(); 74 | view.SortDescriptions.Add(new SortDescription(sortProp, sortInfo.descending ? ListSortDirection.Descending : ListSortDirection.Ascending)); 75 | } 76 | } 77 | 78 | e.Handled = true; 79 | } 80 | } 81 | 82 | private static readonly RoutedEventHandler clickHandler = new RoutedEventHandler(ColumnHeader_Click); 83 | private static void OnIsSortingEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 84 | { 85 | var lvw = d as ListView; 86 | if (lvw != null) 87 | if ((bool)e.NewValue) 88 | { 89 | ListSortInfo sortInfo = new ListSortInfo(); 90 | SetSortInfo(lvw, sortInfo); 91 | 92 | lvw.AddHandler(System.Windows.Controls.Primitives.ButtonBase.ClickEvent, clickHandler); 93 | } 94 | else 95 | lvw.RemoveHandler(System.Windows.Controls.Primitives.ButtonBase.ClickEvent, clickHandler); 96 | } 97 | 98 | /// 99 | /// Tracks list sort information for a . 100 | /// 101 | class ListSortInfo 102 | { 103 | /// 104 | /// Name of last column clicked 105 | /// 106 | internal string? sortPropName; 107 | /// 108 | /// true if sorting in descending order 109 | /// 110 | internal bool descending; 111 | } 112 | 113 | private static ListSortInfo GetSortInfo(DependencyObject obj) 114 | => (ListSortInfo)obj.GetValue(SortInfoProperty); 115 | private static void SetSortInfo(DependencyObject obj, ListSortInfo value) 116 | => obj.SetValue(SortInfoProperty, value); 117 | private static readonly DependencyProperty SortInfoProperty = 118 | DependencyProperty.RegisterAttached("SortInfo", typeof(ListSortInfo), typeof(ListViewSorting), new PropertyMetadata(null)); 119 | 120 | 121 | } 122 | -------------------------------------------------------------------------------- /DitExplorer.UI.Core/CollectionHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows.Threading; 7 | 8 | namespace DitExplorer.UI; 9 | public static class CollectionHelpers 10 | { 11 | public static void DispatchAdd(this TList list, T itemToAdd, Dispatcher dispatcher, DispatcherPriority priority) 12 | where TList : IList 13 | { 14 | if (dispatcher is null) throw new ArgumentNullException(nameof(dispatcher)); 15 | 16 | dispatcher.BeginInvoke((TList list, T item) => { list.Add(item); }, priority, list, itemToAdd); 17 | } 18 | public static void DispatchAddRange(this TList list, IEnumerable enumerable, Dispatcher dispatcher, DispatcherPriority priority) 19 | where TList : IList 20 | { 21 | if (enumerable is null) throw new ArgumentNullException(nameof(enumerable)); 22 | if (dispatcher is null) throw new ArgumentNullException(nameof(dispatcher)); 23 | 24 | foreach (var item in enumerable) 25 | list.DispatchAdd(item, dispatcher, priority); 26 | } 27 | 28 | public static object[] ToArray(this System.Collections.IList list) 29 | { 30 | object[] arr = new object[list.Count]; 31 | for (int i = 0; i < arr.Length; i++) 32 | arr[i] = list[i]; 33 | return arr; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /DitExplorer.UI.Core/DitExplorer.UI.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0-windows 5 | enable 6 | true 7 | enable 8 | DitExplorer.UI 9 | 10 | 11 | 12 | 13 | True 14 | True 15 | Messages.resx 16 | 17 | 18 | 19 | 20 | 21 | ResXFileCodeGenerator 22 | Messages.Designer.cs 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /DitExplorer.UI.Core/ICommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Input; 3 | 4 | namespace DitExplorer.UI; 5 | 6 | public interface ICommandHandler 7 | { 8 | bool CanClose(); 9 | void HandleCanExecute(CanExecuteRoutedEventArgs e); 10 | void HandleExecute(ExecutedRoutedEventArgs e); 11 | void OnLoaded(FrameworkElement element); 12 | void OnUnloaded(); 13 | } -------------------------------------------------------------------------------- /DitExplorer.UI.Core/IContextCommandProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Input; 9 | 10 | namespace DitExplorer.UI 11 | { 12 | public class CommandContext 13 | { 14 | internal CommandContext( 15 | ContextMenu menu, 16 | object[] items, 17 | object? parameter 18 | ) 19 | { 20 | Menu = menu; 21 | Items = items; 22 | Parameter = parameter; 23 | } 24 | 25 | public ContextMenu Menu { get; } 26 | public object[] Items { get; } 27 | public object? Parameter { get; } 28 | } 29 | 30 | public interface IContextCommandProvider 31 | { 32 | void GetContextCommands( 33 | CommandContext context, 34 | FrameworkElement? target, 35 | DependencyObject? source 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /DitExplorer.UI.Core/Messages.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace DitExplorer.UI { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Messages { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Messages() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DitExplorer.UI.Messages", typeof(Messages).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /DitExplorer.UI.Core/Messages.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | text/microsoft-resx 91 | 92 | 93 | 1.3 94 | 95 | 96 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 97 | 98 | 99 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 100 | 101 | -------------------------------------------------------------------------------- /DitExplorer.UI.Core/Notifier.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace DitExplorer.UI; 5 | 6 | /// 7 | /// Implements 8 | /// 9 | /// 10 | /// Call from a property setter 11 | /// to set the backing field and detect if its value has changed. 12 | /// 13 | public class Notifier : INotifyPropertyChanged 14 | { 15 | public event PropertyChangedEventHandler? PropertyChanged; 16 | protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) 17 | { 18 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 19 | } 20 | 21 | protected bool NotifyIfChanged(ref T field, T newValue, [CallerMemberName] string? propertyName = null) 22 | { 23 | if (!EqualityComparer.Default.Equals(field, newValue)) 24 | { 25 | field = newValue; 26 | OnPropertyChanged(propertyName); 27 | return true; 28 | } 29 | else 30 | return false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DitExplorer.UI.Core/ViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Windows; 3 | using System.Windows.Input; 4 | 5 | namespace DitExplorer.UI; 6 | 7 | /// 8 | /// Base class for viewmodel implementations. 9 | /// 10 | /// 11 | /// This class inheris for property-change notifications and 12 | /// implements . Register command handlers by calling 13 | /// or one of its overloads. 14 | /// 15 | public class ViewModel : Notifier, ICommandHandler 16 | { 17 | protected ViewModel() 18 | { 19 | } 20 | 21 | #region Commands 22 | class CommandHandlerEntry 23 | { 24 | internal Action handler; 25 | internal Func canExecute; 26 | } 27 | 28 | Dictionary? _handlers; 29 | 30 | private void AddCommandEntry(ICommand command, CommandHandlerEntry entry) 31 | { 32 | (_handlers ??= new Dictionary()).Add(command, entry); 33 | } 34 | protected void RegisterCommand(ICommand command, Action action, Func? canExecute = null) 35 | { 36 | AddCommandEntry(command, new CommandHandlerEntry 37 | { 38 | handler = action, 39 | canExecute = canExecute 40 | }); 41 | } 42 | protected void RegisterCommand(ICommand command, Action action, Func? canExecute = null) 43 | { 44 | RegisterCommand(command, 45 | o => action(), 46 | canExecute == null ? null : o => canExecute() 47 | ); 48 | } 49 | protected void RegisterCommand(ICommand command, Action action) 50 | where T : class 51 | => RegisterCommand(command, action, null); 52 | protected void RegisterCommand(ICommand command, Action action, Func? canExecute, bool includeNull = false) 53 | where T : class 54 | { 55 | RegisterCommand(command, 56 | o => 57 | { 58 | T? param = o as T; 59 | if (param != null || o is null && includeNull) 60 | action(param); 61 | }, 62 | canExecute == null ? null : o => o is T typed && canExecute(typed) 63 | ); 64 | } 65 | 66 | protected virtual bool CanClose() 67 | { 68 | return true; 69 | } 70 | bool ICommandHandler.CanClose() => CanClose(); 71 | 72 | void ICommandHandler.HandleCanExecute(CanExecuteRoutedEventArgs e) 73 | { 74 | if (_handlers != null && _handlers.TryGetValue(e.Command, out var entry)) 75 | { 76 | e.CanExecute = entry.canExecute?.Invoke(e.Parameter) ?? true; 77 | e.Handled = true; 78 | } 79 | } 80 | 81 | void ICommandHandler.HandleExecute(ExecutedRoutedEventArgs e) 82 | { 83 | try 84 | { 85 | if (_handlers != null && _handlers.TryGetValue(e.Command, out var entry)) 86 | { 87 | entry.handler(e.Parameter); 88 | e.Handled = true; 89 | } 90 | } 91 | catch (Exception ex) 92 | { 93 | this.ReportError("An error occurred while executing the command: " + ex.Message, "DIT Explorer", ex); 94 | } 95 | } 96 | #endregion 97 | 98 | protected void ReportError(string message, string title, Exception ex) 99 | { 100 | // TODO: Show a better error interface 101 | if (Window == null) 102 | MessageBox.Show( 103 | message + "\r\n\r\n" + ex.Message, 104 | title, 105 | MessageBoxButton.OK, 106 | MessageBoxImage.Error 107 | ); 108 | else 109 | MessageBox.Show( 110 | Window, 111 | message + "\r\n\r\n" + ex.Message, 112 | title, 113 | MessageBoxButton.OK, 114 | MessageBoxImage.Error 115 | ); 116 | } 117 | 118 | protected virtual void OnViewUnloaded() 119 | { 120 | 121 | } 122 | void ICommandHandler.OnUnloaded() 123 | => OnViewUnloaded(); 124 | 125 | protected FrameworkElement? ViewElement { get; private set; } 126 | public Window? Window { get; private set; } 127 | 128 | protected virtual void OnViewLoaded(FrameworkElement viewElement) 129 | { 130 | } 131 | void ICommandHandler.OnLoaded(FrameworkElement element) 132 | { 133 | ViewElement = element; 134 | Window = Window.GetWindow(element); 135 | OnViewLoaded(element); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /DitExplorer.Utilities/DitExplorer.Utilities.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /DitExplorer.Utilities/HexHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace DitExplorer; 8 | 9 | [Flags] 10 | public enum HexStringOptions 11 | { 12 | None = 0, 13 | Lowercase = 1, 14 | Uppercase = 0, 15 | } 16 | public static class HexHelper 17 | { 18 | public static string? ToHexString(this byte[]? bytes, HexStringOptions options = HexStringOptions.None, string separator = " ") 19 | { 20 | if (bytes is null) 21 | return null; 22 | 23 | string format = (0 != (options & HexStringOptions.Lowercase)) ? "x2" : "X2"; 24 | // TODO: This could be a bit more efficient 25 | return string.Join(separator, bytes.Select(r => r.ToString(format))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DitExplorer.Utilities/TabularTextWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace DitExplorer.UI.WpfApp 9 | { 10 | /// 11 | /// Writes formatted tabular data to a . 12 | /// 13 | public abstract class TabularTextWriter 14 | { 15 | protected TabularTextWriter(TextWriter writer) 16 | { 17 | this.writer = writer; 18 | } 19 | 20 | protected readonly TextWriter writer; 21 | private bool _lineHasValue; 22 | 23 | /// 24 | /// Writes a value. 25 | /// 26 | /// Value to write 27 | public void WriteValue(string text) 28 | { 29 | if (_lineHasValue) 30 | WriteSeparator(); 31 | else 32 | _lineHasValue = true; 33 | 34 | if (!string.IsNullOrEmpty(text)) 35 | WriteValueCore(text); 36 | } 37 | 38 | protected abstract char[] EscapedChars { get; } 39 | protected virtual void WriteValueCore(string text) 40 | { 41 | if (text.IndexOfAny(EscapedChars) >= 0) 42 | { 43 | if (text.Contains("\"")) 44 | text = text.Replace("\"", "\"\""); 45 | 46 | // TODO: Yeah, this could be more efficient using the TextWriter properly 47 | 48 | text = '"' + text + '"'; 49 | } 50 | 51 | writer.Write(text); 52 | } 53 | 54 | /// 55 | /// Writes the separator between values. 56 | /// 57 | protected abstract void WriteSeparator(); 58 | /// 59 | /// Writes the end of record. 60 | /// 61 | public void EndRecord() 62 | { 63 | writer.WriteLine(); 64 | _lineHasValue = false; 65 | } 66 | } 67 | /// 68 | /// Writes tabular data as tab-separated values. 69 | /// 70 | public sealed class TsvBuilder : TabularTextWriter 71 | { 72 | /// 73 | /// Initializes a new 74 | /// 75 | /// Underlying to write to 76 | public TsvBuilder(TextWriter writer) : base(writer) 77 | { 78 | } 79 | 80 | /// 81 | protected sealed override void WriteSeparator() 82 | { 83 | writer.Write('\t'); 84 | } 85 | 86 | 87 | private static readonly char[] escapeChars = new char[] { '\t', '\r', '\n' }; 88 | protected override char[] EscapedChars => escapeChars; 89 | } 90 | /// 91 | /// Writes tabular data as comma-separated values. 92 | /// 93 | public sealed class CsvBuilder : TabularTextWriter 94 | { 95 | /// 96 | /// Initializes a new 97 | /// 98 | /// Underlying to write to 99 | public CsvBuilder(TextWriter writer) : base(writer) 100 | { 101 | } 102 | 103 | /// 104 | protected sealed override void WriteSeparator() 105 | { 106 | writer.Write(','); 107 | } 108 | 109 | private static readonly char[] escapeChars = new char[] { ',', '\r', '\n' }; 110 | protected override char[] EscapedChars => escapeChars; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/Actions/ViewMembersAction.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.Ntds; 2 | 3 | namespace DitExplorer.UI.WpfApp.Actions 4 | { 5 | public sealed class ViewMembersAction : SingleItemAction 6 | { 7 | public override string MenuText => "View Members"; 8 | 9 | public sealed override bool CanExecute(IDirectoryNode node, IItemActionContext context) 10 | => node.Object.ObjectClass.HasAttribute("member") 11 | || node.Object.ObjectClass.HasAttribute("memberOf"); 12 | 13 | public sealed override void Execute(IDirectoryNode node, IItemActionContext context) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/App.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/AppViewModel.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.EseInterop; 2 | using DitExplorer.Ntds; 3 | using Microsoft.Isam.Esent.Interop; 4 | using Microsoft.Win32; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Collections.ObjectModel; 8 | using System.ComponentModel; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | using System.Windows; 14 | using System.Windows.Controls; 15 | using System.Windows.Data; 16 | using System.Windows.Input; 17 | using System.Windows.Media; 18 | using System.Windows.Threading; 19 | using System.Xml.Linq; 20 | 21 | namespace DitExplorer.UI.WpfApp; 22 | 23 | internal partial class AppViewModel : ViewModel 24 | { 25 | internal AppViewModel() 26 | { 27 | // TODO: Restrict some of these commands based on state, such as whether a directory is open 28 | 29 | var itemActionRegistry = (IItemActionRegistry)App.services.GetService(typeof(IItemActionRegistry)); 30 | ListVM = new DirectoryListViewModel(itemActionRegistry, null); 31 | 32 | RegisterCommand(ApplicationCommands.Open, OpenDirectoryFile); 33 | //RegisterCommand(MyCommands.ChooseColumns, () => this.ListVM?.ShowColumnChooser(), ); 34 | RegisterCommand(ApplicationCommands.Properties, InspectObject); 35 | RegisterCommand(MyCommands.ViewDatabaseSchema, ViewDatabaseSchema, CanViewDatabaseSchema); 36 | } 37 | 38 | internal IDirectory? CurrentDirectory { get; private set; } 39 | public bool HasOpenDirectory => (this.CurrentDirectory != null); 40 | 41 | public DirectoryListViewModel ListVM { get; } 42 | 43 | 44 | private string? _windowTitle = Messages.MainWindowTitle; 45 | public string? WindowTitle 46 | { 47 | get { return _windowTitle; } 48 | set => NotifyIfChanged(ref _windowTitle, value); 49 | } 50 | 51 | protected override void OnViewUnloaded() 52 | { 53 | CurrentDirectory?.Dispose(); 54 | } 55 | 56 | public void OpenDirectoryFile() 57 | { 58 | OpenFileDialog of = new OpenFileDialog() 59 | { 60 | Title = Messages.File_OpenDitFileTitle, 61 | Filter = "DIT files (*.dit)|*.dit|All files (*.*)|*.*", 62 | }; 63 | 64 | var res = of.ShowDialog(Window); 65 | if (res ?? false) 66 | { 67 | string fileName = of.FileName; 68 | this.OpenDirectoryFile(fileName); 69 | } 70 | } 71 | public void OpenDirectoryFile(string? fileName) 72 | { 73 | if (fileName != null) 74 | try 75 | { 76 | var dir = NtdsDirectory.Open(fileName, null, DirectoryOpenOptions.ReadOnly); 77 | CurrentDirectory = dir; 78 | this.OnPropertyChanged(nameof(HasOpenDirectory)); 79 | WindowTitle = Messages.MainWindowTitle + " - " + fileName; 80 | 81 | DirectoryView dirView = new DirectoryView(dir); 82 | _dirView = dirView; 83 | 84 | DirectoryNode node = dirView.NodeForObject(CurrentDirectory.RootDomain); 85 | node.IsExpanded = true; 86 | 87 | RootNodes.Add(node); 88 | SelectedNode = node; 89 | 90 | ListVM.OnDirectoryLoaded(dirView); 91 | } 92 | catch (EsentDatabaseDirtyShutdownException ex) 93 | { 94 | MessageBox.Show(this.Window, Messages.Error_DirtyShutdown, this.WindowTitle, MessageBoxButton.OK, MessageBoxImage.Error); 95 | } 96 | catch (Exception ex) 97 | { 98 | JetSession? ses = null; 99 | try 100 | { 101 | ses = JetSession.Begin(); 102 | JetDatabase? db = null; 103 | try 104 | { 105 | db = ses.AttachAndOpen(fileName, OpenDatabaseOptions.ReadOnly); 106 | ViewDatabaseSchema(db, ses); 107 | db = null; 108 | ses = null; 109 | } 110 | finally 111 | { 112 | db?.Dispose(); 113 | } 114 | } 115 | finally 116 | { 117 | ses?.Dispose(); 118 | } 119 | ReportError(Messages.App_DitOpenFailed, Messages.File_OpenDitFileTitle, ex); 120 | } 121 | 122 | CommandManager.InvalidateRequerySuggested(); 123 | } 124 | 125 | public ObservableCollection RootNodes { get; } = new ObservableCollection(); 126 | 127 | private DirectoryNode? _selectedNode; 128 | 129 | public DirectoryNode? SelectedNode 130 | { 131 | get { return _selectedNode; } 132 | private set 133 | { 134 | if (NotifyIfChanged(ref _selectedNode, value)) 135 | StartPopulateItems(value); 136 | } 137 | } 138 | 139 | internal void OnTreeNodeSelected(object newValue) 140 | { 141 | var dirobj = newValue as DirectoryNode; 142 | if (dirobj != null) 143 | SelectedNode = dirobj; 144 | } 145 | 146 | private ObservableCollection? _items; 147 | private DirectoryView _dirView; 148 | 149 | public ObservableCollection? Items 150 | { 151 | get { return _items; } 152 | set => NotifyIfChanged(ref _items, value); 153 | } 154 | 155 | private void StartPopulateItems(DirectoryNode? node) 156 | { 157 | if (node != null) 158 | { 159 | var disp = Dispatcher.CurrentDispatcher; 160 | 161 | ObservableCollection items = new ObservableCollection(); 162 | foreach (var obj in node.Object.GetChildren()) 163 | items.Add(_dirView.NodeForObject(obj)); 164 | Items = items; 165 | } 166 | } 167 | 168 | internal void OnSelectionChanged() 169 | { 170 | } 171 | 172 | 173 | private void InspectObject(object param) 174 | { 175 | Window? owner = Window; 176 | param = App.ShowObjectInspector(param, owner); 177 | } 178 | 179 | private bool CanViewDatabaseSchema() 180 | => CurrentDirectory is NtdsDirectory; 181 | 182 | private void ViewDatabaseSchema() 183 | => ViewDatabaseSchema(((NtdsDirectory)CurrentDirectory).Database, null); 184 | private void ViewDatabaseSchema(JetDatabase db, JetSession? session) 185 | { 186 | DatabaseSchemaViewModel vm = new DatabaseSchemaViewModel(db, session, false); 187 | DatabaseSchemaWindow wnd = new DatabaseSchemaWindow() { DataContext = vm, Owner = Window, WindowStartupLocation = WindowStartupLocation.CenterOwner }; 188 | wnd.Show(); 189 | } 190 | } 191 | 192 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/ColumnChooserViewModel.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.Ntds; 2 | using DitExplorer.UI.Behaviors; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using System.Windows.Controls; 10 | using System.Windows.Input; 11 | 12 | namespace DitExplorer.UI.WpfApp; 13 | internal class ColumnChooserViewModel : ViewModel 14 | { 15 | private readonly IList availableProperties; 16 | private readonly IClassSchema[] classes; 17 | private readonly GridView grid; 18 | private readonly List _allColumns; 19 | private Dictionary _columnsByName; 20 | 21 | internal ColumnChooserViewModel( 22 | IList availableProperties, 23 | IClassSchema[] classes, 24 | GridView grid) 25 | { 26 | RegisterCommand(MyCommands.Accept, AcceptChanges); 27 | 28 | this.availableProperties = availableProperties; 29 | this.classes = classes; 30 | this.grid = grid; 31 | 32 | // Determine which columns are visible 33 | Dictionary columns = new Dictionary(); 34 | foreach (var col in grid.Columns) 35 | { 36 | var name = ListViewSorting.GetSortProperty(col); 37 | if (name != null) 38 | columns.Add(name, col); 39 | } 40 | _columnsByName = columns; 41 | 42 | List columnChoices = new List(availableProperties.Count); 43 | Dictionary columnsByName = new Dictionary(); 44 | foreach (PropertyDescriptor prop in availableProperties) 45 | { 46 | if (!prop.IsBrowsable) 47 | continue; 48 | if (prop.Name == nameof(DirectoryNode.Name)) 49 | // TODO: Find a better way to detect the Name property 50 | continue; 51 | 52 | columns.TryGetValue(prop.Name, out var col); 53 | 54 | var choice = new ColumnChoice(prop) 55 | { 56 | IsEnabled = prop.Name != nameof(DirectoryNode.Name) 57 | }; 58 | if (col != null) 59 | { 60 | choice.originalChecked = true; 61 | choice.IsChecked = true; 62 | } 63 | 64 | columnChoices.Add(choice); 65 | if (prop is DirectoryPropertyDescriptor dirprop) 66 | columnsByName.Add(dirprop.AttributeSchema.LdapDisplayName, choice); 67 | } 68 | 69 | _allColumns = columnChoices; 70 | 71 | List columnSets = new List(); 72 | var setAll = new ColumnSet("", columnChoices); 73 | columnSets.Add(setAll); 74 | 75 | foreach (var objcls in classes) 76 | { 77 | ColumnSet set = new ColumnSet(objcls.Name, () => 78 | { 79 | var attrs = objcls.GetAttributes(true); 80 | List cols = new List(attrs.Length); 81 | foreach (var attr in attrs) 82 | if (columnsByName.TryGetValue(attr.LdapDisplayName, out var col)) 83 | cols.Add(col); 84 | return cols; 85 | }); 86 | columnSets.Add(set); 87 | } 88 | 89 | ColumnSets = columnSets; 90 | SelectedColumnSet = setAll; 91 | } 92 | 93 | 94 | private ColumnSet _selectedColumnSet; 95 | 96 | public ColumnSet SelectedColumnSet 97 | { 98 | get { return _selectedColumnSet; } 99 | set 100 | { 101 | if (NotifyIfChanged(ref _selectedColumnSet, value)) 102 | UpdateColumns(value, ColumnSearch); 103 | } 104 | } 105 | 106 | public List Columns { get; private set; } 107 | 108 | private void AcceptChanges() 109 | { 110 | foreach (var choice in _allColumns) 111 | if (choice.IsChecked != choice.originalChecked) 112 | if (choice.IsChecked) 113 | { 114 | var col = DirectoryListViewModel.CreateColumn(choice.Property, choice.Property.DisplayName); 115 | grid.Columns.Add(col); 116 | } 117 | else 118 | if (_columnsByName.TryGetValue(choice.Name, out var col)) 119 | grid.Columns.Remove(col); 120 | 121 | ICommand cmd = ApplicationCommands.Close; 122 | cmd.Execute(null); 123 | } 124 | 125 | private string? _columnSearch; 126 | 127 | public string? ColumnSearch 128 | { 129 | get { return _columnSearch; } 130 | set 131 | { 132 | if (NotifyIfChanged(ref _columnSearch, value)) 133 | _ = Task.Factory.StartNew(() => UpdateColumns(_selectedColumnSet, value)); 134 | } 135 | } 136 | 137 | public IList ColumnSets { get; } 138 | 139 | private void UpdateColumns(ColumnSet? set, string? searchText) 140 | { 141 | var columns = set?.Columns ?? new List(); 142 | if (searchText != null) 143 | columns = columns.FindAll(r => 144 | r.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase) 145 | || (r.Property.DisplayName?.Contains(searchText, StringComparison.OrdinalIgnoreCase) ?? false) 146 | || (r.Property.Description?.Contains(searchText, StringComparison.OrdinalIgnoreCase) ?? false) 147 | ); 148 | 149 | if (ColumnSearch == searchText && SelectedColumnSet == set) 150 | { 151 | Columns = columns; 152 | OnPropertyChanged(nameof(Columns)); 153 | } 154 | } 155 | } 156 | 157 | class ColumnSet 158 | { 159 | private List? _columns; 160 | private readonly Func> _columnsFactory; 161 | 162 | internal List Columns => _columns ??= _columnsFactory(); 163 | 164 | public string Name { get; } 165 | 166 | internal ColumnSet(string name, List columns) 167 | { 168 | Name = name; 169 | _columns = columns; 170 | } 171 | internal ColumnSet(string name, Func> columnsFactory) 172 | { 173 | Name = name; 174 | _columnsFactory = columnsFactory; 175 | } 176 | 177 | public sealed override string ToString() 178 | => Name; 179 | } 180 | 181 | class ColumnChoice : Notifier 182 | { 183 | internal ColumnChoice(PropertyDescriptor property) 184 | { 185 | Property = property; 186 | } 187 | public bool IsEnabled { get; set; } 188 | public string? Description => Property.Description; 189 | public PropertyDescriptor Property { get; } 190 | public string Name => Property.Name; 191 | 192 | private bool _isVisible; 193 | 194 | internal bool originalChecked; 195 | public bool IsChecked 196 | { 197 | get { return _isVisible; } 198 | set => NotifyIfChanged(ref _isVisible, value); 199 | } 200 | 201 | } -------------------------------------------------------------------------------- /DitExplorer.WpfApp/ColumnChooserWindow.xaml: -------------------------------------------------------------------------------- 1 |  14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 44 | 45 | 46 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 75 | 76 | 77 | 78 | 79 | 29 | 30 | 31 | 32 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 76 | 77 | 78 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 116 | 117 | 118 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/DatabaseSchemaWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Shapes; 14 | 15 | namespace DitExplorer.UI.WpfApp 16 | { 17 | /// 18 | /// Interaction logic for DatabaseSchemaWindow.xaml 19 | /// 20 | public partial class DatabaseSchemaWindow : Window 21 | { 22 | public DatabaseSchemaWindow() 23 | { 24 | InitializeComponent(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/DirectoryNode.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.Ntds; 2 | using DitExplorer.UI.WpfApp; 3 | using System.Collections.ObjectModel; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Windows; 7 | using System.Windows.Data; 8 | using System.Windows.Threading; 9 | 10 | namespace DitExplorer.UI.WpfApp; 11 | 12 | internal partial class DirectoryNode : Notifier 13 | { 14 | [Browsable(false)] 15 | public IDirectoryObject Object { get; } 16 | 17 | internal DirectoryNode(IDirectoryObject obj, DirectoryView owner) 18 | { 19 | this.Object = obj; 20 | this.Owner = owner; 21 | } 22 | 23 | internal DirectoryView Owner { get; } 24 | 25 | public override string ToString() => this.Object.Name; 26 | public string Name => this.Object.Name; 27 | 28 | private bool _isExpanded; 29 | 30 | [Browsable(false)] 31 | public bool IsExpanded 32 | { 33 | get { return _isExpanded; } 34 | set => this.NotifyIfChanged(ref this._isExpanded, value); 35 | } 36 | 37 | 38 | [DisplayName("Obj. Class")] 39 | public string ObjectClassName => this.Object.ObjectClass.LdapDisplayName; 40 | 41 | [DisplayName("Path")] 42 | public string ObjectPath => this.Object.ObjectPath; 43 | 44 | [DisplayName("DN")] 45 | public string DistinguishedName => this.Object.DistinguishedName; 46 | 47 | 48 | private ObservableCollection? _children; 49 | private CollectionView? _childrenView; 50 | 51 | [Browsable(false)] 52 | public object? ChildNodesView 53 | { 54 | get 55 | { 56 | if (this._childrenView == null) 57 | { 58 | this._children = new ObservableCollection(); 59 | this._childrenView = new ListCollectionView(this._children); 60 | 61 | // TODO: Run in background once Jet session synchronization is implemented 62 | this.PopulateChildren(); 63 | } 64 | return this._childrenView; 65 | } 66 | } 67 | 68 | private async void PopulateChildren() 69 | { 70 | //try 71 | { 72 | var children = this.Object.GetChildren().Select(r => this.Owner.NodeForObject(r)); 73 | 74 | var items = this._children; 75 | foreach (var item in children) 76 | { 77 | await Task.Yield(); 78 | items.Add(item); 79 | } 80 | } 81 | //catch 82 | //{ 83 | // // TODO: Display error 84 | //} 85 | } 86 | } 87 | 88 | partial class DirectoryNode : ICustomTypeDescriptor 89 | { 90 | private static EventDescriptorCollection emptyEvents = new EventDescriptorCollection(Array.Empty(), true); 91 | private static AttributeCollection emptyAttrs = new AttributeCollection(Array.Empty()); 92 | 93 | AttributeCollection ICustomTypeDescriptor.GetAttributes() => emptyAttrs; 94 | string? ICustomTypeDescriptor.GetClassName() => this.GetType().Name; 95 | string? ICustomTypeDescriptor.GetComponentName() => null; 96 | TypeConverter ICustomTypeDescriptor.GetConverter() => null; 97 | EventDescriptor? ICustomTypeDescriptor.GetDefaultEvent() => null; 98 | PropertyDescriptor? ICustomTypeDescriptor.GetDefaultProperty() => null; 99 | object? ICustomTypeDescriptor.GetEditor(Type editorBaseType) => null; 100 | 101 | EventDescriptorCollection ICustomTypeDescriptor.GetEvents() => emptyEvents; 102 | 103 | EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[]? attributes) => emptyEvents; 104 | PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties() => this.Owner.attrProps; 105 | 106 | PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[]? attributes) 107 | { 108 | if (attributes is null || attributes.Length == 0) 109 | return this.Owner.attrProps; 110 | 111 | var allProps = this.Owner.attrProps; 112 | List matchingProps = new List(); 113 | foreach (PropertyDescriptor prop in allProps) 114 | { 115 | bool allFiltersMatch = MatchesFilters(prop, attributes); 116 | 117 | if (allFiltersMatch) 118 | matchingProps.Add(prop); 119 | } 120 | 121 | return new PropertyDescriptorCollection(matchingProps.ToArray(), true); 122 | } 123 | 124 | private static bool MatchesFilters(PropertyDescriptor prop, Attribute[]? attributes) 125 | { 126 | var propAttrs = prop.Attributes; 127 | 128 | bool allFiltersMatch = true; 129 | foreach (Attribute filterAttr in attributes) 130 | { 131 | if (filterAttr is not null) 132 | { 133 | bool foundMatch = false; 134 | foreach (Attribute attr in propAttrs) 135 | { 136 | if (attr is not null) 137 | { 138 | foundMatch = attr.Match(filterAttr); 139 | if (foundMatch) 140 | break; 141 | } 142 | } 143 | 144 | allFiltersMatch &= foundMatch; 145 | } 146 | 147 | if (!allFiltersMatch) 148 | break; 149 | } 150 | 151 | return allFiltersMatch; 152 | } 153 | 154 | object? ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor? pd) => this; 155 | } 156 | 157 | internal partial class DirectoryNode : Notifier, IDirectoryNode 158 | { 159 | IDirectoryView IDirectoryNode.DirectoryView => this.Owner; 160 | } 161 | 162 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/DirectoryPropertyDescriptor.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.Ntds; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Reflection.Metadata.Ecma335; 7 | using System.Runtime.CompilerServices; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace DitExplorer.UI.WpfApp; 12 | 13 | /// 14 | /// Represents a directory attribute as a property. 15 | /// 16 | internal sealed class DirectoryPropertyDescriptor : PropertyDescriptor 17 | { 18 | private readonly object UnsupportedValue = ""; 19 | 20 | public IAttributeSchema AttributeSchema { get; } 21 | 22 | private static Attribute[] GetAttributesFor(IAttributeSchema attrSchema) 23 | { 24 | List? attrs = null; 25 | 26 | if (attrSchema is NtdsAttributeSchema ntdsAttr) 27 | if (!string.IsNullOrEmpty(ntdsAttr.AdminDescription)) 28 | (attrs = new List()).Add(new DescriptionAttribute(ntdsAttr.AdminDescription)); 29 | 30 | if (attrs == null || attrs.Count == 0) 31 | return Array.Empty(); 32 | else 33 | return attrs.ToArray(); 34 | } 35 | internal DirectoryPropertyDescriptor(IAttributeSchema attributeSchema) 36 | : base(attributeSchema.LdapDisplayName, GetAttributesFor(attributeSchema)) 37 | { 38 | AttributeSchema = attributeSchema; 39 | } 40 | 41 | /// 42 | public sealed override Type ComponentType => typeof(DirectoryNode); 43 | /// 44 | public sealed override bool IsReadOnly => true; 45 | 46 | private Type? _propertyType; 47 | /// 48 | public sealed override Type PropertyType => _propertyType ??= MakeType(); 49 | private Type MakeType() 50 | { 51 | if (AttributeSchema.IsLink) 52 | return AttributeSchema.IsSingleValued ? typeof(NtdsDirectoryObjectReference) : typeof(MultiValue); 53 | else 54 | { 55 | var type = AttributeSchema.Syntax?.AttributeType ?? typeof(object); 56 | if (!AttributeSchema.IsSingleValued) 57 | type = typeof(MultiValue<>).MakeGenericType(type); 58 | 59 | return type; 60 | } 61 | } 62 | /// 63 | public sealed override bool CanResetValue(object component) => false; 64 | /// 65 | public sealed override object? GetValue(object? component) 66 | { 67 | if (component is null) 68 | return null; 69 | 70 | var syntax = AttributeSchema.Syntax; 71 | if (syntax == null || !syntax.CanRetrieveValue) 72 | return UnsupportedValue; 73 | 74 | var node = (DirectoryNode)component; 75 | if (AttributeSchema.IsSingleValued) 76 | return node.Object.GetValueOf(AttributeSchema); 77 | else 78 | { 79 | var multi = node.Object.GetMultiValuesOf(AttributeSchema); 80 | return multi; 81 | } 82 | } 83 | 84 | /// 85 | public sealed override void ResetValue(object component) 86 | { 87 | throw new NotImplementedException(); 88 | } 89 | 90 | /// 91 | public sealed override void SetValue(object? component, object? value) 92 | { 93 | throw new NotImplementedException(); 94 | } 95 | 96 | /// 97 | public sealed override bool ShouldSerializeValue(object component) 98 | { 99 | throw new NotImplementedException(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/DirectoryView.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.Ntds; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace DitExplorer.UI.WpfApp 10 | { 11 | class DirectoryView : IDirectoryView 12 | { 13 | internal DirectoryView(IDirectory directory) 14 | { 15 | if (directory is null) throw new ArgumentNullException(nameof(directory)); 16 | Directory = directory; 17 | 18 | // Build property descriptors 19 | var attrs = directory.GetAllAttributeSchemas(); 20 | 21 | var classProps = TypeDescriptor.GetProperties(typeof(DirectoryNode)); 22 | List props = new List(); 23 | foreach (PropertyDescriptor prop in classProps) 24 | props.Add(prop); 25 | foreach (var attr in attrs) 26 | { 27 | var prop = new DirectoryPropertyDescriptor(attr); 28 | props.Add(prop); 29 | } 30 | attrPropList = props; 31 | attrProps = new PropertyDescriptorCollection(props.ToArray(), true); 32 | } 33 | 34 | public IDirectory Directory { get; } 35 | 36 | internal IList attrPropList; 37 | internal PropertyDescriptorCollection attrProps; 38 | 39 | internal DirectoryNode NodeForObject(IDirectoryObject obj) 40 | { 41 | if (obj is null) throw new ArgumentNullException(nameof(obj)); 42 | return new DirectoryNode(obj, this); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/DitExplorer.UI.WpfApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net8.0-windows 6 | enable 7 | enable 8 | true 9 | $(SolutionDir)_build\ 10 | obj\ 11 | 12 | $(BaseOutputPath)\$(Configuration)\ 13 | 14 | False 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | True 33 | True 34 | Messages.resx 35 | 36 | 37 | 38 | 39 | 40 | PublicResXFileCodeGenerator 41 | Messages.Designer.cs 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | True 54 | \ 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/ItemActionCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Input; 2 | 3 | namespace DitExplorer.UI.WpfApp; 4 | internal class ItemActionCommand : ICommand 5 | { 6 | private readonly ItemAction _action; 7 | private readonly IItemActionContext _context; 8 | 9 | public ItemActionCommand(ItemAction action, IItemActionContext context) 10 | { 11 | if (action is null) throw new ArgumentNullException(nameof(action)); 12 | this._action = action; 13 | this._context = context; 14 | } 15 | 16 | public event EventHandler? CanExecuteChanged; 17 | 18 | public bool CanExecute(object? parameter) 19 | { 20 | return _action.CanExecute(parameter as object[], this._context); 21 | } 22 | 23 | public void Execute(object? parameter) 24 | { 25 | _action.Execute((object[])parameter, this._context); 26 | } 27 | } -------------------------------------------------------------------------------- /DitExplorer.WpfApp/ItemActivation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | 8 | namespace DitExplorer.UI.WpfApp 9 | { 10 | static class ItemActivation 11 | { 12 | 13 | 14 | public static bool GetIsEnabled(DependencyObject obj) 15 | => (bool)obj.GetValue(IsEnabledProperty); 16 | public static void SetIsEnabled(DependencyObject obj, bool value) 17 | => obj.SetValue(IsEnabledProperty, value); 18 | public static readonly DependencyProperty IsEnabledProperty = 19 | DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(ItemActivation), new PropertyMetadata(false, OnIsEnabledChanged)); 20 | 21 | private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 22 | { 23 | var elem = d as FrameworkElement; 24 | if (elem != null) 25 | if ((bool)e.NewValue) 26 | { 27 | elem.PreviewKeyDown += Elem_PreviewKeyDown; 28 | elem.MouseLeftButtonDown += Elem_MouseLeftButtonDown; 29 | } 30 | else 31 | { 32 | elem.PreviewKeyDown += Elem_PreviewKeyDown; 33 | elem.MouseLeftButtonDown += Elem_MouseLeftButtonDown; 34 | } 35 | } 36 | 37 | private static void Elem_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e) 38 | { 39 | } 40 | 41 | private static void Elem_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) 42 | { 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | open a directory file 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 63 | 64 | 65 | 66 | 67 | 70 | 71 | 72 | 73 | 80 | 81 | 95 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.UI.WpfApp; 2 | using System.Text; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | using System.Windows.Data; 6 | using System.Windows.Documents; 7 | using System.Windows.Input; 8 | using System.Windows.Media; 9 | using System.Windows.Media.Imaging; 10 | using System.Windows.Navigation; 11 | using System.Windows.Shapes; 12 | 13 | namespace DitExplorer.UI.WpfApp; 14 | /// 15 | /// Interaction logic for MainWindow.xaml 16 | /// 17 | public partial class MainWindow : Window 18 | { 19 | public MainWindow() 20 | { 21 | InitializeComponent(); 22 | } 23 | 24 | private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs e) 25 | { 26 | TreeView tvw = (TreeView)sender; 27 | var vm = (AppViewModel)this.DataContext; 28 | vm.OnTreeNodeSelected(e.NewValue); 29 | } 30 | 31 | private void itemsList_SelectionChanged(object sender, SelectionChangedEventArgs e) 32 | { 33 | ListView lvw = (ListView)sender; 34 | var vm = (AppViewModel)this.DataContext; 35 | vm.OnSelectionChanged(); 36 | } 37 | } -------------------------------------------------------------------------------- /DitExplorer.WpfApp/MyCommands.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows.Input; 7 | 8 | namespace DitExplorer.UI.WpfApp; 9 | internal class MyCommands 10 | { 11 | private static InputGestureCollection MakeGesture(Key key, ModifierKeys mod = ModifierKeys.None) 12 | { 13 | return new InputGestureCollection() 14 | { 15 | new KeyGesture(key, mod) 16 | }; 17 | } 18 | public static RoutedUICommand ChooseColumns = new RoutedUICommand(Messages.View_ColumnsMenuText, "View.SelectColumns", typeof(MyCommands), MakeGesture(Key.F7)); 19 | public static RoutedUICommand ExportList = new RoutedUICommand(Messages.File_ExportMenuText, "List.Export", typeof(MyCommands), MakeGesture(Key.X, ModifierKeys.Alt | ModifierKeys.Control)); 20 | public static RoutedUICommand CopyValue = new RoutedUICommand(Messages.Edit_CopyValueMenuText, "List.CopyValue", typeof(MyCommands), null); 21 | public static RoutedUICommand CopySelection = new RoutedUICommand(Messages.Edit_CopyMenuText, "List.CopyRows", typeof(MyCommands), MakeGesture(Key.C, ModifierKeys.Control)); 22 | public static RoutedUICommand CopyDN = new RoutedUICommand(Messages.Edit_CopyDnMenuText, "Edit.CopyDN", typeof(MyCommands)); 23 | public static RoutedUICommand SearchSubtree = new RoutedUICommand(Messages.Edit_SearchSubtreeMenuText, "Edit.SearchSubtree", typeof(MyCommands), MakeGesture(Key.F, ModifierKeys.Control)); 24 | public static RoutedUICommand SearchNow = new RoutedUICommand(Messages.Edit_SearchNowAccessText, "Edit.SearchNow", typeof(MyCommands)); 25 | 26 | public static RoutedUICommand ViewDatabaseSchema = new RoutedUICommand(Messages.View_DatabaseSchema, "View.DatabaseSchema", typeof(MyCommands), MakeGesture(Key.F8, ModifierKeys.None)); 27 | 28 | public static RoutedUICommand ExportTableData = new RoutedUICommand(Messages.File_ExportTableDataMenuText, "DataViewer.ExportTableData", typeof(MyCommands), MakeGesture(Key.E, ModifierKeys.Control)); 29 | 30 | public static RoutedCommand Accept = new RoutedCommand(); 31 | public static RoutedCommand Cancel = new RoutedCommand(); 32 | 33 | public static RoutedCommand ShowAllValues = new RoutedCommand("View.ShowAllValues", typeof(MyCommands)); 34 | } 35 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/ObjectInspectorViewModel.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.Ntds; 2 | using DitExplorer.UI.WpfApp; 3 | using DitExplorer.UI.WpfApp.Services; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using System.Windows; 10 | using System.Windows.Controls; 11 | 12 | namespace DitExplorer.UI.WpfApp; 13 | internal partial class ObjectInspectorViewModel : ViewModel 14 | { 15 | public IDirectoryObject Object { get; } 16 | 17 | internal ObjectInspectorViewModel(DirectoryNode node, IItemActionRegistry itemActionRegistry) 18 | { 19 | IDirectoryObject obj = node.Object; 20 | this.Object = obj; 21 | this._dirView = node.Owner; 22 | this.Title = Messages.ObjectInspector_Title + " - " + obj.DistinguishedName; 23 | 24 | var dir = obj.Directory; 25 | var allAttrs = dir.GetAllAttributeSchemas(); 26 | 27 | object[] allValues = obj.GetValueOfMultiple(allAttrs); 28 | List propertyItems = new List(); 29 | for (int i = 0; i < allAttrs.Length; i++) 30 | { 31 | var attr = allAttrs[i]; 32 | var value = allValues[i]; 33 | 34 | var ntdsAttr = attr as NtdsAttributeSchema; 35 | if (value is MultiValue multi) 36 | { 37 | for (int j = 0; j < multi.Count; j++) 38 | { 39 | var element = multi.GetElementAt(j); 40 | var item = new PropertyItem(attr.Name, j + 1, element) { LdapName = attr.LdapDisplayName, ColumnName = ntdsAttr?.ColumnName }; 41 | propertyItems.Add(item); 42 | } 43 | } 44 | else if (value != null) 45 | { 46 | var item = new PropertyItem(attr.Name, 1, value) { LdapName = attr.LdapDisplayName, ColumnName = ntdsAttr?.ColumnName }; 47 | propertyItems.Add(item); 48 | } 49 | } 50 | 51 | this._allProps = propertyItems; 52 | this.Properties = propertyItems; 53 | 54 | this.HasMembers = obj.ObjectClass.HasAttribute("member"); 55 | if (this.HasMembers) 56 | { 57 | this.MembersListVM = new DirectoryListViewModel(itemActionRegistry, this._dirView); 58 | } 59 | this.HasMemberOf = obj.ObjectClass.HasAttribute("memberOf"); 60 | if (this.HasMemberOf) 61 | { 62 | this.MemberOfListVM = new DirectoryListViewModel(itemActionRegistry, this._dirView); 63 | } 64 | } 65 | 66 | public string Title { get; } 67 | 68 | private readonly List _allProps; 69 | private readonly DirectoryView _dirView; 70 | private List _props; 71 | 72 | public List Properties 73 | { 74 | get { return _props; } 75 | set => this.NotifyIfChanged(ref _props, value); 76 | } 77 | 78 | public bool HasMembers { get; } 79 | public bool HasMemberOf { get; } 80 | 81 | private List? _members; 82 | public List Members => (this._members ??= this.GetMembers()); 83 | private List? GetMembers() 84 | { 85 | return this.Object.GetMembers().Select(r => (IDirectoryNode)new DirectoryNode(r, this._dirView)).ToList(); 86 | } 87 | public DirectoryListViewModel? MembersListVM { get; } 88 | 89 | 90 | private List? _memberOf; 91 | public List MemberOf => (this._memberOf ??= this.GetMemberOf()); 92 | private List? GetMemberOf() 93 | { 94 | return this.Object.GetMemberOfGroups().Select(r => (IDirectoryNode)new DirectoryNode(r, this._dirView)).ToList(); 95 | } 96 | public DirectoryListViewModel? MemberOfListVM { get; } 97 | 98 | internal record struct NameSortKey(string name, int seq) : IComparable 99 | { 100 | public int CompareTo(NameSortKey other) 101 | { 102 | int cmp = StringComparer.OrdinalIgnoreCase.Compare(this.name, other.name); 103 | if (cmp != 0) 104 | return cmp; 105 | 106 | return this.seq - other.seq; 107 | } 108 | } 109 | 110 | internal class PropertyItem 111 | { 112 | public PropertyItem(string name, int seq, object? value) 113 | { 114 | Name = name; 115 | Seq = seq; 116 | Value = value; 117 | } 118 | 119 | public string Name { get; } 120 | internal NameSortKey NameSortKey => new NameSortKey(this.Name, this.Seq); 121 | public int Seq { get; } 122 | public string LdapName { get; set; } 123 | public string? ColumnName { get; set; } 124 | public object? RawValue { get; set; } 125 | public object? Value { get; set; } 126 | 127 | /// 128 | /// Provides a value for sorting. 129 | /// 130 | /// 131 | /// The default WPF sort behavior will fail to compare objects of different types. 132 | /// Although basic, this property provides a uniform string array for comparison. 133 | /// It won't handle numbers well. 134 | /// 135 | // TODO: More intelligent sorting 136 | public string? SortValue => this.Value?.ToString(); 137 | } 138 | 139 | private string? _propertySearch; 140 | 141 | public string? PropertySearch 142 | { 143 | get { return _propertySearch; } 144 | set 145 | { 146 | if (this.NotifyIfChanged(ref _propertySearch, value)) 147 | this.UpdateProperties(value); 148 | } 149 | } 150 | 151 | private void UpdateProperties(string? value) 152 | { 153 | var props = this._allProps; 154 | if (value != null) 155 | { 156 | props = props.FindAll(r => 157 | r.Name.Contains(value, StringComparison.OrdinalIgnoreCase) 158 | || r.LdapName.Contains(value, StringComparison.OrdinalIgnoreCase) 159 | || (r.RawValue?.ToString()?.Contains(value, StringComparison.OrdinalIgnoreCase) ?? false) 160 | || (r.SortValue?.Contains(value, StringComparison.OrdinalIgnoreCase) ?? false) 161 | || (r.ColumnName?.Contains(value, StringComparison.OrdinalIgnoreCase) ?? false) 162 | ); 163 | } 164 | 165 | if (this.PropertySearch == value) 166 | { 167 | this.Properties = props; 168 | } 169 | } 170 | } 171 | 172 | internal partial class ObjectInspectorViewModel : IContextCommandProvider 173 | { 174 | void IContextCommandProvider.GetContextCommands(CommandContext context, FrameworkElement? target, DependencyObject? source) 175 | { 176 | var menu = context.Menu; 177 | var lvw = target as ListView; 178 | if (context.Items.Length > 0) 179 | { 180 | // CopyItems 181 | menu.Items.Add(new MenuItem() { Header = Messages.Edit_CopyItemsMenuText, Command = MyCommands.CopySelection, CommandParameter = lvw, CommandTarget = lvw }); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/ObjectInspectorWindow.xaml: -------------------------------------------------------------------------------- 1 |  12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 64 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 85 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/ObjectInspectorWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Shapes; 14 | 15 | namespace DitExplorer.UI.WpfApp; 16 | /// 17 | /// Interaction logic for ObjectInspectorWindow.xaml 18 | /// 19 | public partial class ObjectInspectorWindow : Window 20 | { 21 | public ObjectInspectorWindow() 22 | { 23 | InitializeComponent(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/ObjectSearchViewModel.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.Ntds; 2 | using DitExplorer.UI.WpfApp; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace DitExplorer.UI.WpfApp 10 | { 11 | class ObjectSearchViewModel : ViewModel 12 | { 13 | public ObjectSearchViewModel(IDirectoryObject searchRoot, DirectoryView directory) 14 | { 15 | if (searchRoot == null) throw new ArgumentNullException(nameof(SearchRoot)); 16 | SearchRoot = searchRoot; 17 | this.directory = directory; 18 | RegisterCommand(MyCommands.SearchNow, SearchNow); 19 | 20 | ListVM = new DirectoryListViewModel((IItemActionRegistry)App.services.GetService(typeof(IItemActionRegistry)), directory); 21 | 22 | Title = Messages.SubtreeSearch_Title + " - " + SearchRoot.DistinguishedName; 23 | 24 | var classes = directory.Directory.GetClassSchemas(); 25 | Array.Sort(classes, (x, y) => x.LdapDisplayName.CompareTo(y.LdapDisplayName)); 26 | List classList = new List(classes.Length + 1); 27 | classList.Add(null); 28 | classList.AddRange(classes); 29 | this.Classes = classList; 30 | } 31 | 32 | public string? Title { get; } 33 | 34 | public DirectoryListViewModel ListVM { get; private set; } 35 | public IList Classes { get; } 36 | 37 | private IClassSchema? _selectedClass; 38 | public IClassSchema? SelectedClass 39 | { 40 | get { return _selectedClass; } 41 | set => this.NotifyIfChanged(ref _selectedClass, value); 42 | } 43 | 44 | private bool _includesSubclasses = true; 45 | public bool IncludesSubclasses 46 | { 47 | get { return _includesSubclasses; } 48 | set => this.NotifyIfChanged(ref _includesSubclasses, value); 49 | } 50 | 51 | 52 | public IDirectoryObject SearchRoot { get; } 53 | public string SearchRootPath => SearchRoot.DistinguishedName; 54 | 55 | 56 | private int _searchMode; 57 | 58 | public int SearchMode 59 | { 60 | get { return _searchMode; } 61 | set => NotifyIfChanged(ref _searchMode, value); 62 | } 63 | 64 | 65 | 66 | private string? _searchName; 67 | 68 | public string? SearchName 69 | { 70 | get { return _searchName; } 71 | set => NotifyIfChanged(ref _searchName, value); 72 | } 73 | 74 | 75 | private IList _results; 76 | private readonly DirectoryView directory; 77 | 78 | public IList Results 79 | { 80 | get { return _results; } 81 | set => NotifyIfChanged(ref _results, value); 82 | } 83 | 84 | private void SearchNow() 85 | { 86 | var results = this.SearchRoot.SearchSubtree(this.SearchName, this.SelectedClass, this.IncludesSubclasses); 87 | var array = results.Select(r => directory.NodeForObject(r)).ToArray(); 88 | Results = array; 89 | } 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Windows; 3 | 4 | [assembly: ThemeInfo( 5 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 6 | //(used if a resource is not found in the page, 7 | // or application resource dictionaries) 8 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 9 | //(used if a resource is not found in the page, 10 | // app, or any theme specific resource dictionaries) 11 | )] 12 | 13 | // Version information for an assembly consists of the following four values: 14 | // 15 | // Major Version 16 | // Minor Version 17 | // Build Number 18 | // Revision 19 | // 20 | -------------------------------------------------------------------------------- /DitExplorer.WpfApp/SearchWindow.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 76 | 77 | 78 | 79 | 80 | 81 | 74 | 75 | 76 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Extensions/DitExplorer.CredentialExtraction/CredentialExtractorWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Shapes; 14 | 15 | namespace DitExplorer.WpfApp; 16 | /// 17 | /// Interaction logic for CredentialExtractorWindow.xaml 18 | /// 19 | public partial class CredentialExtractorWindow : Window 20 | { 21 | public CredentialExtractorWindow() 22 | { 23 | InitializeComponent(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Extensions/DitExplorer.CredentialExtraction/DitExplorer.CredentialExtraction.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0-windows7.0 5 | enable 6 | true 7 | enable 8 | 9 | $(SolutionDir)_build\$(Configuration)\Extensions\DitExplorer.CredentialExtraction\ 10 | 11 | True 12 | 13 | DIT Explorer Credential Extractor 14 | 15 | README.md 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | True 28 | \ 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Code 45 | 46 | 47 | True 48 | True 49 | Messages.resx 50 | 51 | 52 | 53 | 54 | 55 | PublicResXFileCodeGenerator 56 | Messages.Designer.cs 57 | 58 | 59 | 60 | 61 | 62 | PreserveNewest 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Extensions/DitExplorer.CredentialExtraction/ExtractCredentialsAction.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.Ntds; 2 | using DitExplorer.WpfApp; 3 | using System.ComponentModel.Composition; 4 | using System.Windows; 5 | 6 | namespace DitExplorer.CredentialExtraction 7 | { 8 | [Export(typeof(ItemAction))] 9 | internal sealed class ExtractCredentialsAction : ItemAction 10 | { 11 | [ImportingConstructor] 12 | internal ExtractCredentialsAction() 13 | { 14 | 15 | } 16 | 17 | public override string MenuText => "E_xtract Credentials..."; 18 | 19 | public sealed override bool CanExecute(object[] items, IItemActionContext context) 20 | { 21 | bool found = false; 22 | foreach (var item in items) 23 | if (IsEligible(item)) 24 | { 25 | found = true; 26 | break; 27 | } 28 | return found; 29 | } 30 | 31 | private static bool IsEligible(object item) 32 | { 33 | if (item is IDirectoryNode node) 34 | { 35 | var objcls = node.Object.ObjectClass; 36 | bool hasCredentials = 37 | objcls.HasAttribute("unicodePwd") 38 | || objcls.HasAttribute("dBCSPwd") 39 | || objcls.HasAttribute("lmPwdHistory") 40 | || objcls.HasAttribute("ntPwdHistory") 41 | || objcls.HasAttribute("supplementalCredentials"); 42 | if (hasCredentials) 43 | return true; 44 | } 45 | 46 | return false; 47 | } 48 | 49 | public sealed override void Execute(object[] items, IItemActionContext context) 50 | { 51 | List nodes = new List(items.Length); 52 | foreach (var item in items) 53 | if (IsEligible(item)) 54 | nodes.Add((IDirectoryNode)item); 55 | 56 | if (nodes.Count > 0) 57 | { 58 | CredentialExtractorViewModel vm = new CredentialExtractorViewModel(nodes.ToArray()); 59 | CredentialExtractorWindow wnd = new CredentialExtractorWindow() 60 | { 61 | DataContext = vm, 62 | Owner = context.Owner, 63 | WindowStartupLocation = WindowStartupLocation.CenterOwner 64 | }; 65 | wnd.Show(); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Extensions/DitExplorer.CredentialExtraction/Messages.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace DitExplorer.CredentialExtraction { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | public class Messages { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Messages() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | public static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DitExplorer.CredentialExtraction.Messages", typeof(Messages).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | public static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to LM hash. 65 | /// 66 | public static string Credential_LmHash { 67 | get { 68 | return ResourceManager.GetString("Credential_LmHash", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to NT hash. 74 | /// 75 | public static string Credential_NtHash { 76 | get { 77 | return ResourceManager.GetString("Credential_NtHash", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to Credential Extractor. 83 | /// 84 | public static string CredentialExtractor_Title { 85 | get { 86 | return ResourceManager.GetString("CredentialExtractor_Title", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to E_xport.... 92 | /// 93 | public static string Export_Command { 94 | get { 95 | return ResourceManager.GetString("Export_Command", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to Export Credentials. 101 | /// 102 | public static string Export_Title { 103 | get { 104 | return ResourceManager.GetString("Export_Title", resourceCulture); 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Extensions/DitExplorer.CredentialExtraction/Messages.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Credential Extractor 122 | 123 | 124 | LM hash 125 | 126 | 127 | NT hash 128 | 129 | 130 | E_xport... 131 | 132 | 133 | Export Credentials 134 | 135 | -------------------------------------------------------------------------------- /Extensions/DitExplorer.CredentialExtraction/SidExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.Collections.Generic; 4 | using System.Data.SqlTypes; 5 | using System.Linq; 6 | using System.Security.Principal; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace DitExplorer.CredentialExtraction; 11 | internal static class SidExtensions 12 | { 13 | public static uint GetRid(this SecurityIdentifier sid) 14 | { 15 | byte[] sidBytes = new byte[sid.BinaryLength]; 16 | sid.GetBinaryForm(sidBytes, 0); 17 | var rid = BinaryPrimitives.ReadUInt32LittleEndian(new ReadOnlySpan(sidBytes).Slice(sid.BinaryLength - 4,4)); 18 | return rid; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Extensions/DitExplorer.CredentialExtraction/Structs.cs: -------------------------------------------------------------------------------- 1 | using DitExplorer.CredentialExtraction; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Runtime.InteropServices; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace DitExplorer.CredentialExtraction 11 | { 12 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 13 | struct Salt 14 | { 15 | const int SaltSize = 16; 16 | internal unsafe fixed byte salt[SaltSize]; 17 | public Span GetBytes() 18 | { 19 | unsafe 20 | { 21 | fixed (byte* pSalt = this.salt) 22 | { 23 | return new Span(pSalt, SaltSize); 24 | } 25 | } 26 | } 27 | } 28 | 29 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 30 | struct PekHeader 31 | { 32 | internal static unsafe int StructSize => sizeof(PekHeader); 33 | 34 | internal int versionMajor; 35 | internal int versionMinor; 36 | internal Salt salt; 37 | } 38 | 39 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 40 | struct FILETIME 41 | { 42 | int lo; 43 | int hi; 44 | } 45 | 46 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 47 | struct PekEncryptedHeader 48 | { 49 | internal static unsafe int StructSize => sizeof(PekEncryptedHeader); 50 | 51 | internal Salt auth; 52 | internal FILETIME ftModified; 53 | internal int unk1; 54 | internal int keyCount; 55 | internal int unk2; 56 | 57 | // Followed by a list of keys 58 | } 59 | 60 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 61 | struct PekList 62 | { 63 | internal static unsafe int StructSize => sizeof(PekList); 64 | internal const int KeySize = 16; 65 | internal const int SaltSize = 16; 66 | 67 | internal PekHeader hdr; 68 | internal PekEncryptedHeader encHdr; 69 | } 70 | 71 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 72 | struct SecretHeader 73 | { 74 | internal static unsafe int StructSize => sizeof(SecretHeader); 75 | 76 | internal const short Win2016SecretVersion = 0x13; 77 | 78 | internal short version; 79 | internal short unk1; 80 | internal int keyIndex; 81 | internal Salt salt; 82 | 83 | internal int cbData; 84 | } 85 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DIT Explorer 2 | DIT Explorer is a Windows application written in C# for browsing a NTDS.dit file. 3 | Created in Visual Studio 2022. 4 | 5 | I wrote this as a tool for researching the structure of NTDS.dit. For more 6 | information, see my [blog post](https://trustedsec.com/blog/exploring-ntds-dit-part-1-cracking-the-surface-with-dit-explorer). 7 | 8 | ![image](https://github.com/user-attachments/assets/8efd1f31-8d2d-4adc-9832-4b33c68b5dea) 9 | 10 | # Building 11 | To build the application: 12 | 13 | 1. Open DitExplorer.sln with Visual Studio 2022. 14 | 2. Build and run DitExplorer.UI.WpfApp 15 | 16 | # Using the Application 17 | 18 | From the main application window, open a .DIT file using File > Open DIT File. 19 | DIT Explorer uses ManagedEsent to open the database. If the file was pulled from a 20 | shadow copy and is unclean, you may need to repair it before opening it with 21 | DIT Explorer using `esent /p`. 22 | 23 | After opening a directory database, DIT Explorer displays the hierarchy of the 24 | domain on the left-hand side and the contents of the selected node in the right-hand pane. 25 | 26 | * To view the attributes, members, and groups of an object, double-click it or right-click `> Properties`. 27 | * To view the database schema, select `Tools > Database Schema` 28 | * To view the directory schema, navigate to Configuration\Schema under the domain. 29 | * To search a subtree, right-click the node at the root of the subtree, then select `Search Subtree`. 30 | * To change the attributes shown as columns, select `View > Columns`, or right-click the list view and select `Columns...` 31 | * To copy one or more items to the clipboard, highlight them, then select one of the Copy commands. 32 | * To dump hashes, select one or more accounts, then right-click and select `Extract Credentials` 33 | 34 | Most of the lists support sorting by clicking the column headers and searching by 35 | typing directly into the list. 36 | 37 | To perform an action (such as Extract Credential) on multiple objects within a 38 | subtree, search the subtree, highlight the objects in the search results, then 39 | right-click and select the desired action. 40 | 41 | # Customizing the View 42 | 43 | In both the main application window and the Search window, you may select which columns the list displays. 44 | Right-click in the list, then select `Columns...` 45 | 46 | ![image](https://github.com/user-attachments/assets/cc40cce8-3deb-49ca-885e-4bb27e1ab3a9) 47 | 48 | Column Chooser allows you to select which schema attributes to display as a column in the list view. 49 | The `Column set` selection allows you to browse the attributes contained by a particular class. 50 | 51 | # Searching the Directory 52 | 53 | To search the directory: 54 | 1. Right-click on the node you want to search in, then select `Search Subtree`. 55 | 2. Enter part of the name of the object you wish to search for, or leave the Name field blank to find all objects in the subtree. DIT Explorer searchs for the 56 | search text within the attributes marked ANR to produce a similar experience to searching for a 57 | user in Active Directory. 58 | 3. Optionally select the class of object to search for, optionally 59 | including subclasses. Leave the selection blank to search for all objects matching the name entered above. 60 | 4. Click `Search Now`. 61 | 5. Interact with the results by selecting and right-clicking them. 62 | 63 | ![image](https://github.com/user-attachments/assets/c7797618-b0ed-4c64-8cd0-ca939c73d8c7) 64 | 65 | # Extracting Credentials 66 | 67 | To extract credentials, you'll need the system key of the DC that you pulled the NTDS.dit file 68 | from. 69 | 70 | ![image](https://github.com/user-attachments/assets/b930e108-ae30-4e5d-9fe0-c6f3d6e5686f) 71 | 72 | 1. Right-click the user or computer that you want to extract credentials from, then select `Extract Credentials`. 73 | 3. Enter the system key. 74 | 4. Click `Extract Credentials`. 75 | 5. Click `Export...` to export the credentials to a file. DIT Explorer supports exporting to a tab-delimited text file, CSV, or pwdump-style text file. 76 | 77 | # Viewing the Database Schema 78 | 79 | ![image](https://github.com/user-attachments/assets/c3969d0e-2aba-4dc1-8b7c-6d01f9e73cc6) 80 | 81 | 1. Open the .DIT file. 82 | 2. From the `Tools` menu, select `Database Schema`. 83 | 84 | The right-hand pane lists the columns and indexes in the table. You may highlight and copy a list of columns or indexes to the clipboard. Use the `Export Table Data` button to export the raw data from the selected table. 85 | --------------------------------------------------------------------------------