< Summary

Class:SharpHoundCommonLib.Processors.UserProperties
Assembly:SharpHoundCommonLib
File(s):D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Processors\LDAPPropertyProcessor.cs
Covered lines:3
Uncovered lines:0
Coverable lines:3
Total lines:819
Line coverage:100% (3 of 3)
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.Reflection;
 6using System.Runtime.InteropServices;
 7using System.Security.AccessControl;
 8using System.Security.Cryptography.X509Certificates;
 9using System.Security.Principal;
 10using System.Threading.Tasks;
 11using SharpHoundCommonLib.Enums;
 12using SharpHoundCommonLib.LDAPQueries;
 13using SharpHoundCommonLib.OutputTypes;
 14
 15namespace SharpHoundCommonLib.Processors
 16{
 17    public class LDAPPropertyProcessor
 18    {
 19        private static readonly string[] ReservedAttributes = CommonProperties.TypeResolutionProps
 20            .Concat(CommonProperties.BaseQueryProps).Concat(CommonProperties.GroupResolutionProps)
 21            .Concat(CommonProperties.ComputerMethodProps).Concat(CommonProperties.ACLProps)
 22            .Concat(CommonProperties.ObjectPropsProps).Concat(CommonProperties.ContainerProps)
 23            .Concat(CommonProperties.SPNTargetProps).Concat(CommonProperties.DomainTrustProps)
 24            .Concat(CommonProperties.GPOLocalGroupProps).ToArray();
 25
 26        private readonly ILDAPUtils _utils;
 27
 28        public LDAPPropertyProcessor(ILDAPUtils utils)
 29        {
 30            _utils = utils;
 31        }
 32
 33        private static Dictionary<string, object> GetCommonProps(ISearchResultEntry entry)
 34        {
 35            return new Dictionary<string, object>
 36            {
 37                {
 38                    "description", entry.GetProperty(LDAPProperties.Description)
 39                },
 40                {
 41                    "whencreated", Helpers.ConvertTimestampToUnixEpoch(entry.GetProperty(LDAPProperties.WhenCreated))
 42                }
 43            };
 44        }
 45
 46        /// <summary>
 47        ///     Reads specific LDAP properties related to Domains
 48        /// </summary>
 49        /// <param name="entry"></param>
 50        /// <returns></returns>
 51        public static Dictionary<string, object> ReadDomainProperties(ISearchResultEntry entry)
 52        {
 53            var props = GetCommonProps(entry);
 54
 55            if (!int.TryParse(entry.GetProperty(LDAPProperties.DomainFunctionalLevel), out var level)) level = -1;
 56
 57            props.Add("functionallevel", FunctionalLevelToString(level));
 58
 59            return props;
 60        }
 61
 62        /// <summary>
 63        ///     Converts a numeric representation of a functional level to its appropriate functional level string
 64        /// </summary>
 65        /// <param name="level"></param>
 66        /// <returns></returns>
 67        public static string FunctionalLevelToString(int level)
 68        {
 69            var functionalLevel = level switch
 70            {
 71                0 => "2000 Mixed/Native",
 72                1 => "2003 Interim",
 73                2 => "2003",
 74                3 => "2008",
 75                4 => "2008 R2",
 76                5 => "2012",
 77                6 => "2012 R2",
 78                7 => "2016",
 79                _ => "Unknown"
 80            };
 81
 82            return functionalLevel;
 83        }
 84
 85        /// <summary>
 86        ///     Reads specific LDAP properties related to GPOs
 87        /// </summary>
 88        /// <param name="entry"></param>
 89        /// <returns></returns>
 90        public static Dictionary<string, object> ReadGPOProperties(ISearchResultEntry entry)
 91        {
 92            var props = GetCommonProps(entry);
 93            props.Add("gpcpath", entry.GetProperty(LDAPProperties.GPCFileSYSPath)?.ToUpper());
 94            return props;
 95        }
 96
 97        /// <summary>
 98        ///     Reads specific LDAP properties related to OUs
 99        /// </summary>
 100        /// <param name="entry"></param>
 101        /// <returns></returns>
 102        public static Dictionary<string, object> ReadOUProperties(ISearchResultEntry entry)
 103        {
 104            var props = GetCommonProps(entry);
 105            return props;
 106        }
 107
 108        /// <summary>
 109        ///     Reads specific LDAP properties related to Groups
 110        /// </summary>
 111        /// <param name="entry"></param>
 112        /// <returns></returns>
 113        public static Dictionary<string, object> ReadGroupProperties(ISearchResultEntry entry)
 114        {
 115            var props = GetCommonProps(entry);
 116
 117            var ac = entry.GetProperty(LDAPProperties.AdminCount);
 118            if (ac != null)
 119            {
 120                var a = int.Parse(ac);
 121                props.Add("admincount", a != 0);
 122            }
 123            else
 124            {
 125                props.Add("admincount", false);
 126            }
 127
 128            return props;
 129        }
 130
 131        /// <summary>
 132        ///     Reads specific LDAP properties related to containers
 133        /// </summary>
 134        /// <param name="entry"></param>
 135        /// <returns></returns>
 136        public static Dictionary<string, object> ReadContainerProperties(ISearchResultEntry entry)
 137        {
 138            var props = GetCommonProps(entry);
 139            return props;
 140        }
 141
 142        /// <summary>
 143        ///     Reads specific LDAP properties related to Users
 144        /// </summary>
 145        /// <param name="entry"></param>
 146        /// <returns></returns>
 147        public async Task<UserProperties> ReadUserProperties(ISearchResultEntry entry)
 148        {
 149            var userProps = new UserProperties();
 150            var props = GetCommonProps(entry);
 151
 152            var uacFlags = (UacFlags)0;
 153            var uac = entry.GetProperty(LDAPProperties.UserAccountControl);
 154            if (int.TryParse(uac, out var flag))
 155            {
 156                uacFlags = (UacFlags)flag;
 157            }
 158
 159            props.Add("sensitive", uacFlags.HasFlag(UacFlags.NotDelegated));
 160            props.Add("dontreqpreauth", uacFlags.HasFlag(UacFlags.DontReqPreauth));
 161            props.Add("passwordnotreqd", uacFlags.HasFlag(UacFlags.PasswordNotRequired));
 162            props.Add("unconstraineddelegation", uacFlags.HasFlag(UacFlags.TrustedForDelegation));
 163            props.Add("pwdneverexpires", uacFlags.HasFlag(UacFlags.DontExpirePassword));
 164            props.Add("enabled", !uacFlags.HasFlag(UacFlags.AccountDisable));
 165            props.Add("trustedtoauth", uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation));
 166
 167            var domain = Helpers.DistinguishedNameToDomain(entry.DistinguishedName);
 168
 169            var comps = new List<TypedPrincipal>();
 170            if (uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation))
 171            {
 172                var delegates = entry.GetArrayProperty(LDAPProperties.AllowedToDelegateTo);
 173                props.Add("allowedtodelegate", delegates);
 174
 175                foreach (var d in delegates)
 176                {
 177                    if (d == null)
 178                        continue;
 179
 180                    var resolvedHost = await _utils.ResolveHostToSid(d, domain);
 181                    if (resolvedHost != null && resolvedHost.Contains("S-1"))
 182                        comps.Add(new TypedPrincipal
 183                        {
 184                            ObjectIdentifier = resolvedHost,
 185                            ObjectType = Label.Computer
 186                        });
 187                }
 188            }
 189
 190            userProps.AllowedToDelegate = comps.Distinct().ToArray();
 191
 192            props.Add("lastlogon", Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogon)));
 193            props.Add("lastlogontimestamp",
 194                Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogonTimestamp)));
 195            props.Add("pwdlastset",
 196                Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.PasswordLastSet)));
 197            var spn = entry.GetArrayProperty(LDAPProperties.ServicePrincipalNames);
 198            props.Add("serviceprincipalnames", spn);
 199            props.Add("hasspn", spn.Length > 0);
 200            props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName));
 201            props.Add("email", entry.GetProperty(LDAPProperties.Email));
 202            props.Add("title", entry.GetProperty(LDAPProperties.Title));
 203            props.Add("homedirectory", entry.GetProperty(LDAPProperties.HomeDirectory));
 204            props.Add("userpassword", entry.GetProperty(LDAPProperties.UserPassword));
 205            props.Add("unixpassword", entry.GetProperty(LDAPProperties.UnixUserPassword));
 206            props.Add("unicodepassword", entry.GetProperty(LDAPProperties.UnicodePassword));
 207            props.Add("sfupassword", entry.GetProperty(LDAPProperties.MsSFU30Password));
 208            props.Add("logonscript", entry.GetProperty(LDAPProperties.ScriptPath));
 209
 210            var ac = entry.GetProperty(LDAPProperties.AdminCount);
 211            if (ac != null)
 212            {
 213                if (int.TryParse(ac, out var parsed))
 214                    props.Add("admincount", parsed != 0);
 215                else
 216                    props.Add("admincount", false);
 217            }
 218            else
 219            {
 220                props.Add("admincount", false);
 221            }
 222
 223            var sh = entry.GetByteArrayProperty(LDAPProperties.SIDHistory);
 224            var sidHistoryList = new List<string>();
 225            var sidHistoryPrincipals = new List<TypedPrincipal>();
 226            foreach (var sid in sh)
 227            {
 228                string sSid;
 229                try
 230                {
 231                    sSid = new SecurityIdentifier(sid, 0).Value;
 232                }
 233                catch
 234                {
 235                    continue;
 236                }
 237
 238                sidHistoryList.Add(sSid);
 239
 240                var res = _utils.ResolveIDAndType(sSid, domain);
 241
 242                sidHistoryPrincipals.Add(res);
 243            }
 244
 245            userProps.SidHistory = sidHistoryPrincipals.Distinct().ToArray();
 246
 247            props.Add("sidhistory", sidHistoryList.ToArray());
 248
 249            userProps.Props = props;
 250
 251            return userProps;
 252        }
 253
 254        /// <summary>
 255        ///     Reads specific LDAP properties related to Computers
 256        /// </summary>
 257        /// <param name="entry"></param>
 258        /// <returns></returns>
 259        public async Task<ComputerProperties> ReadComputerProperties(ISearchResultEntry entry)
 260        {
 261            var compProps = new ComputerProperties();
 262            var props = GetCommonProps(entry);
 263
 264            var flags = (UacFlags)0;
 265            var uac = entry.GetProperty(LDAPProperties.UserAccountControl);
 266            if (int.TryParse(uac, out var flag))
 267            {
 268                flags = (UacFlags)flag;
 269            }
 270
 271            props.Add("enabled", !flags.HasFlag(UacFlags.AccountDisable));
 272            props.Add("unconstraineddelegation", flags.HasFlag(UacFlags.TrustedForDelegation));
 273            props.Add("trustedtoauth", flags.HasFlag(UacFlags.TrustedToAuthForDelegation));
 274            props.Add("isdc", flags.HasFlag(UacFlags.ServerTrustAccount));
 275
 276            var domain = Helpers.DistinguishedNameToDomain(entry.DistinguishedName);
 277
 278            var comps = new List<TypedPrincipal>();
 279            if (flags.HasFlag(UacFlags.TrustedToAuthForDelegation))
 280            {
 281                var delegates = entry.GetArrayProperty(LDAPProperties.AllowedToDelegateTo);
 282                props.Add("allowedtodelegate", delegates);
 283
 284                foreach (var d in delegates)
 285                {
 286                    var hname = d.Contains("/") ? d.Split('/')[1] : d;
 287                    hname = hname.Split(':')[0];
 288                    var resolvedHost = await _utils.ResolveHostToSid(hname, domain);
 289                    if (resolvedHost != null && resolvedHost.Contains("S-1"))
 290                        comps.Add(new TypedPrincipal
 291                        {
 292                            ObjectIdentifier = resolvedHost,
 293                            ObjectType = Label.Computer
 294                        });
 295                }
 296            }
 297
 298            compProps.AllowedToDelegate = comps.Distinct().ToArray();
 299
 300            var allowedToActPrincipals = new List<TypedPrincipal>();
 301            var rawAllowedToAct = entry.GetByteProperty(LDAPProperties.AllowedToActOnBehalfOfOtherIdentity);
 302            if (rawAllowedToAct != null)
 303            {
 304                var sd = _utils.MakeSecurityDescriptor();
 305                sd.SetSecurityDescriptorBinaryForm(rawAllowedToAct, AccessControlSections.Access);
 306                foreach (var rule in sd.GetAccessRules(true, true, typeof(SecurityIdentifier)))
 307                {
 308                    var res = _utils.ResolveIDAndType(rule.IdentityReference(), domain);
 309                    allowedToActPrincipals.Add(res);
 310                }
 311            }
 312
 313            compProps.AllowedToAct = allowedToActPrincipals.ToArray();
 314
 315            props.Add("lastlogon", Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogon)));
 316            props.Add("lastlogontimestamp",
 317                Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogonTimestamp)));
 318            props.Add("pwdlastset",
 319                Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.PasswordLastSet)));
 320            props.Add("serviceprincipalnames", entry.GetArrayProperty(LDAPProperties.ServicePrincipalNames));
 321            props.Add("email", entry.GetProperty(LDAPProperties.Email));
 322            var os = entry.GetProperty(LDAPProperties.OperatingSystem);
 323            var sp = entry.GetProperty(LDAPProperties.ServicePack);
 324
 325            if (sp != null) os = $"{os} {sp}";
 326
 327            props.Add("operatingsystem", os);
 328
 329            var sh = entry.GetByteArrayProperty(LDAPProperties.SIDHistory);
 330            var sidHistoryList = new List<string>();
 331            var sidHistoryPrincipals = new List<TypedPrincipal>();
 332            foreach (var sid in sh)
 333            {
 334                string sSid;
 335                try
 336                {
 337                    sSid = new SecurityIdentifier(sid, 0).Value;
 338                }
 339                catch
 340                {
 341                    continue;
 342                }
 343
 344                sidHistoryList.Add(sSid);
 345
 346                var res = _utils.ResolveIDAndType(sSid, domain);
 347
 348                sidHistoryPrincipals.Add(res);
 349            }
 350
 351            compProps.SidHistory = sidHistoryPrincipals.ToArray();
 352
 353            props.Add("sidhistory", sidHistoryList.ToArray());
 354
 355            var hsa = entry.GetArrayProperty(LDAPProperties.HostServiceAccount);
 356            var smsaPrincipals = new List<TypedPrincipal>();
 357            if (hsa != null)
 358            {
 359                foreach (var dn in hsa)
 360                {
 361                    var resolvedPrincipal = _utils.ResolveDistinguishedName(dn);
 362
 363                    if (resolvedPrincipal != null)
 364                        smsaPrincipals.Add(resolvedPrincipal);
 365                }
 366            }
 367
 368            compProps.DumpSMSAPassword = smsaPrincipals.ToArray();
 369
 370            compProps.Props = props;
 371
 372            return compProps;
 373        }
 374
 375        /// <summary>
 376        /// Returns the properties associated with the RootCA
 377        /// </summary>
 378        /// <param name="entry"></param>
 379        /// <returns>Returns a dictionary with the common properties of the RootCA</returns>
 380        public static Dictionary<string, object> ReadRootCAProperties(ISearchResultEntry entry)
 381        {
 382            var props = GetCommonProps(entry);
 383
 384            // Certificate
 385            var rawCertificate = entry.GetByteProperty(LDAPProperties.CACertificate);
 386            if (rawCertificate != null)
 387            {
 388                var cert = new ParsedCertificate(rawCertificate);
 389                props.Add("certthumbprint", cert.Thumbprint);
 390                props.Add("certname", cert.Name);
 391                props.Add("certchain", cert.Chain);
 392                props.Add("hasbasicconstraints", cert.HasBasicConstraints);
 393                props.Add("basicconstraintpathlength", cert.BasicConstraintPathLength);
 394            }
 395
 396            return props;
 397        }
 398
 399        /// <summary>
 400        /// Returns the properties associated with the AIACA
 401        /// </summary>
 402        /// <param name="entry"></param>
 403        /// <returns>Returns a dictionary with the common properties and the crosscertificatepair property of the AICA</
 404        public static Dictionary<string, object> ReadAIACAProperties(ISearchResultEntry entry)
 405        {
 406            var props = GetCommonProps(entry);
 407            var crossCertificatePair = entry.GetByteArrayProperty((LDAPProperties.CrossCertificatePair));
 408            var hasCrossCertificatePair = crossCertificatePair.Length > 0;
 409
 410            props.Add("crosscertificatepair", crossCertificatePair);
 411            props.Add("hascrosscertificatepair", hasCrossCertificatePair);
 412
 413            // Certificate
 414            var rawCertificate = entry.GetByteProperty(LDAPProperties.CACertificate);
 415            if (rawCertificate != null)
 416            {
 417                var cert = new ParsedCertificate(rawCertificate);
 418                props.Add("certthumbprint", cert.Thumbprint);
 419                props.Add("certname", cert.Name);
 420                props.Add("certchain", cert.Chain);
 421                props.Add("hasbasicconstraints", cert.HasBasicConstraints);
 422                props.Add("basicconstraintpathlength", cert.BasicConstraintPathLength);
 423            }
 424
 425            return props;
 426        }
 427
 428        public static Dictionary<string, object> ReadEnterpriseCAProperties(ISearchResultEntry entry)
 429        {
 430            var props = GetCommonProps(entry);
 431            if (entry.GetIntProperty("flags", out var flags)) props.Add("flags", (PKICertificateAuthorityFlags)flags);
 432            props.Add("caname", entry.GetProperty(LDAPProperties.Name));
 433            props.Add("dnshostname", entry.GetProperty(LDAPProperties.DNSHostName));
 434
 435            // Certificate
 436            var rawCertificate = entry.GetByteProperty(LDAPProperties.CACertificate);
 437            if (rawCertificate != null)
 438            {
 439                var cert = new ParsedCertificate(rawCertificate);
 440                props.Add("certthumbprint", cert.Thumbprint);
 441                props.Add("certname", cert.Name);
 442                props.Add("certchain", cert.Chain);
 443                props.Add("hasbasicconstraints", cert.HasBasicConstraints);
 444                props.Add("basicconstraintpathlength", cert.BasicConstraintPathLength);
 445            }
 446
 447            return props;
 448        }
 449
 450        /// <summary>
 451        /// Returns the properties associated with the NTAuthStore. These properties will only contain common properties
 452        /// </summary>
 453        /// <param name="entry"></param>
 454        /// <returns>Returns a dictionary with the common properties of the NTAuthStore</returns>
 455        public static Dictionary<string, object> ReadNTAuthStoreProperties(ISearchResultEntry entry)
 456        {
 457            var props = GetCommonProps(entry);
 458            return props;
 459        }
 460
 461        /// <summary>
 462        /// Reads specific LDAP properties related to CertTemplates
 463        /// </summary>
 464        /// <param name="entry"></param>
 465        /// <returns>Returns a dictionary associated with the CertTemplate properties that were read</returns>
 466        public static Dictionary<string, object> ReadCertTemplateProperties(ISearchResultEntry entry)
 467        {
 468            var props = GetCommonProps(entry);
 469
 470            props.Add("validityperiod", ConvertPKIPeriod(entry.GetByteProperty(LDAPProperties.PKIExpirationPeriod)));
 471            props.Add("renewalperiod", ConvertPKIPeriod(entry.GetByteProperty(LDAPProperties.PKIOverlappedPeriod)));
 472
 473            if (entry.GetIntProperty(LDAPProperties.TemplateSchemaVersion, out var schemaVersion))
 474                props.Add("schemaversion", schemaVersion);
 475
 476            props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName));
 477            props.Add("oid", entry.GetProperty(LDAPProperties.CertTemplateOID));
 478
 479            if (entry.GetIntProperty(LDAPProperties.PKIEnrollmentFlag, out var enrollmentFlagsRaw))
 480            {
 481                var enrollmentFlags = (PKIEnrollmentFlag)enrollmentFlagsRaw;
 482
 483                props.Add("enrollmentflag", enrollmentFlags);
 484                props.Add("requiresmanagerapproval", enrollmentFlags.HasFlag(PKIEnrollmentFlag.PEND_ALL_REQUESTS));
 485                props.Add("nosecurityextension", enrollmentFlags.HasFlag(PKIEnrollmentFlag.NO_SECURITY_EXTENSION));
 486            }
 487
 488            if (entry.GetIntProperty(LDAPProperties.PKINameFlag, out var nameFlagsRaw))
 489            {
 490                var nameFlags = (PKICertificateNameFlag)nameFlagsRaw;
 491
 492                props.Add("certificatenameflag", nameFlags);
 493                props.Add("enrolleesuppliessubject",
 494                    nameFlags.HasFlag(PKICertificateNameFlag.ENROLLEE_SUPPLIES_SUBJECT));
 495                props.Add("subjectaltrequireupn",
 496                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_UPN));
 497                props.Add("subjectaltrequiredns",
 498                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_DNS));
 499                props.Add("subjectaltrequiredomaindns",
 500                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_DOMAIN_DNS));
 501                props.Add("subjectaltrequireemail",
 502                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_EMAIL));
 503                props.Add("subjectaltrequirespn",
 504                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_SPN));
 505                props.Add("subjectrequireemail",
 506                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_REQUIRE_EMAIL));
 507            }
 508
 509            var ekus = entry.GetArrayProperty(LDAPProperties.ExtendedKeyUsage);
 510            props.Add("ekus", ekus);
 511            var certificateApplicationPolicy = entry.GetArrayProperty(LDAPProperties.CertificateApplicationPolicy);
 512            props.Add("certificateapplicationpolicy", certificateApplicationPolicy);
 513
 514            var certificatePolicy = entry.GetArrayProperty(LDAPProperties.CertificatePolicy);
 515            props.Add("certificatepolicy", certificatePolicy);
 516
 517            if (entry.GetIntProperty(LDAPProperties.NumSignaturesRequired, out var authorizedSignatures))
 518                props.Add("authorizedsignatures", authorizedSignatures);
 519
 520            var hasUseLegacyProvider = false;
 521            if (entry.GetIntProperty(LDAPProperties.PKIPrivateKeyFlag, out var privateKeyFlagsRaw))
 522            {
 523                var privateKeyFlags = (PKIPrivateKeyFlag)privateKeyFlagsRaw;
 524                hasUseLegacyProvider = privateKeyFlags.HasFlag(PKIPrivateKeyFlag.USE_LEGACY_PROVIDER);
 525            }
 526
 527            props.Add("applicationpolicies", ParseCertTemplateApplicationPolicies(entry.GetArrayProperty(LDAPProperties.
 528            props.Add("issuancepolicies", entry.GetArrayProperty(LDAPProperties.IssuancePolicies));
 529
 530            // Construct effectiveekus
 531            var effectiveekus = schemaVersion == 1 & ekus.Length > 0 ? ekus : certificateApplicationPolicy;
 532            props.Add("effectiveekus", effectiveekus);
 533
 534            // Construct authenticationenabled
 535            var authenticationEnabled = effectiveekus.Intersect(Helpers.AuthenticationOIDs).Any() | effectiveekus.Length
 536            props.Add("authenticationenabled", authenticationEnabled);
 537
 538            return props;
 539        }
 540
 541        public IssuancePolicyProperties ReadIssuancePolicyProperties(ISearchResultEntry entry)
 542        {
 543            var ret = new IssuancePolicyProperties();
 544            var props = GetCommonProps(entry);
 545            props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName));
 546            props.Add("certtemplateoid", entry.GetProperty(LDAPProperties.CertTemplateOID));
 547
 548            var link = entry.GetProperty(LDAPProperties.OIDGroupLink);
 549            if (!string.IsNullOrEmpty(link))
 550            {
 551                var linkedGroup = _utils.ResolveDistinguishedName(link);
 552                if (linkedGroup != null)
 553                {
 554                    props.Add("oidgrouplink", linkedGroup.ObjectIdentifier);
 555                    ret.GroupLink = linkedGroup;
 556                }
 557            }
 558
 559            ret.Props = props;
 560            return ret;
 561        }
 562
 563        /// <summary>
 564        ///     Attempts to parse all LDAP attributes outside of the ones already collected and converts them to a human
 565        ///     format using a best guess
 566        /// </summary>
 567        /// <param name="entry"></param>
 568        public Dictionary<string, object> ParseAllProperties(ISearchResultEntry entry)
 569        {
 570            var props = new Dictionary<string, object>();
 571
 572            var type = typeof(LDAPProperties);
 573            var reserved = type.GetFields(BindingFlags.Static | BindingFlags.Public).Select(x => x.GetValue(null).ToStri
 574
 575            foreach (var property in entry.PropertyNames())
 576            {
 577                if (ReservedAttributes.Contains(property, StringComparer.OrdinalIgnoreCase))
 578                    continue;
 579
 580                var collCount = entry.PropCount(property);
 581                if (collCount == 0)
 582                    continue;
 583
 584                if (collCount == 1)
 585                {
 586                    var testBytes = entry.GetByteProperty(property);
 587
 588                    if (testBytes == null || testBytes.Length == 0) continue;
 589
 590                    var testString = entry.GetProperty(property);
 591
 592                    if (!string.IsNullOrEmpty(testString))
 593                        if (property == "badpasswordtime")
 594                            props.Add(property, Helpers.ConvertFileTimeToUnixEpoch(testString));
 595                        else
 596                            props.Add(property, BestGuessConvert(testString));
 597                }
 598                else
 599                {
 600                    var arrBytes = entry.GetByteArrayProperty(property);
 601                    if (arrBytes.Length == 0)
 602                        continue;
 603
 604                    var arr = entry.GetArrayProperty(property);
 605                    if (arr.Length > 0) props.Add(property, arr.Select(BestGuessConvert).ToArray());
 606                }
 607            }
 608
 609            return props;
 610        }
 611
 612        /// <summary>
 613        ///     Parse CertTemplate attribute msPKI-RA-Application-Policies
 614        /// </summary>
 615        /// <param name="applicationPolicies"></param>
 616        /// <param name="schemaVersion"></param>
 617        /// <param name="hasUseLegacyProvider"></param>
 618        private static string[] ParseCertTemplateApplicationPolicies(string[] applicationPolicies, int schemaVersion, bo
 619        {
 620            if (applicationPolicies == null
 621                || applicationPolicies.Length == 0
 622                || schemaVersion == 1
 623                || schemaVersion == 2
 624                || (schemaVersion == 4 && hasUseLegacyProvider))
 625            {
 626                return applicationPolicies;
 627            }
 628            else
 629            {
 630                // Format: "Name`Type`Value`Name`Type`Value`..."
 631                // (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/c55ec697-be3f-4117-8316-8895e4
 632                // Return the Value of Name = "msPKI-RA-Application-Policies" entries
 633                var entries = applicationPolicies[0].Split('`');
 634                return Enumerable.Range(0, entries.Length / 3)
 635                    .Select(i => entries.Skip(i * 3).Take(3).ToArray())
 636                    .Where(parts => parts.Length == 3 && parts[0].Equals(LDAPProperties.ApplicationPolicies, StringCompa
 637                    .Select(parts => parts[2])
 638                    .ToArray();
 639            }
 640        }
 641
 642        /// <summary>
 643        ///     Does a best guess conversion of the property to a type useable by the UI
 644        /// </summary>
 645        /// <param name="property"></param>
 646        /// <returns></returns>
 647        private static object BestGuessConvert(string property)
 648        {
 649            //Parse boolean values
 650            if (bool.TryParse(property, out var boolResult)) return boolResult;
 651
 652            //A string ending with 0Z is likely a timestamp
 653            if (property.EndsWith("0Z")) return Helpers.ConvertTimestampToUnixEpoch(property);
 654
 655            //This string corresponds to the max int, and is usually set in accountexpires
 656            if (property == "9223372036854775807") return -1;
 657
 658            //Try parsing as an int
 659            if (int.TryParse(property, out var num)) return num;
 660
 661            //Just return the property as a string
 662            return property;
 663        }
 664
 665        /// <summary>
 666        ///     Converts PKIExpirationPeriod/PKIOverlappedPeriod attributes to time approximate times
 667        /// </summary>
 668        /// <remarks>https://www.sysadmins.lv/blog-en/how-to-convert-pkiexirationperiod-and-pkioverlapperiod-active-dire
 669        /// <param name="bytes"></param>
 670        /// <returns>Returns a string representing the time period associated with the input byte array in a human reada
 671        private static string ConvertPKIPeriod(byte[] bytes)
 672        {
 673            if (bytes == null || bytes.Length == 0)
 674                return "Unknown";
 675
 676            try
 677            {
 678                Array.Reverse(bytes);
 679                var temp = BitConverter.ToString(bytes).Replace("-", "");
 680                var value = Convert.ToInt64(temp, 16) * -.0000001;
 681
 682                if (value % 31536000 == 0 && value / 31536000 >= 1)
 683                {
 684                    if (value / 31536000 == 1) return "1 year";
 685
 686                    return $"{value / 31536000} years";
 687                }
 688
 689                if (value % 2592000 == 0 && value / 2592000 >= 1)
 690                {
 691                    if (value / 2592000 == 1) return "1 month";
 692
 693                    return $"{value / 2592000} months";
 694                }
 695
 696                if (value % 604800 == 0 && value / 604800 >= 1)
 697                {
 698                    if (value / 604800 == 1) return "1 week";
 699
 700                    return $"{value / 604800} weeks";
 701                }
 702
 703                if (value % 86400 == 0 && value / 86400 >= 1)
 704                {
 705                    if (value / 86400 == 1) return "1 day";
 706
 707                    return $"{value / 86400} days";
 708                }
 709
 710                if (value % 3600 == 0 && value / 3600 >= 1)
 711                {
 712                    if (value / 3600 == 1) return "1 hour";
 713
 714                    return $"{value / 3600} hours";
 715                }
 716
 717                return "";
 718            }
 719            catch (Exception)
 720            {
 721                return "Unknown";
 722            }
 723        }
 724
 725        [DllImport("Advapi32", SetLastError = false)]
 726        private static extern bool IsTextUnicode(byte[] buf, int len, ref IsTextUnicodeFlags opt);
 727
 728        [Flags]
 729        [SuppressMessage("ReSharper", "UnusedMember.Local")]
 730        [SuppressMessage("ReSharper", "InconsistentNaming")]
 731        private enum IsTextUnicodeFlags
 732        {
 733            IS_TEXT_UNICODE_ASCII16 = 0x0001,
 734            IS_TEXT_UNICODE_REVERSE_ASCII16 = 0x0010,
 735
 736            IS_TEXT_UNICODE_STATISTICS = 0x0002,
 737            IS_TEXT_UNICODE_REVERSE_STATISTICS = 0x0020,
 738
 739            IS_TEXT_UNICODE_CONTROLS = 0x0004,
 740            IS_TEXT_UNICODE_REVERSE_CONTROLS = 0x0040,
 741
 742            IS_TEXT_UNICODE_SIGNATURE = 0x0008,
 743            IS_TEXT_UNICODE_REVERSE_SIGNATURE = 0x0080,
 744
 745            IS_TEXT_UNICODE_ILLEGAL_CHARS = 0x0100,
 746            IS_TEXT_UNICODE_ODD_LENGTH = 0x0200,
 747            IS_TEXT_UNICODE_DBCS_LEADBYTE = 0x0400,
 748            IS_TEXT_UNICODE_NULL_BYTES = 0x1000,
 749
 750            IS_TEXT_UNICODE_UNICODE_MASK = 0x000F,
 751            IS_TEXT_UNICODE_REVERSE_MASK = 0x00F0,
 752            IS_TEXT_UNICODE_NOT_UNICODE_MASK = 0x0F00,
 753            IS_TEXT_UNICODE_NOT_ASCII_MASK = 0xF000
 754        }
 755    }
 756
 757    public class ParsedCertificate
 758    {
 759        public string Thumbprint { get; set; }
 760        public string Name { get; set; }
 761        public string[] Chain { get; set; } = Array.Empty<string>();
 762        public bool HasBasicConstraints { get; set; } = false;
 763        public int BasicConstraintPathLength { get; set; }
 764
 765        public ParsedCertificate(byte[] rawCertificate)
 766        {
 767            var parsedCertificate = new X509Certificate2(rawCertificate);
 768            Thumbprint = parsedCertificate.Thumbprint;
 769            var name = parsedCertificate.FriendlyName;
 770            Name = string.IsNullOrEmpty(name) ? Thumbprint : name;
 771
 772            // Chain
 773            X509Chain chain = new X509Chain();
 774            chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
 775            chain.Build(parsedCertificate);
 776            var temp = new List<string>();
 777            foreach (X509ChainElement cert in chain.ChainElements) temp.Add(cert.Certificate.Thumbprint);
 778            Chain = temp.ToArray();
 779
 780            // Extensions
 781            X509ExtensionCollection extensions = parsedCertificate.Extensions;
 782            List<CertificateExtension> certificateExtensions = new List<CertificateExtension>();
 783            foreach (X509Extension extension in extensions)
 784            {
 785                CertificateExtension certificateExtension = new CertificateExtension(extension);
 786                switch (certificateExtension.Oid.Value)
 787                {
 788                    case CAExtensionTypes.BasicConstraints:
 789                        X509BasicConstraintsExtension ext = (X509BasicConstraintsExtension)extension;
 790                        HasBasicConstraints = ext.HasPathLengthConstraint;
 791                        BasicConstraintPathLength = ext.PathLengthConstraint;
 792                        break;
 793                }
 794            }
 795        }
 796    }
 797
 798    public class UserProperties
 799    {
 12800        public Dictionary<string, object> Props { get; set; } = new();
 9801        public TypedPrincipal[] AllowedToDelegate { get; set; } = Array.Empty<TypedPrincipal>();
 10802        public TypedPrincipal[] SidHistory { get; set; } = Array.Empty<TypedPrincipal>();
 803    }
 804
 805    public class ComputerProperties
 806    {
 807        public Dictionary<string, object> Props { get; set; } = new();
 808        public TypedPrincipal[] AllowedToDelegate { get; set; } = Array.Empty<TypedPrincipal>();
 809        public TypedPrincipal[] AllowedToAct { get; set; } = Array.Empty<TypedPrincipal>();
 810        public TypedPrincipal[] SidHistory { get; set; } = Array.Empty<TypedPrincipal>();
 811        public TypedPrincipal[] DumpSMSAPassword { get; set; } = Array.Empty<TypedPrincipal>();
 812    }
 813
 814    public class IssuancePolicyProperties
 815    {
 816        public Dictionary<string, object> Props { get; set; } = new();
 817        public TypedPrincipal GroupLink { get; set; } = new TypedPrincipal();
 818    }
 819}