< Summary

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