< Summary

Class:SharpHoundCommonLib.Processors.UserProperties
Assembly:SharpHoundCommonLib
File(s):D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Processors\LdapPropertyProcessor.cs
Covered lines:4
Uncovered lines:0
Coverable lines:4
Total lines:952
Line coverage:100% (4 of 4)
Covered branches:0
Total branches:0

File(s)

D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Processors\LdapPropertyProcessor.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Diagnostics.CodeAnalysis;
 4using System.Linq;
 5using System.Runtime.InteropServices;
 6using System.Security.AccessControl;
 7using System.Security.Cryptography.X509Certificates;
 8using System.Security.Principal;
 9using System.Threading.Tasks;
 10using Microsoft.Extensions.Logging;
 11using SharpHoundCommonLib.Enums;
 12using SharpHoundCommonLib.LDAPQueries;
 13using SharpHoundCommonLib.OutputTypes;
 14
 15// ReSharper disable StringLiteralTypo
 16
 17namespace SharpHoundCommonLib.Processors {
 18    public class LdapPropertyProcessor {
 19        private static readonly HashSet<string> ReservedAttributes = new();
 20
 21        static LdapPropertyProcessor() {
 22            ReservedAttributes.UnionWith(CommonProperties.TypeResolutionProps);
 23            ReservedAttributes.UnionWith(CommonProperties.BaseQueryProps);
 24            ReservedAttributes.UnionWith(CommonProperties.GroupResolutionProps);
 25            ReservedAttributes.UnionWith(CommonProperties.ComputerMethodProps);
 26            ReservedAttributes.UnionWith(CommonProperties.ACLProps);
 27            ReservedAttributes.UnionWith(CommonProperties.ObjectPropsProps);
 28            ReservedAttributes.UnionWith(CommonProperties.ContainerProps);
 29            ReservedAttributes.UnionWith(CommonProperties.SPNTargetProps);
 30            ReservedAttributes.UnionWith(CommonProperties.DomainTrustProps);
 31            ReservedAttributes.UnionWith(CommonProperties.GPOLocalGroupProps);
 32            ReservedAttributes.UnionWith(CommonProperties.CertAbuseProps);
 33            ReservedAttributes.Add(LDAPProperties.DSASignature);
 34        }
 35
 36        private readonly ILdapUtils _utils;
 37
 38        public LdapPropertyProcessor(ILdapUtils utils) {
 39            _utils = utils;
 40        }
 41
 42        private static Dictionary<string, object> GetCommonProps(IDirectoryObject entry) {
 43            var ret = new Dictionary<string, object>();
 44            if (entry.TryGetProperty(LDAPProperties.Description, out var description)) {
 45                ret["description"] = description;
 46            }
 47
 48            if (entry.TryGetProperty(LDAPProperties.WhenCreated, out var wc)) {
 49                ret["whencreated"] = Helpers.ConvertTimestampToUnixEpoch(wc);
 50            }
 51
 52            return ret;
 53        }
 54
 55        /// <summary>
 56        ///     Reads specific LDAP properties related to Domains
 57        /// </summary>
 58        /// <param name="entry"></param>
 59        /// <returns></returns>
 60        public async Task<Dictionary<string, object>> ReadDomainProperties(IDirectoryObject entry, string domain) {
 61            var props = GetCommonProps(entry);
 62
 63            if (entry.TryGetProperty(LDAPProperties.ExpirePasswordsOnSmartCardOnlyAccounts, out var expirePassword) &&
 64                bool.TryParse(expirePassword, out var expirePasswordBool)) {
 65                props.Add("expirepasswordsonsmartcardonlyaccounts", expirePasswordBool);
 66            }
 67
 68            if (entry.TryGetLongProperty(LDAPProperties.MachineAccountQuota, out var machineAccountQuota)) {
 69                props.Add("machineaccountquota", machineAccountQuota);
 70            }
 71
 72            if (entry.TryGetLongProperty(LDAPProperties.MinPwdLength, out var minPwdLength)) {
 73                props.Add("minpwdlength", minPwdLength);
 74            }
 75
 76            if (entry.TryGetLongProperty(LDAPProperties.PwdProperties, out var pwdProperties)) {
 77                props.Add("pwdproperties", pwdProperties);
 78            }
 79
 80            if (entry.TryGetLongProperty(LDAPProperties.PwdHistoryLength, out var pwdHistoryLength)) {
 81                props.Add("pwdhistorylength", pwdHistoryLength);
 82            }
 83
 84            if (entry.TryGetLongProperty(LDAPProperties.LockoutThreshold, out var lockoutThreshold)) {
 85                props.Add("lockoutthreshold", lockoutThreshold);
 86            }
 87
 88            if (entry.TryGetLongProperty(LDAPProperties.MinPwdAge, out var minpwdage)) {
 89                var duration = ConvertNanoDuration(minpwdage);
 90                if (duration != null) {
 91                    props.Add("minpwdage", duration);
 92                }
 93            }
 94
 95            if (entry.TryGetLongProperty(LDAPProperties.MaxPwdAge, out var maxpwdage)) {
 96                var duration = ConvertNanoDuration(maxpwdage);
 97                if (duration != null) {
 98                    props.Add("maxpwdage", duration);
 99                }
 100            }
 101
 102            if (entry.TryGetLongProperty(LDAPProperties.LockoutDuration, out var lockoutduration)) {
 103                var duration = ConvertNanoDuration(lockoutduration);
 104                if (duration != null) {
 105                    props.Add("lockoutduration", duration);
 106                }
 107            }
 108
 109            if (entry.TryGetLongProperty(LDAPProperties.LockOutObservationWindow, out var lockoutobservationwindow)) {
 110                var duration = ConvertNanoDuration(lockoutobservationwindow);
 111                if (duration != null) {
 112                    props.Add("lockoutobservationwindow", lockoutobservationwindow);
 113                }
 114            }
 115
 116            if (!entry.TryGetLongProperty(LDAPProperties.DomainFunctionalLevel, out var functionalLevel)) {
 117                functionalLevel = -1;
 118            }
 119
 120            props.Add("functionallevel", FunctionalLevelToString((int)functionalLevel));
 121
 122            var dn = entry.GetProperty(LDAPProperties.DistinguishedName);
 123            var dsh = await _utils.GetDSHueristics(domain, dn);
 124            props.Add("dsheuristics", dsh.DSHeuristics);
 125
 126            return props;
 127        }
 128
 129        /// <summary>
 130        ///     Converts a numeric representation of a functional level to its appropriate functional level string
 131        /// </summary>
 132        /// <param name="level"></param>
 133        /// <returns></returns>
 134        public static string FunctionalLevelToString(int level) {
 135            var functionalLevel = level switch {
 136                0 => "2000 Mixed/Native",
 137                1 => "2003 Interim",
 138                2 => "2003",
 139                3 => "2008",
 140                4 => "2008 R2",
 141                5 => "2012",
 142                6 => "2012 R2",
 143                7 => "2016",
 144                8 => "2025",
 145                _ => "Unknown"
 146            };
 147
 148            return functionalLevel;
 149        }
 150
 151        /// <summary>
 152        ///     Reads specific LDAP properties related to GPOs
 153        /// </summary>
 154        /// <param name="entry"></param>
 155        /// <returns></returns>
 156        public static Dictionary<string, object> ReadGPOProperties(IDirectoryObject entry) {
 157            var props = GetCommonProps(entry);
 158            entry.TryGetProperty(LDAPProperties.GPCFileSYSPath, out var path);
 159            props.Add("gpcpath", path.ToUpper());
 160            return props;
 161        }
 162
 163        /// <summary>
 164        ///     Reads specific LDAP properties related to OUs
 165        /// </summary>
 166        /// <param name="entry"></param>
 167        /// <returns></returns>
 168        public static Dictionary<string, object> ReadOUProperties(IDirectoryObject entry) {
 169            var props = GetCommonProps(entry);
 170            return props;
 171        }
 172
 173        /// <summary>
 174        ///     Reads specific LDAP properties related to Groups
 175        /// </summary>
 176        /// <param name="entry"></param>
 177        /// <returns></returns>
 178        public static Dictionary<string, object> ReadGroupProperties(IDirectoryObject entry) {
 179            var props = GetCommonProps(entry);
 180            entry.TryGetLongProperty(LDAPProperties.AdminCount, out var ac);
 181            props.Add("admincount", ac != 0);
 182            return props;
 183        }
 184
 185        /// <summary>
 186        ///     Reads specific LDAP properties related to containers
 187        /// </summary>
 188        /// <param name="entry"></param>
 189        /// <returns></returns>
 190        public static Dictionary<string, object> ReadContainerProperties(IDirectoryObject entry) {
 191            var props = GetCommonProps(entry);
 192            return props;
 193        }
 194
 195        public Task<UserProperties>
 196            ReadUserProperties(IDirectoryObject entry, ResolvedSearchResult searchResult) {
 197            return ReadUserProperties(entry, searchResult.Domain);
 198        }
 199
 200        /// <summary>
 201        ///     Reads specific LDAP properties related to Users
 202        /// </summary>
 203        /// <param name="entry"></param>
 204        /// <param name="domain"></param>
 205        /// <returns></returns>
 206        public async Task<UserProperties> ReadUserProperties(IDirectoryObject entry, string domain) {
 207            var userProps = new UserProperties();
 208            var props = GetCommonProps(entry);
 209
 210            var uacFlags = (UacFlags)0;
 211            if (entry.TryGetLongProperty(LDAPProperties.UserAccountControl, out var uac)) {
 212                uacFlags = (UacFlags)uac;
 213            }
 214
 215            props.Add("sensitive", uacFlags.HasFlag(UacFlags.NotDelegated));
 216            props.Add("dontreqpreauth", uacFlags.HasFlag(UacFlags.DontReqPreauth));
 217            props.Add("passwordnotreqd", uacFlags.HasFlag(UacFlags.PasswordNotRequired));
 218            props.Add("unconstraineddelegation", uacFlags.HasFlag(UacFlags.TrustedForDelegation));
 219            props.Add("pwdneverexpires", uacFlags.HasFlag(UacFlags.DontExpirePassword));
 220            props.Add("enabled", !uacFlags.HasFlag(UacFlags.AccountDisable));
 221            props.Add("trustedtoauth", uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation));
 222            props.Add("smartcardrequired", uacFlags.HasFlag(UacFlags.SmartcardRequired));
 223            props.Add("encryptedtextpwdallowed", uacFlags.HasFlag(UacFlags.EncryptedTextPwdAllowed));
 224            props.Add("usedeskeyonly", uacFlags.HasFlag(UacFlags.UseDesKeyOnly));
 225            props.Add("logonscriptenabled", uacFlags.HasFlag(UacFlags.Script));
 226            props.Add("lockedout", uacFlags.HasFlag(UacFlags.Lockout));
 227            props.Add("passwordcantchange", uacFlags.HasFlag(UacFlags.PasswordCantChange));
 228            props.Add("passwordexpired", uacFlags.HasFlag(UacFlags.PasswordExpired));
 229
 230            userProps.UnconstrainedDelegation = uacFlags.HasFlag(UacFlags.TrustedForDelegation);
 231
 232            var comps = new List<TypedPrincipal>();
 233            if (uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation) &&
 234                entry.TryGetArrayProperty(LDAPProperties.AllowedToDelegateTo, out var delegates)) {
 235                props.Add("allowedtodelegate", delegates);
 236
 237                foreach (var d in delegates) {
 238                    if (d == null)
 239                        continue;
 240
 241                    var resolvedHost = await _utils.ResolveHostToSid(d, domain);
 242                    if (resolvedHost.Success && resolvedHost.SecurityIdentifier.Contains("S-1"))
 243                        comps.Add(new TypedPrincipal {
 244                            ObjectIdentifier = resolvedHost.SecurityIdentifier,
 245                            ObjectType = Label.Computer
 246                        });
 247                }
 248            }
 249
 250            userProps.AllowedToDelegate = comps.Distinct().ToArray();
 251
 252            if (!entry.TryGetProperty(LDAPProperties.LastLogon, out var lastLogon)) {
 253                lastLogon = null;
 254            }
 255
 256            props.Add("lastlogon", Helpers.ConvertFileTimeToUnixEpoch(lastLogon));
 257
 258            if (!entry.TryGetProperty(LDAPProperties.LastLogonTimestamp, out var lastLogonTimeStamp)) {
 259                lastLogonTimeStamp = null;
 260            }
 261
 262            props.Add("lastlogontimestamp", Helpers.ConvertFileTimeToUnixEpoch(lastLogonTimeStamp));
 263
 264            if (!entry.TryGetProperty(LDAPProperties.PasswordLastSet, out var passwordLastSet)) {
 265                passwordLastSet = null;
 266            }
 267
 268            props.Add("pwdlastset",
 269                Helpers.ConvertFileTimeToUnixEpoch(passwordLastSet));
 270            entry.TryGetArrayProperty(LDAPProperties.ServicePrincipalNames, out var spn);
 271            props.Add("serviceprincipalnames", spn);
 272            props.Add("hasspn", spn.Length > 0);
 273            props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName));
 274            props.Add("email", entry.GetProperty(LDAPProperties.Email));
 275            props.Add("title", entry.GetProperty(LDAPProperties.Title));
 276            props.Add("homedirectory", entry.GetProperty(LDAPProperties.HomeDirectory));
 277            props.Add("userpassword", entry.GetProperty(LDAPProperties.UserPassword));
 278            props.Add("unixpassword", entry.GetProperty(LDAPProperties.UnixUserPassword));
 279            props.Add("unicodepassword", entry.GetProperty(LDAPProperties.UnicodePassword));
 280            props.Add("sfupassword", entry.GetProperty(LDAPProperties.MsSFU30Password));
 281            props.Add("logonscript", entry.GetProperty(LDAPProperties.ScriptPath));
 282            props.Add("useraccountcontrol", uac);
 283            props.Add("profilepath", entry.GetProperty(LDAPProperties.ProfilePath));
 284
 285            entry.TryGetLongProperty(LDAPProperties.AdminCount, out var ac);
 286            props.Add("admincount", ac != 0);
 287
 288            var encryptionTypes = ConvertEncryptionTypes(entry.GetProperty(LDAPProperties.SupportedEncryptionTypes));
 289            props.Add("supportedencryptiontypes", encryptionTypes);
 290
 291            entry.TryGetByteArrayProperty(LDAPProperties.SIDHistory, out var sh);
 292            var sidHistoryList = new List<string>();
 293            var sidHistoryPrincipals = new List<TypedPrincipal>();
 294            foreach (var sid in sh) {
 295                string sSid;
 296                try {
 297                    sSid = new SecurityIdentifier(sid, 0).Value;
 298                } catch {
 299                    continue;
 300                }
 301
 302                sidHistoryList.Add(sSid);
 303
 304                if (await _utils.ResolveIDAndType(sSid, domain) is (true, var res))
 305                    sidHistoryPrincipals.Add(res);
 306            }
 307
 308            userProps.SidHistory = sidHistoryPrincipals.Distinct().ToArray();
 309
 310            props.Add("sidhistory", sidHistoryList.ToArray());
 311
 312            userProps.Props = props;
 313
 314            return userProps;
 315        }
 316
 317        public Task<ComputerProperties> ReadComputerProperties(IDirectoryObject entry,
 318            ResolvedSearchResult searchResult) {
 319            return ReadComputerProperties(entry, searchResult.Domain);
 320        }
 321
 322        /// <summary>
 323        ///     Reads specific LDAP properties related to Computers
 324        /// </summary>
 325        /// <param name="entry"></param>
 326        /// <param name="domain"></param>
 327        /// <returns></returns>
 328        public async Task<ComputerProperties> ReadComputerProperties(IDirectoryObject entry, string domain) {
 329            var compProps = new ComputerProperties();
 330            var props = GetCommonProps(entry);
 331
 332            var flags = (UacFlags)0;
 333            if (entry.TryGetLongProperty(LDAPProperties.UserAccountControl, out var uac)) {
 334                flags = (UacFlags)uac;
 335            }
 336
 337            props.Add("enabled", !flags.HasFlag(UacFlags.AccountDisable));
 338            props.Add("unconstraineddelegation", flags.HasFlag(UacFlags.TrustedForDelegation));
 339            props.Add("trustedtoauth", flags.HasFlag(UacFlags.TrustedToAuthForDelegation));
 340            props.Add("isdc", flags.HasFlag(UacFlags.ServerTrustAccount));
 341            props.Add("encryptedtextpwdallowed", flags.HasFlag(UacFlags.EncryptedTextPwdAllowed));
 342            props.Add("usedeskeyonly", flags.HasFlag(UacFlags.UseDesKeyOnly));
 343            props.Add("logonscriptenabled", flags.HasFlag(UacFlags.Script));
 344            props.Add("lockedout", flags.HasFlag(UacFlags.Lockout));
 345            props.Add("passwordexpired", flags.HasFlag(UacFlags.PasswordExpired));
 346
 347            compProps.UnconstrainedDelegation = flags.HasFlag(UacFlags.TrustedForDelegation);
 348
 349            var encryptionTypes = ConvertEncryptionTypes(entry.GetProperty(LDAPProperties.SupportedEncryptionTypes));
 350            props.Add("supportedencryptiontypes", encryptionTypes);
 351
 352            var comps = new List<TypedPrincipal>();
 353            if (flags.HasFlag(UacFlags.TrustedToAuthForDelegation) &&
 354                entry.TryGetArrayProperty(LDAPProperties.AllowedToDelegateTo, out var delegates)) {
 355                props.Add("allowedtodelegate", delegates);
 356
 357                foreach (var d in delegates) {
 358                    if (d == null)
 359                        continue;
 360
 361                    var resolvedHost = await _utils.ResolveHostToSid(d, domain);
 362                    if (resolvedHost.Success && resolvedHost.SecurityIdentifier.Contains("S-1"))
 363                        comps.Add(new TypedPrincipal {
 364                            ObjectIdentifier = resolvedHost.SecurityIdentifier,
 365                            ObjectType = Label.Computer
 366                        });
 367                }
 368            }
 369
 370            compProps.AllowedToDelegate = comps.Distinct().ToArray();
 371
 372            var allowedToActPrincipals = new List<TypedPrincipal>();
 373            if (entry.TryGetByteProperty(LDAPProperties.AllowedToActOnBehalfOfOtherIdentity, out var rawAllowedToAct)) {
 374                var sd = _utils.MakeSecurityDescriptor();
 375                sd.SetSecurityDescriptorBinaryForm(rawAllowedToAct, AccessControlSections.Access);
 376                foreach (var rule in sd.GetAccessRules(true, true, typeof(SecurityIdentifier))) {
 377                    if (await _utils.ResolveIDAndType(rule.IdentityReference(), domain) is (true, var res))
 378                        allowedToActPrincipals.Add(res);
 379                }
 380            }
 381
 382            compProps.AllowedToAct = allowedToActPrincipals.ToArray();
 383
 384            props.Add("lastlogon", Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogon)));
 385            props.Add("lastlogontimestamp",
 386                Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogonTimestamp)));
 387            props.Add("pwdlastset",
 388                Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.PasswordLastSet)));
 389            entry.TryGetArrayProperty(LDAPProperties.ServicePrincipalNames, out var spn);
 390            props.Add("serviceprincipalnames", spn);
 391            props.Add("email", entry.GetProperty(LDAPProperties.Email));
 392            props.Add("useraccountcontrol", uac);
 393            var os = entry.GetProperty(LDAPProperties.OperatingSystem);
 394            var sp = entry.GetProperty(LDAPProperties.ServicePack);
 395
 396            if (sp != null) os = $"{os} {sp}";
 397
 398            props.Add("operatingsystem", os);
 399
 400            entry.TryGetByteArrayProperty(LDAPProperties.SIDHistory, out var sh);
 401            var sidHistoryList = new List<string>();
 402            var sidHistoryPrincipals = new List<TypedPrincipal>();
 403            foreach (var sid in sh) {
 404                string sSid;
 405                try {
 406                    sSid = new SecurityIdentifier(sid, 0).Value;
 407                } catch {
 408                    continue;
 409                }
 410
 411                sidHistoryList.Add(sSid);
 412
 413                if (await _utils.ResolveIDAndType(sSid, domain) is (true, var res))
 414                    sidHistoryPrincipals.Add(res);
 415            }
 416
 417            compProps.SidHistory = sidHistoryPrincipals.ToArray();
 418
 419            props.Add("sidhistory", sidHistoryList.ToArray());
 420
 421            var smsaPrincipals = new List<TypedPrincipal>();
 422            if (entry.TryGetArrayProperty(LDAPProperties.HostServiceAccount, out var hsa)) {
 423                foreach (var dn in hsa) {
 424                    if (await _utils.ResolveDistinguishedName(dn) is (true, var resolvedPrincipal))
 425                        smsaPrincipals.Add(resolvedPrincipal);
 426                }
 427            }
 428
 429            compProps.DumpSMSAPassword = smsaPrincipals.ToArray();
 430
 431            compProps.Props = props;
 432
 433            return compProps;
 434        }
 435
 436        /// <summary>
 437        /// Returns the properties associated with the RootCA
 438        /// </summary>
 439        /// <param name="entry"></param>
 440        /// <returns>Returns a dictionary with the common properties of the RootCA</returns>
 441        public static Dictionary<string, object> ReadRootCAProperties(IDirectoryObject entry) {
 442            var props = GetCommonProps(entry);
 443
 444            // Certificate
 445            if (entry.TryGetByteProperty(LDAPProperties.CACertificate, out var rawCertificate)) {
 446                var cert = new ParsedCertificate(rawCertificate);
 447                props.Add("certthumbprint", cert.Thumbprint);
 448                props.Add("certname", cert.Name);
 449                props.Add("certchain", cert.Chain);
 450                props.Add("hasbasicconstraints", cert.HasBasicConstraints);
 451                props.Add("basicconstraintpathlength", cert.BasicConstraintPathLength);
 452            }
 453
 454            return props;
 455        }
 456
 457        /// <summary>
 458        /// Returns the properties associated with the AIACA
 459        /// </summary>
 460        /// <param name="entry"></param>
 461        /// <returns>Returns a dictionary with the common properties and the crosscertificatepair property of the AICA</
 462        public static Dictionary<string, object> ReadAIACAProperties(IDirectoryObject entry) {
 463            var props = GetCommonProps(entry);
 464            entry.TryGetByteArrayProperty(LDAPProperties.CrossCertificatePair, out var crossCertificatePair);
 465            var hasCrossCertificatePair = crossCertificatePair.Length > 0;
 466
 467            props.Add("crosscertificatepair", crossCertificatePair);
 468            props.Add("hascrosscertificatepair", hasCrossCertificatePair);
 469
 470            // Certificate
 471            if (entry.TryGetByteProperty(LDAPProperties.CACertificate, out var rawCertificate)) {
 472                var cert = new ParsedCertificate(rawCertificate);
 473                props.Add("certthumbprint", cert.Thumbprint);
 474                props.Add("certname", cert.Name);
 475                props.Add("certchain", cert.Chain);
 476                props.Add("hasbasicconstraints", cert.HasBasicConstraints);
 477                props.Add("basicconstraintpathlength", cert.BasicConstraintPathLength);
 478            }
 479
 480            return props;
 481        }
 482
 483        public static Dictionary<string, object> ReadEnterpriseCAProperties(IDirectoryObject entry) {
 484            var props = GetCommonProps(entry);
 485            if (entry.TryGetLongProperty("flags", out var flags))
 486                props.Add("flags", (PKICertificateAuthorityFlags)flags);
 487            props.Add("caname", entry.GetProperty(LDAPProperties.Name));
 488            props.Add("dnshostname", entry.GetProperty(LDAPProperties.DNSHostName));
 489
 490            // Certificate
 491            if (entry.TryGetByteProperty(LDAPProperties.CACertificate, out var rawCertificate)) {
 492                var cert = new ParsedCertificate(rawCertificate);
 493                props.Add("certthumbprint", cert.Thumbprint);
 494                props.Add("certname", cert.Name);
 495                props.Add("certchain", cert.Chain);
 496                props.Add("hasbasicconstraints", cert.HasBasicConstraints);
 497                props.Add("basicconstraintpathlength", cert.BasicConstraintPathLength);
 498            }
 499
 500            return props;
 501        }
 502
 503        /// <summary>
 504        /// Returns the properties associated with the NTAuthStore. These properties will only contain common properties
 505        /// </summary>
 506        /// <param name="entry"></param>
 507        /// <returns>Returns a dictionary with the common properties of the NTAuthStore</returns>
 508        public static Dictionary<string, object> ReadNTAuthStoreProperties(IDirectoryObject entry) {
 509            var props = GetCommonProps(entry);
 510            return props;
 511        }
 512
 513        /// <summary>
 514        /// Reads specific LDAP properties related to CertTemplates
 515        /// </summary>
 516        /// <param name="entry"></param>
 517        /// <returns>Returns a dictionary associated with the CertTemplate properties that were read</returns>
 518        public static Dictionary<string, object> ReadCertTemplateProperties(IDirectoryObject entry) {
 519            var props = GetCommonProps(entry);
 520
 521            props.Add("validityperiod", ConvertPKIPeriod(entry.GetByteProperty(LDAPProperties.PKIExpirationPeriod)));
 522            props.Add("renewalperiod", ConvertPKIPeriod(entry.GetByteProperty(LDAPProperties.PKIOverlappedPeriod)));
 523
 524            if (entry.TryGetLongProperty(LDAPProperties.TemplateSchemaVersion, out var schemaVersion))
 525                props.Add("schemaversion", schemaVersion);
 526
 527            props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName));
 528            props.Add("oid", entry.GetProperty(LDAPProperties.CertTemplateOID));
 529
 530            if (entry.TryGetLongProperty(LDAPProperties.PKIEnrollmentFlag, out var enrollmentFlagsRaw)) {
 531                var enrollmentFlags = (PKIEnrollmentFlag)enrollmentFlagsRaw;
 532
 533                props.Add("enrollmentflag", enrollmentFlags);
 534                props.Add("requiresmanagerapproval", enrollmentFlags.HasFlag(PKIEnrollmentFlag.PEND_ALL_REQUESTS));
 535                props.Add("nosecurityextension", enrollmentFlags.HasFlag(PKIEnrollmentFlag.NO_SECURITY_EXTENSION));
 536            }
 537
 538            if (entry.TryGetLongProperty(LDAPProperties.PKINameFlag, out var nameFlagsRaw)) {
 539                var nameFlags = (PKICertificateNameFlag)nameFlagsRaw;
 540
 541                props.Add("certificatenameflag", nameFlags);
 542                props.Add("enrolleesuppliessubject",
 543                    nameFlags.HasFlag(PKICertificateNameFlag.ENROLLEE_SUPPLIES_SUBJECT));
 544                props.Add("subjectaltrequireupn",
 545                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_UPN));
 546                props.Add("subjectaltrequiredns",
 547                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_DNS));
 548                props.Add("subjectaltrequiredomaindns",
 549                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_DOMAIN_DNS));
 550                props.Add("subjectaltrequireemail",
 551                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_EMAIL));
 552                props.Add("subjectaltrequirespn",
 553                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_SPN));
 554                props.Add("subjectrequireemail",
 555                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_REQUIRE_EMAIL));
 556            }
 557
 558            entry.TryGetArrayProperty(LDAPProperties.ExtendedKeyUsage, out var ekus);
 559            props.Add("ekus", ekus);
 560            entry.TryGetArrayProperty(LDAPProperties.CertificateApplicationPolicy,
 561                out var certificateApplicationPolicy);
 562            props.Add("certificateapplicationpolicy", certificateApplicationPolicy);
 563
 564            entry.TryGetArrayProperty(LDAPProperties.CertificatePolicy, out var certificatePolicy);
 565            props.Add("certificatepolicy", certificatePolicy);
 566
 567            if (entry.TryGetLongProperty(LDAPProperties.NumSignaturesRequired, out var authorizedSignatures))
 568                props.Add("authorizedsignatures", authorizedSignatures);
 569
 570            var hasUseLegacyProvider = false;
 571            if (entry.TryGetLongProperty(LDAPProperties.PKIPrivateKeyFlag, out var privateKeyFlagsRaw)) {
 572                var privateKeyFlags = (PKIPrivateKeyFlag)privateKeyFlagsRaw;
 573                hasUseLegacyProvider = privateKeyFlags.HasFlag(PKIPrivateKeyFlag.USE_LEGACY_PROVIDER);
 574            }
 575
 576            entry.TryGetArrayProperty(LDAPProperties.ApplicationPolicies, out var appPolicies);
 577
 578            props.Add("applicationpolicies",
 579                ParseCertTemplateApplicationPolicies(appPolicies,
 580                    (int)schemaVersion, hasUseLegacyProvider));
 581            entry.TryGetArrayProperty(LDAPProperties.IssuancePolicies, out var issuancePolicies);
 582            props.Add("issuancepolicies", issuancePolicies);
 583
 584            // Construct effectiveekus
 585            var effectiveekus = schemaVersion == 1 & ekus.Length > 0 ? ekus : certificateApplicationPolicy;
 586            props.Add("effectiveekus", effectiveekus);
 587
 588            // Construct authenticationenabled
 589            var authenticationEnabled =
 590                effectiveekus.Intersect(Helpers.AuthenticationOIDs).Any() | effectiveekus.Length == 0;
 591            props.Add("authenticationenabled", authenticationEnabled);
 592
 593            // Construct schannelauthenticationenabled
 594            var schannelAuthenticationEnabled =
 595                effectiveekus.Intersect(Helpers.SchannelAuthenticationOIDs).Any() | effectiveekus.Length == 0;
 596            props.Add("schannelauthenticationenabled", schannelAuthenticationEnabled);
 597
 598            return props;
 599        }
 600
 601        public async Task<IssuancePolicyProperties> ReadIssuancePolicyProperties(IDirectoryObject entry) {
 602            var ret = new IssuancePolicyProperties();
 603            var props = GetCommonProps(entry);
 604            props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName));
 605            props.Add("certtemplateoid", entry.GetProperty(LDAPProperties.CertTemplateOID));
 606
 607            if (entry.TryGetProperty(LDAPProperties.OIDGroupLink, out var link)) {
 608                if (await _utils.ResolveDistinguishedName(link) is (true, var linkedGroup)) {
 609                    props.Add("oidgrouplink", linkedGroup.ObjectIdentifier);
 610                    ret.GroupLink = linkedGroup;
 611                }
 612            }
 613
 614            ret.Props = props;
 615            return ret;
 616        }
 617
 618        /// <summary>
 619        ///     Attempts to parse all LDAP attributes outside of the ones already collected and converts them to a human
 620        ///     format using a best guess
 621        /// </summary>
 622        /// <param name="entry"></param>
 623        public Dictionary<string, object> ParseAllProperties(IDirectoryObject entry) {
 624            var props = new Dictionary<string, object>();
 625
 626            foreach (var property in entry.PropertyNames()) {
 627                if (ReservedAttributes.Contains(property, StringComparer.OrdinalIgnoreCase))
 628                    continue;
 629
 630                var collCount = entry.PropertyCount(property);
 631                if (collCount == 0)
 632                    continue;
 633
 634                if (collCount == 1) {
 635                    var testString = entry.GetProperty(property);
 636                    if (!string.IsNullOrEmpty(testString)) {
 637                        if (property.Equals("badpasswordtime", StringComparison.OrdinalIgnoreCase))
 638                            props.Add(property, Helpers.ConvertFileTimeToUnixEpoch(testString));
 639                        else
 640                            props.Add(property, BestGuessConvert(testString));
 641                    }
 642                } else {
 643                    if (entry.TryGetByteProperty(property, out var testBytes)) {
 644                        if (testBytes == null || testBytes.Length == 0) {
 645                            continue;
 646                        }
 647
 648                        // SIDs
 649                        try {
 650                            var sid = new SecurityIdentifier(testBytes, 0);
 651                            props.Add(property, sid.Value);
 652                            continue;
 653                        } catch {
 654                            /* Ignore */
 655                        }
 656
 657                        // GUIDs
 658                        try {
 659                            var guid = new Guid(testBytes);
 660                            props.Add(property, guid.ToString());
 661                            continue;
 662                        } catch {
 663                            /* Ignore */
 664                        }
 665                    }
 666
 667                    if (entry.TryGetArrayProperty(property, out var arr) && arr.Length > 0) {
 668                        props.Add(property, arr.Select(BestGuessConvert).ToArray());
 669                    }
 670                }
 671            }
 672
 673            return props;
 674        }
 675
 676        /// <summary>
 677        ///     Parse CertTemplate attribute msPKI-RA-Application-Policies
 678        /// </summary>
 679        /// <param name="applicationPolicies"></param>
 680        /// <param name="schemaVersion"></param>
 681        /// <param name="hasUseLegacyProvider"></param>
 682        private static string[] ParseCertTemplateApplicationPolicies(string[] applicationPolicies, int schemaVersion,
 683            bool hasUseLegacyProvider) {
 684            if (applicationPolicies == null
 685                || applicationPolicies.Length == 0
 686                || schemaVersion == 1
 687                || schemaVersion == 2
 688                || (schemaVersion == 4 && hasUseLegacyProvider)) {
 689                return applicationPolicies;
 690            } else {
 691                // Format: "Name`Type`Value`Name`Type`Value`..."
 692                // (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/c55ec697-be3f-4117-8316-8895e4
 693                // Return the Value of Name = "msPKI-RA-Application-Policies" entries
 694                var entries = applicationPolicies[0].Split('`');
 695                return Enumerable.Range(0, entries.Length / 3)
 696                    .Select(i => entries.Skip(i * 3).Take(3).ToArray())
 697                    .Where(parts => parts.Length == 3 && parts[0].Equals(LDAPProperties.ApplicationPolicies,
 698                        StringComparison.OrdinalIgnoreCase))
 699                    .Select(parts => parts[2])
 700                    .ToArray();
 701            }
 702        }
 703
 704        /// <summary>
 705        ///     Does a best guess conversion of the property to a type useable by the UI
 706        /// </summary>
 707        /// <param name="value"></param>
 708        /// <returns></returns>
 709        private static object BestGuessConvert(string value) {
 710            //Parse boolean values
 711            if (bool.TryParse(value, out var boolResult)) return boolResult;
 712
 713            //A string ending with 0Z is likely a timestamp
 714            if (value.EndsWith("0Z")) return Helpers.ConvertTimestampToUnixEpoch(value);
 715
 716            //This string corresponds to the max int, and is usually set in accountexpires
 717            if (value == "9223372036854775807") return -1;
 718
 719            //Try parsing as an int
 720            if (int.TryParse(value, out var num)) return num;
 721
 722            // If we have binary unicode, encode it
 723            foreach (char c in value) {
 724                if (char.IsControl(c)) return System.Text.Encoding.UTF8.GetBytes(value);
 725            }
 726
 727            //Just return the property as a string
 728            return value;
 729        }
 730
 731        private static List<string> ConvertEncryptionTypes(string encryptionTypes) {
 732            if (encryptionTypes == null) {
 733                return null;
 734            }
 735
 736            int encryptionTypesInt = Int32.Parse(encryptionTypes);
 737            List<string> supportedEncryptionTypes = new List<string>();
 738            if (encryptionTypesInt == 0) {
 739                supportedEncryptionTypes.Add("Not defined");
 740            }
 741
 742            if ((encryptionTypesInt & KerberosEncryptionTypes.DES_CBC_CRC) == KerberosEncryptionTypes.DES_CBC_CRC) {
 743                supportedEncryptionTypes.Add("DES-CBC-CRC");
 744            }
 745
 746            if ((encryptionTypesInt & KerberosEncryptionTypes.DES_CBC_MD5) == KerberosEncryptionTypes.DES_CBC_MD5) {
 747                supportedEncryptionTypes.Add("DES-CBC-MD5");
 748            }
 749
 750            if ((encryptionTypesInt & KerberosEncryptionTypes.RC4_HMAC_MD5) == KerberosEncryptionTypes.RC4_HMAC_MD5) {
 751                supportedEncryptionTypes.Add("RC4-HMAC-MD5");
 752            }
 753
 754            if ((encryptionTypesInt & KerberosEncryptionTypes.AES128_CTS_HMAC_SHA1_96) ==
 755                KerberosEncryptionTypes.AES128_CTS_HMAC_SHA1_96) {
 756                supportedEncryptionTypes.Add("AES128-CTS-HMAC-SHA1-96");
 757            }
 758
 759            if ((encryptionTypesInt & KerberosEncryptionTypes.AES256_CTS_HMAC_SHA1_96) ==
 760                KerberosEncryptionTypes.AES256_CTS_HMAC_SHA1_96) {
 761                supportedEncryptionTypes.Add("AES256-CTS-HMAC-SHA1-96");
 762            }
 763
 764            return supportedEncryptionTypes;
 765        }
 766
 767        private static string ConvertNanoDuration(long duration) {
 768            // In case duration is long.MinValue, Math.Abs will overflow.  Value represents Forever or Never
 769            if (duration == long.MinValue) {
 770                return "Forever";
 771                // And if the value is positive, it indicates an error code
 772            } else if (duration > 0) {
 773                return null;
 774            }
 775
 776            // duration is in 100-nanosecond intervals
 777            // Convert it to TimeSpan (which uses 1 tick = 100 nanoseconds)
 778            TimeSpan durationSpan = TimeSpan.FromTicks(Math.Abs(duration));
 779
 780            // Create a list to hold non-zero time components
 781            List<string> timeComponents = new List<string>();
 782
 783            // Add each time component if it's greater than zero
 784            if (durationSpan.Days > 0) {
 785                timeComponents.Add($"{durationSpan.Days} {(durationSpan.Days == 1 ? "day" : "days")}");
 786            }
 787
 788            if (durationSpan.Hours > 0) {
 789                timeComponents.Add($"{durationSpan.Hours} {(durationSpan.Hours == 1 ? "hour" : "hours")}");
 790            }
 791
 792            if (durationSpan.Minutes > 0) {
 793                timeComponents.Add($"{durationSpan.Minutes} {(durationSpan.Minutes == 1 ? "minute" : "minutes")}");
 794            }
 795
 796            if (durationSpan.Seconds > 0) {
 797                timeComponents.Add($"{durationSpan.Seconds} {(durationSpan.Seconds == 1 ? "second" : "seconds")}");
 798            }
 799
 800            // Join the non-zero components into a single readable string
 801            string readableDuration = string.Join(", ", timeComponents);
 802
 803            return readableDuration;
 804        }
 805
 806        /// <summary>
 807        ///     Converts PKIExpirationPeriod/PKIOverlappedPeriod attributes to time approximate times
 808        /// </summary>
 809        /// <remarks>https://www.sysadmins.lv/blog-en/how-to-convert-pkiexirationperiod-and-pkioverlapperiod-active-dire
 810        /// <param name="bytes"></param>
 811        /// <returns>Returns a string representing the time period associated with the input byte array in a human reada
 812        private static string ConvertPKIPeriod(byte[] bytes) {
 813            if (bytes == null || bytes.Length == 0)
 814                return "Unknown";
 815
 816            try {
 817                Array.Reverse(bytes);
 818                var temp = BitConverter.ToString(bytes).Replace("-", "");
 819                var value = Convert.ToInt64(temp, 16) * -.0000001;
 820
 821                if (value % 31536000 == 0 && value / 31536000 >= 1) {
 822                    if (value / 31536000 == 1) return "1 year";
 823
 824                    return $"{value / 31536000} years";
 825                }
 826
 827                if (value % 2592000 == 0 && value / 2592000 >= 1) {
 828                    if (value / 2592000 == 1) return "1 month";
 829
 830                    return $"{value / 2592000} months";
 831                }
 832
 833                if (value % 604800 == 0 && value / 604800 >= 1) {
 834                    if (value / 604800 == 1) return "1 week";
 835
 836                    return $"{value / 604800} weeks";
 837                }
 838
 839                if (value % 86400 == 0 && value / 86400 >= 1) {
 840                    if (value / 86400 == 1) return "1 day";
 841
 842                    return $"{value / 86400} days";
 843                }
 844
 845                if (value % 3600 == 0 && value / 3600 >= 1) {
 846                    if (value / 3600 == 1) return "1 hour";
 847
 848                    return $"{value / 3600} hours";
 849                }
 850
 851                return "";
 852            } catch (Exception) {
 853                return "Unknown";
 854            }
 855        }
 856
 857        [DllImport("Advapi32", SetLastError = false)]
 858        private static extern bool IsTextUnicode(byte[] buf, int len, ref IsTextUnicodeFlags opt);
 859
 860        [Flags]
 861        [SuppressMessage("ReSharper", "UnusedMember.Local")]
 862        [SuppressMessage("ReSharper", "InconsistentNaming")]
 863        private enum IsTextUnicodeFlags {
 864            IS_TEXT_UNICODE_ASCII16 = 0x0001,
 865            IS_TEXT_UNICODE_REVERSE_ASCII16 = 0x0010,
 866
 867            IS_TEXT_UNICODE_STATISTICS = 0x0002,
 868            IS_TEXT_UNICODE_REVERSE_STATISTICS = 0x0020,
 869
 870            IS_TEXT_UNICODE_CONTROLS = 0x0004,
 871            IS_TEXT_UNICODE_REVERSE_CONTROLS = 0x0040,
 872
 873            IS_TEXT_UNICODE_SIGNATURE = 0x0008,
 874            IS_TEXT_UNICODE_REVERSE_SIGNATURE = 0x0080,
 875
 876            IS_TEXT_UNICODE_ILLEGAL_CHARS = 0x0100,
 877            IS_TEXT_UNICODE_ODD_LENGTH = 0x0200,
 878            IS_TEXT_UNICODE_DBCS_LEADBYTE = 0x0400,
 879            IS_TEXT_UNICODE_NULL_BYTES = 0x1000,
 880
 881            IS_TEXT_UNICODE_UNICODE_MASK = 0x000F,
 882            IS_TEXT_UNICODE_REVERSE_MASK = 0x00F0,
 883            IS_TEXT_UNICODE_NOT_UNICODE_MASK = 0x0F00,
 884            IS_TEXT_UNICODE_NOT_ASCII_MASK = 0xF000
 885        }
 886    }
 887
 888    public class ParsedCertificate {
 889        public string Thumbprint { get; set; }
 890        public string Name { get; set; }
 891        public string[] Chain { get; set; }
 892        public bool HasBasicConstraints { get; set; }
 893        public int BasicConstraintPathLength { get; set; }
 894
 895        public ParsedCertificate(byte[] rawCertificate) {
 896            var parsedCertificate = new X509Certificate2(rawCertificate);
 897            Thumbprint = parsedCertificate.Thumbprint;
 898            var name = parsedCertificate.FriendlyName;
 899            Name = string.IsNullOrEmpty(name) ? Thumbprint : name;
 900
 901            // Chain
 902            try {
 903                var chain = new X509Chain();
 904                chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
 905                chain.Build(parsedCertificate);
 906                var temp = new List<string>();
 907                foreach (var cert in chain.ChainElements) temp.Add(cert.Certificate.Thumbprint);
 908                Chain = temp.ToArray();
 909            } catch (Exception e) {
 910                Logging.LogProvider.CreateLogger("ParsedCertificate").LogWarning(e,
 911                    "Failed to read certificate chain for certificate {Name} with Algo {Algorithm}", name,
 912                    parsedCertificate.SignatureAlgorithm.FriendlyName);
 913                Chain = Array.Empty<string>();
 914            }
 915
 916
 917            // Extensions
 918            var extensions = parsedCertificate.Extensions;
 919            foreach (var extension in extensions) {
 920                var certificateExtension = new CertificateExtension(extension);
 921                switch (certificateExtension.Oid.Value) {
 922                    case CAExtensionTypes.BasicConstraints:
 923                        var ext = (X509BasicConstraintsExtension)extension;
 924                        HasBasicConstraints = ext.HasPathLengthConstraint;
 925                        BasicConstraintPathLength = ext.PathLengthConstraint;
 926                        break;
 927                }
 928            }
 929        }
 930    }
 931
 932    public class UserProperties {
 12933        public Dictionary<string, object> Props { get; set; } = new();
 9934        public TypedPrincipal[] AllowedToDelegate { get; set; } = Array.Empty<TypedPrincipal>();
 10935        public TypedPrincipal[] SidHistory { get; set; } = Array.Empty<TypedPrincipal>();
 4936        public bool UnconstrainedDelegation { get; set; }
 937    }
 938
 939    public class ComputerProperties {
 940        public Dictionary<string, object> Props { get; set; } = new();
 941        public TypedPrincipal[] AllowedToDelegate { get; set; } = Array.Empty<TypedPrincipal>();
 942        public TypedPrincipal[] AllowedToAct { get; set; } = Array.Empty<TypedPrincipal>();
 943        public TypedPrincipal[] SidHistory { get; set; } = Array.Empty<TypedPrincipal>();
 944        public TypedPrincipal[] DumpSMSAPassword { get; set; } = Array.Empty<TypedPrincipal>();
 945        public bool UnconstrainedDelegation { get; set; }
 946    }
 947
 948    public class IssuancePolicyProperties {
 949        public Dictionary<string, object> Props { get; set; } = new();
 950        public TypedPrincipal GroupLink { get; set; } = new TypedPrincipal();
 951    }
 952}