< Summary

Class:SharpHoundCommonLib.Processors.LdapPropertyProcessor
Assembly:SharpHoundCommonLib
File(s):D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Processors\LdapPropertyProcessor.cs
Covered lines:390
Uncovered lines:160
Coverable lines:550
Total lines:931
Line coverage:70.9% (390 of 550)
Covered branches:126
Total branches:254
Branch coverage:49.6% (126 of 254)

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.cctor()100%10100%
.ctor(...)100%10100%
GetCommonProps(...)100%40100%
ReadDomainProperties()35%20051.21%
FunctionalLevelToString(...)90%10093.33%
ReadGPOProperties(...)100%10100%
ReadOUProperties(...)100%10100%
ReadGroupProperties(...)100%10100%
ReadContainerProperties(...)100%100%
ReadUserProperties(...)100%100%
ReadUserProperties()76.66%30091.86%
ReadComputerProperties(...)100%100%
ReadComputerProperties()67.39%46090.36%
ReadRootCAProperties(...)50%2041.66%
ReadAIACAProperties(...)100%20100%
ReadEnterpriseCAProperties(...)0%400%
ReadNTAuthStoreProperties(...)100%10100%
ReadCertTemplateProperties(...)91.66%120100%
ReadIssuancePolicyProperties()75%80100%
ParseAllProperties(...)62.5%24076.92%
ParseCertTemplateApplicationPolicies(...)50%12050%
BestGuessConvert(...)66.66%120100%
ConvertEncryptionTypes(...)7.14%14013.33%
ConvertNanoDuration(...)0%2000%
ConvertPKIPeriod(...)5.88%34015.38%

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 {
 119        private static readonly HashSet<string> ReservedAttributes = new();
 20
 121        static LdapPropertyProcessor() {
 122            ReservedAttributes.UnionWith(CommonProperties.TypeResolutionProps);
 123            ReservedAttributes.UnionWith(CommonProperties.BaseQueryProps);
 124            ReservedAttributes.UnionWith(CommonProperties.GroupResolutionProps);
 125            ReservedAttributes.UnionWith(CommonProperties.ComputerMethodProps);
 126            ReservedAttributes.UnionWith(CommonProperties.ACLProps);
 127            ReservedAttributes.UnionWith(CommonProperties.ObjectPropsProps);
 128            ReservedAttributes.UnionWith(CommonProperties.ContainerProps);
 129            ReservedAttributes.UnionWith(CommonProperties.SPNTargetProps);
 130            ReservedAttributes.UnionWith(CommonProperties.DomainTrustProps);
 131            ReservedAttributes.UnionWith(CommonProperties.GPOLocalGroupProps);
 132            ReservedAttributes.UnionWith(CommonProperties.CertAbuseProps);
 133            ReservedAttributes.Add(LDAPProperties.DSASignature);
 134        }
 35
 36        private readonly ILdapUtils _utils;
 37
 3838        public LdapPropertyProcessor(ILdapUtils utils) {
 1939            _utils = utils;
 1940        }
 41
 2042        private static Dictionary<string, object> GetCommonProps(IDirectoryObject entry) {
 2043            var ret = new Dictionary<string, object>();
 3344            if (entry.TryGetProperty(LDAPProperties.Description, out var description)) {
 1345                ret["description"] = description;
 1346            }
 47
 2648            if (entry.TryGetProperty(LDAPProperties.WhenCreated, out var wc)) {
 649                ret["whencreated"] = Helpers.ConvertTimestampToUnixEpoch(wc);
 650            }
 51
 2052            return ret;
 2053        }
 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)
 261        {
 262            var props = GetCommonProps(entry);
 63
 64
 265            props.Add("expirepasswordsonsmartcardonlyaccounts", entry.GetProperty(LDAPProperties.ExpirePasswordsOnSmartC
 266            props.Add("machineaccountquota", entry.GetProperty(LDAPProperties.MachineAccountQuota));
 267            props.Add("minpwdlength", entry.GetProperty(LDAPProperties.MinPwdLength));
 268            props.Add("pwdproperties", entry.GetProperty(LDAPProperties.PwdProperties));
 269            props.Add("pwdhistorylength", entry.GetProperty(LDAPProperties.PwdHistoryLength));
 270            props.Add("lockoutthreshold", entry.GetProperty(LDAPProperties.LockoutThreshold));
 71
 272            if (entry.TryGetLongProperty(LDAPProperties.MinPwdAge, out var minpwdage)) {
 073                var duration = ConvertNanoDuration(minpwdage);
 074                if (duration != null) {
 075                    props.Add("minpwdage", duration);
 076                }
 077            }
 278            if (entry.TryGetLongProperty(LDAPProperties.MaxPwdAge, out var maxpwdage)) {
 079                var duration = ConvertNanoDuration(maxpwdage);
 080                if (duration != null) {
 081                    props.Add("maxpwdage", duration);
 082                }
 083            }
 284            if (entry.TryGetLongProperty(LDAPProperties.LockoutDuration, out var lockoutduration)) {
 085                var duration = ConvertNanoDuration(lockoutduration);
 086                if (duration != null) {
 087                    props.Add("lockoutduration", duration);
 088                }
 089            }
 290            if (entry.TryGetLongProperty(LDAPProperties.LockOutObservationWindow, out var lockoutobservationwindow)) {
 091                var duration = ConvertNanoDuration(lockoutobservationwindow);
 092                if (duration != null) {
 093                    props.Add("lockoutobservationwindow", lockoutobservationwindow);
 094                }
 095            }
 396            if (!entry.TryGetLongProperty(LDAPProperties.DomainFunctionalLevel, out var functionalLevel)) {
 197                functionalLevel = -1;
 198            }
 299            props.Add("functionallevel", FunctionalLevelToString((int)functionalLevel));
 100
 2101            var dn = entry.GetProperty(LDAPProperties.DistinguishedName);
 2102            var dsh = await _utils.GetDSHueristics(domain, dn);
 2103            props.Add("dsheuristics", dsh.DSHeuristics);
 104
 2105            return props;
 2106        }
 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>
 11113        public static string FunctionalLevelToString(int level) {
 11114            var functionalLevel = level switch {
 1115                0 => "2000 Mixed/Native",
 1116                1 => "2003 Interim",
 1117                2 => "2003",
 1118                3 => "2008",
 1119                4 => "2008 R2",
 1120                5 => "2012",
 2121                6 => "2012 R2",
 1122                7 => "2016",
 0123                8 => "2025",
 2124                _ => "Unknown"
 11125            };
 126
 11127            return functionalLevel;
 11128        }
 129
 130        /// <summary>
 131        ///     Reads specific LDAP properties related to GPOs
 132        /// </summary>
 133        /// <param name="entry"></param>
 134        /// <returns></returns>
 1135        public static Dictionary<string, object> ReadGPOProperties(IDirectoryObject entry) {
 1136            var props = GetCommonProps(entry);
 1137            entry.TryGetProperty(LDAPProperties.GPCFileSYSPath, out var path);
 1138            props.Add("gpcpath", path.ToUpper());
 1139            return props;
 1140        }
 141
 142        /// <summary>
 143        ///     Reads specific LDAP properties related to OUs
 144        /// </summary>
 145        /// <param name="entry"></param>
 146        /// <returns></returns>
 1147        public static Dictionary<string, object> ReadOUProperties(IDirectoryObject entry) {
 1148            var props = GetCommonProps(entry);
 1149            return props;
 1150        }
 151
 152        /// <summary>
 153        ///     Reads specific LDAP properties related to Groups
 154        /// </summary>
 155        /// <param name="entry"></param>
 156        /// <returns></returns>
 3157        public static Dictionary<string, object> ReadGroupProperties(IDirectoryObject entry) {
 3158            var props = GetCommonProps(entry);
 3159            entry.TryGetLongProperty(LDAPProperties.AdminCount, out var ac);
 3160            props.Add("admincount", ac != 0);
 3161            return props;
 3162        }
 163
 164        /// <summary>
 165        ///     Reads specific LDAP properties related to containers
 166        /// </summary>
 167        /// <param name="entry"></param>
 168        /// <returns></returns>
 0169        public static Dictionary<string, object> ReadContainerProperties(IDirectoryObject entry) {
 0170            var props = GetCommonProps(entry);
 0171            return props;
 0172        }
 173
 174        public Task<UserProperties>
 0175            ReadUserProperties(IDirectoryObject entry, ResolvedSearchResult searchResult) {
 0176            return ReadUserProperties(entry, searchResult.Domain);
 0177        }
 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>
 4185        public async Task<UserProperties> ReadUserProperties(IDirectoryObject entry, string domain) {
 4186            var userProps = new UserProperties();
 4187            var props = GetCommonProps(entry);
 188
 4189            var uacFlags = (UacFlags)0;
 7190            if (entry.TryGetLongProperty(LDAPProperties.UserAccountControl, out var uac)) {
 3191                uacFlags = (UacFlags)uac;
 3192            }
 193
 4194            props.Add("sensitive", uacFlags.HasFlag(UacFlags.NotDelegated));
 4195            props.Add("dontreqpreauth", uacFlags.HasFlag(UacFlags.DontReqPreauth));
 4196            props.Add("passwordnotreqd", uacFlags.HasFlag(UacFlags.PasswordNotRequired));
 4197            props.Add("unconstraineddelegation", uacFlags.HasFlag(UacFlags.TrustedForDelegation));
 4198            props.Add("pwdneverexpires", uacFlags.HasFlag(UacFlags.DontExpirePassword));
 4199            props.Add("enabled", !uacFlags.HasFlag(UacFlags.AccountDisable));
 4200            props.Add("trustedtoauth", uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation));
 4201            props.Add("smartcardrequired", uacFlags.HasFlag(UacFlags.SmartcardRequired));
 4202            props.Add("encryptedtextpwdallowed", uacFlags.HasFlag(UacFlags.EncryptedTextPwdAllowed));
 4203            props.Add("usedeskeyonly", uacFlags.HasFlag(UacFlags.UseDesKeyOnly));
 4204            props.Add("logonscriptenabled", uacFlags.HasFlag(UacFlags.Script));
 4205            props.Add("lockedout", uacFlags.HasFlag(UacFlags.Lockout));
 4206            props.Add("passwordcantchange", uacFlags.HasFlag(UacFlags.PasswordCantChange));
 4207            props.Add("passwordexpired", uacFlags.HasFlag(UacFlags.PasswordExpired));
 208
 4209            userProps.UnconstrainedDelegation = uacFlags.HasFlag(UacFlags.TrustedForDelegation);
 210
 4211            var comps = new List<TypedPrincipal>();
 4212            if (uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation) &&
 5213                entry.TryGetArrayProperty(LDAPProperties.AllowedToDelegateTo, out var delegates)) {
 1214                props.Add("allowedtodelegate", delegates);
 215
 9216                foreach (var d in delegates) {
 2217                    if (d == null)
 0218                        continue;
 219
 2220                    var resolvedHost = await _utils.ResolveHostToSid(d, domain);
 2221                    if (resolvedHost.Success && resolvedHost.SecurityIdentifier.Contains("S-1"))
 2222                        comps.Add(new TypedPrincipal {
 2223                            ObjectIdentifier = resolvedHost.SecurityIdentifier,
 2224                            ObjectType = Label.Computer
 2225                        });
 2226                }
 1227            }
 228
 4229            userProps.AllowedToDelegate = comps.Distinct().ToArray();
 230
 4231            if (!entry.TryGetProperty(LDAPProperties.LastLogon, out var lastLogon)) {
 0232                lastLogon = null;
 0233            }
 234
 4235            props.Add("lastlogon", Helpers.ConvertFileTimeToUnixEpoch(lastLogon));
 236
 4237            if (!entry.TryGetProperty(LDAPProperties.LastLogonTimestamp, out var lastLogonTimeStamp)) {
 0238                lastLogonTimeStamp = null;
 0239            }
 240
 4241            props.Add("lastlogontimestamp", Helpers.ConvertFileTimeToUnixEpoch(lastLogonTimeStamp));
 242
 4243            if (!entry.TryGetProperty(LDAPProperties.PasswordLastSet, out var passwordLastSet)) {
 0244                passwordLastSet = null;
 0245            }
 246
 4247            props.Add("pwdlastset",
 4248                Helpers.ConvertFileTimeToUnixEpoch(passwordLastSet));
 4249            entry.TryGetArrayProperty(LDAPProperties.ServicePrincipalNames, out var spn);
 4250            props.Add("serviceprincipalnames", spn);
 4251            props.Add("hasspn", spn.Length > 0);
 4252            props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName));
 4253            props.Add("email", entry.GetProperty(LDAPProperties.Email));
 4254            props.Add("title", entry.GetProperty(LDAPProperties.Title));
 4255            props.Add("homedirectory", entry.GetProperty(LDAPProperties.HomeDirectory));
 4256            props.Add("userpassword", entry.GetProperty(LDAPProperties.UserPassword));
 4257            props.Add("unixpassword", entry.GetProperty(LDAPProperties.UnixUserPassword));
 4258            props.Add("unicodepassword", entry.GetProperty(LDAPProperties.UnicodePassword));
 4259            props.Add("sfupassword", entry.GetProperty(LDAPProperties.MsSFU30Password));
 4260            props.Add("logonscript", entry.GetProperty(LDAPProperties.ScriptPath));
 4261            props.Add("useraccountcontrol", uac);
 4262            props.Add("profilepath", entry.GetProperty(LDAPProperties.ProfilePath));
 263
 4264            entry.TryGetLongProperty(LDAPProperties.AdminCount, out var ac);
 4265            props.Add("admincount", ac != 0);
 266
 4267            var encryptionTypes = ConvertEncryptionTypes(entry.GetProperty(LDAPProperties.SupportedEncryptionTypes));
 4268            props.Add("supportedencryptiontypes", encryptionTypes);
 269
 4270            entry.TryGetByteArrayProperty(LDAPProperties.SIDHistory, out var sh);
 4271            var sidHistoryList = new List<string>();
 4272            var sidHistoryPrincipals = new List<TypedPrincipal>();
 24273            foreach (var sid in sh) {
 274                string sSid;
 4275                try {
 4276                    sSid = new SecurityIdentifier(sid, 0).Value;
 5277                } catch {
 1278                    continue;
 279                }
 280
 3281                sidHistoryList.Add(sSid);
 282
 3283                if (await _utils.ResolveIDAndType(sSid, domain) is (true, var res))
 3284                    sidHistoryPrincipals.Add(res);
 3285            }
 286
 4287            userProps.SidHistory = sidHistoryPrincipals.Distinct().ToArray();
 288
 4289            props.Add("sidhistory", sidHistoryList.ToArray());
 290
 4291            userProps.Props = props;
 292
 4293            return userProps;
 4294        }
 295
 296        public Task<ComputerProperties> ReadComputerProperties(IDirectoryObject entry,
 0297            ResolvedSearchResult searchResult) {
 0298            return ReadComputerProperties(entry, searchResult.Domain);
 0299        }
 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>
 3307        public async Task<ComputerProperties> ReadComputerProperties(IDirectoryObject entry, string domain) {
 3308            var compProps = new ComputerProperties();
 3309            var props = GetCommonProps(entry);
 310
 3311            var flags = (UacFlags)0;
 5312            if (entry.TryGetLongProperty(LDAPProperties.UserAccountControl, out var uac)) {
 2313                flags = (UacFlags)uac;
 2314            }
 315
 3316            props.Add("enabled", !flags.HasFlag(UacFlags.AccountDisable));
 3317            props.Add("unconstraineddelegation", flags.HasFlag(UacFlags.TrustedForDelegation));
 3318            props.Add("trustedtoauth", flags.HasFlag(UacFlags.TrustedToAuthForDelegation));
 3319            props.Add("isdc", flags.HasFlag(UacFlags.ServerTrustAccount));
 3320            props.Add("encryptedtextpwdallowed", flags.HasFlag(UacFlags.EncryptedTextPwdAllowed));
 3321            props.Add("usedeskeyonly", flags.HasFlag(UacFlags.UseDesKeyOnly));
 3322            props.Add("logonscriptenabled", flags.HasFlag(UacFlags.Script));
 3323            props.Add("lockedout", flags.HasFlag(UacFlags.Lockout));
 3324            props.Add("passwordexpired", flags.HasFlag(UacFlags.PasswordExpired));
 325
 3326            compProps.UnconstrainedDelegation = flags.HasFlag(UacFlags.TrustedForDelegation);
 327
 3328            var encryptionTypes = ConvertEncryptionTypes(entry.GetProperty(LDAPProperties.SupportedEncryptionTypes));
 3329            props.Add("supportedencryptiontypes", encryptionTypes);
 330
 3331            var comps = new List<TypedPrincipal>();
 3332            if (flags.HasFlag(UacFlags.TrustedToAuthForDelegation) &&
 5333                entry.TryGetArrayProperty(LDAPProperties.AllowedToDelegateTo, out var delegates)) {
 2334                props.Add("allowedtodelegate", delegates);
 335
 24336                foreach (var d in delegates) {
 6337                    if (d == null)
 0338                        continue;
 339
 6340                    var resolvedHost = await _utils.ResolveHostToSid(d, domain);
 6341                    if (resolvedHost.Success && resolvedHost.SecurityIdentifier.Contains("S-1"))
 6342                        comps.Add(new TypedPrincipal {
 6343                            ObjectIdentifier = resolvedHost.SecurityIdentifier,
 6344                            ObjectType = Label.Computer
 6345                        });
 6346                }
 2347            }
 348
 3349            compProps.AllowedToDelegate = comps.Distinct().ToArray();
 350
 3351            var allowedToActPrincipals = new List<TypedPrincipal>();
 3352            if (entry.TryGetByteProperty(LDAPProperties.AllowedToActOnBehalfOfOtherIdentity, out var rawAllowedToAct)) {
 0353                var sd = _utils.MakeSecurityDescriptor();
 0354                sd.SetSecurityDescriptorBinaryForm(rawAllowedToAct, AccessControlSections.Access);
 0355                foreach (var rule in sd.GetAccessRules(true, true, typeof(SecurityIdentifier))) {
 0356                    if (await _utils.ResolveIDAndType(rule.IdentityReference(), domain) is (true, var res))
 0357                        allowedToActPrincipals.Add(res);
 0358                }
 0359            }
 360
 3361            compProps.AllowedToAct = allowedToActPrincipals.ToArray();
 362
 3363            props.Add("lastlogon", Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogon)));
 3364            props.Add("lastlogontimestamp",
 3365                Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogonTimestamp)));
 3366            props.Add("pwdlastset",
 3367                Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.PasswordLastSet)));
 3368            entry.TryGetArrayProperty(LDAPProperties.ServicePrincipalNames, out var spn);
 3369            props.Add("serviceprincipalnames", spn);
 3370            props.Add("email", entry.GetProperty(LDAPProperties.Email));
 3371            props.Add("useraccountcontrol", uac);
 3372            var os = entry.GetProperty(LDAPProperties.OperatingSystem);
 3373            var sp = entry.GetProperty(LDAPProperties.ServicePack);
 374
 5375            if (sp != null) os = $"{os} {sp}";
 376
 3377            props.Add("operatingsystem", os);
 378
 3379            entry.TryGetByteArrayProperty(LDAPProperties.SIDHistory, out var sh);
 3380            var sidHistoryList = new List<string>();
 3381            var sidHistoryPrincipals = new List<TypedPrincipal>();
 18382            foreach (var sid in sh) {
 383                string sSid;
 3384                try {
 3385                    sSid = new SecurityIdentifier(sid, 0).Value;
 4386                } catch {
 1387                    continue;
 388                }
 389
 2390                sidHistoryList.Add(sSid);
 391
 2392                if (await _utils.ResolveIDAndType(sSid, domain) is (true, var res))
 2393                    sidHistoryPrincipals.Add(res);
 2394            }
 395
 3396            compProps.SidHistory = sidHistoryPrincipals.ToArray();
 397
 3398            props.Add("sidhistory", sidHistoryList.ToArray());
 399
 3400            var smsaPrincipals = new List<TypedPrincipal>();
 4401            if (entry.TryGetArrayProperty(LDAPProperties.HostServiceAccount, out var hsa)) {
 9402                foreach (var dn in hsa) {
 2403                    if (await _utils.ResolveDistinguishedName(dn) is (true, var resolvedPrincipal))
 2404                        smsaPrincipals.Add(resolvedPrincipal);
 2405                }
 1406            }
 407
 3408            compProps.DumpSMSAPassword = smsaPrincipals.ToArray();
 409
 3410            compProps.Props = props;
 411
 3412            return compProps;
 3413        }
 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>
 1420        public static Dictionary<string, object> ReadRootCAProperties(IDirectoryObject entry) {
 1421            var props = GetCommonProps(entry);
 422
 423            // Certificate
 1424            if (entry.TryGetByteProperty(LDAPProperties.CACertificate, out var rawCertificate)) {
 0425                var cert = new ParsedCertificate(rawCertificate);
 0426                props.Add("certthumbprint", cert.Thumbprint);
 0427                props.Add("certname", cert.Name);
 0428                props.Add("certchain", cert.Chain);
 0429                props.Add("hasbasicconstraints", cert.HasBasicConstraints);
 0430                props.Add("basicconstraintpathlength", cert.BasicConstraintPathLength);
 0431            }
 432
 1433            return props;
 1434        }
 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</
 1441        public static Dictionary<string, object> ReadAIACAProperties(IDirectoryObject entry) {
 1442            var props = GetCommonProps(entry);
 1443            entry.TryGetByteArrayProperty(LDAPProperties.CrossCertificatePair, out var crossCertificatePair);
 1444            var hasCrossCertificatePair = crossCertificatePair.Length > 0;
 445
 1446            props.Add("crosscertificatepair", crossCertificatePair);
 1447            props.Add("hascrosscertificatepair", hasCrossCertificatePair);
 448
 449            // Certificate
 2450            if (entry.TryGetByteProperty(LDAPProperties.CACertificate, out var rawCertificate)) {
 1451                var cert = new ParsedCertificate(rawCertificate);
 1452                props.Add("certthumbprint", cert.Thumbprint);
 1453                props.Add("certname", cert.Name);
 1454                props.Add("certchain", cert.Chain);
 1455                props.Add("hasbasicconstraints", cert.HasBasicConstraints);
 1456                props.Add("basicconstraintpathlength", cert.BasicConstraintPathLength);
 1457            }
 458
 1459            return props;
 1460        }
 461
 0462        public static Dictionary<string, object> ReadEnterpriseCAProperties(IDirectoryObject entry) {
 0463            var props = GetCommonProps(entry);
 0464            if (entry.TryGetLongProperty("flags", out var flags))
 0465                props.Add("flags", (PKICertificateAuthorityFlags)flags);
 0466            props.Add("caname", entry.GetProperty(LDAPProperties.Name));
 0467            props.Add("dnshostname", entry.GetProperty(LDAPProperties.DNSHostName));
 468
 469            // Certificate
 0470            if (entry.TryGetByteProperty(LDAPProperties.CACertificate, out var rawCertificate)) {
 0471                var cert = new ParsedCertificate(rawCertificate);
 0472                props.Add("certthumbprint", cert.Thumbprint);
 0473                props.Add("certname", cert.Name);
 0474                props.Add("certchain", cert.Chain);
 0475                props.Add("hasbasicconstraints", cert.HasBasicConstraints);
 0476                props.Add("basicconstraintpathlength", cert.BasicConstraintPathLength);
 0477            }
 478
 0479            return props;
 0480        }
 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>
 1487        public static Dictionary<string, object> ReadNTAuthStoreProperties(IDirectoryObject entry) {
 1488            var props = GetCommonProps(entry);
 1489            return props;
 1490        }
 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>
 1497        public static Dictionary<string, object> ReadCertTemplateProperties(IDirectoryObject entry) {
 1498            var props = GetCommonProps(entry);
 499
 1500            props.Add("validityperiod", ConvertPKIPeriod(entry.GetByteProperty(LDAPProperties.PKIExpirationPeriod)));
 1501            props.Add("renewalperiod", ConvertPKIPeriod(entry.GetByteProperty(LDAPProperties.PKIOverlappedPeriod)));
 502
 1503            if (entry.TryGetLongProperty(LDAPProperties.TemplateSchemaVersion, out var schemaVersion))
 1504                props.Add("schemaversion", schemaVersion);
 505
 1506            props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName));
 1507            props.Add("oid", entry.GetProperty(LDAPProperties.CertTemplateOID));
 508
 2509            if (entry.TryGetLongProperty(LDAPProperties.PKIEnrollmentFlag, out var enrollmentFlagsRaw)) {
 1510                var enrollmentFlags = (PKIEnrollmentFlag)enrollmentFlagsRaw;
 511
 1512                props.Add("enrollmentflag", enrollmentFlags);
 1513                props.Add("requiresmanagerapproval", enrollmentFlags.HasFlag(PKIEnrollmentFlag.PEND_ALL_REQUESTS));
 1514                props.Add("nosecurityextension", enrollmentFlags.HasFlag(PKIEnrollmentFlag.NO_SECURITY_EXTENSION));
 1515            }
 516
 2517            if (entry.TryGetLongProperty(LDAPProperties.PKINameFlag, out var nameFlagsRaw)) {
 1518                var nameFlags = (PKICertificateNameFlag)nameFlagsRaw;
 519
 1520                props.Add("certificatenameflag", nameFlags);
 1521                props.Add("enrolleesuppliessubject",
 1522                    nameFlags.HasFlag(PKICertificateNameFlag.ENROLLEE_SUPPLIES_SUBJECT));
 1523                props.Add("subjectaltrequireupn",
 1524                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_UPN));
 1525                props.Add("subjectaltrequiredns",
 1526                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_DNS));
 1527                props.Add("subjectaltrequiredomaindns",
 1528                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_DOMAIN_DNS));
 1529                props.Add("subjectaltrequireemail",
 1530                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_EMAIL));
 1531                props.Add("subjectaltrequirespn",
 1532                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_SPN));
 1533                props.Add("subjectrequireemail",
 1534                    nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_REQUIRE_EMAIL));
 1535            }
 536
 1537            entry.TryGetArrayProperty(LDAPProperties.ExtendedKeyUsage, out var ekus);
 1538            props.Add("ekus", ekus);
 1539            entry.TryGetArrayProperty(LDAPProperties.CertificateApplicationPolicy,
 1540                out var certificateApplicationPolicy);
 1541            props.Add("certificateapplicationpolicy", certificateApplicationPolicy);
 542
 1543            entry.TryGetArrayProperty(LDAPProperties.CertificatePolicy, out var certificatePolicy);
 1544            props.Add("certificatepolicy", certificatePolicy);
 545
 1546            if (entry.TryGetLongProperty(LDAPProperties.NumSignaturesRequired, out var authorizedSignatures))
 1547                props.Add("authorizedsignatures", authorizedSignatures);
 548
 1549            var hasUseLegacyProvider = false;
 2550            if (entry.TryGetLongProperty(LDAPProperties.PKIPrivateKeyFlag, out var privateKeyFlagsRaw)) {
 1551                var privateKeyFlags = (PKIPrivateKeyFlag)privateKeyFlagsRaw;
 1552                hasUseLegacyProvider = privateKeyFlags.HasFlag(PKIPrivateKeyFlag.USE_LEGACY_PROVIDER);
 1553            }
 554
 1555            entry.TryGetArrayProperty(LDAPProperties.ApplicationPolicies, out var appPolicies);
 556
 1557            props.Add("applicationpolicies",
 1558                ParseCertTemplateApplicationPolicies(appPolicies,
 1559                    (int)schemaVersion, hasUseLegacyProvider));
 1560            entry.TryGetArrayProperty(LDAPProperties.IssuancePolicies, out var issuancePolicies);
 1561            props.Add("issuancepolicies", issuancePolicies);
 562
 563            // Construct effectiveekus
 1564            var effectiveekus = schemaVersion == 1 & ekus.Length > 0 ? ekus : certificateApplicationPolicy;
 1565            props.Add("effectiveekus", effectiveekus);
 566
 567            // Construct authenticationenabled
 1568            var authenticationEnabled =
 1569                effectiveekus.Intersect(Helpers.AuthenticationOIDs).Any() | effectiveekus.Length == 0;
 1570            props.Add("authenticationenabled", authenticationEnabled);
 571
 572            // Construct schannelauthenticationenabled
 1573            var schannelAuthenticationEnabled =
 1574                effectiveekus.Intersect(Helpers.SchannelAuthenticationOIDs).Any() | effectiveekus.Length == 0;
 1575            props.Add("schannelauthenticationenabled", schannelAuthenticationEnabled);
 576
 1577            return props;
 1578        }
 579
 2580        public async Task<IssuancePolicyProperties> ReadIssuancePolicyProperties(IDirectoryObject entry) {
 2581            var ret = new IssuancePolicyProperties();
 2582            var props = GetCommonProps(entry);
 2583            props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName));
 2584            props.Add("certtemplateoid", entry.GetProperty(LDAPProperties.CertTemplateOID));
 585
 3586            if (entry.TryGetProperty(LDAPProperties.OIDGroupLink, out var link)) {
 2587                if (await _utils.ResolveDistinguishedName(link) is (true, var linkedGroup)) {
 1588                    props.Add("oidgrouplink", linkedGroup.ObjectIdentifier);
 1589                    ret.GroupLink = linkedGroup;
 1590                }
 1591            }
 592
 2593            ret.Props = props;
 2594            return ret;
 2595        }
 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>
 8602        public Dictionary<string, object> ParseAllProperties(IDirectoryObject entry) {
 8603            var props = new Dictionary<string, object>();
 604
 60605            foreach (var property in entry.PropertyNames()) {
 12606                if (ReservedAttributes.Contains(property, StringComparer.OrdinalIgnoreCase))
 4607                    continue;
 608
 8609                var collCount = entry.PropertyCount(property);
 8610                if (collCount == 0)
 0611                    continue;
 612
 14613                if (collCount == 1) {
 6614                    var testString = entry.GetProperty(property);
 11615                    if (!string.IsNullOrEmpty(testString)) {
 5616                        if (property.Equals("badpasswordtime", StringComparison.OrdinalIgnoreCase))
 1617                            props.Add(property, Helpers.ConvertFileTimeToUnixEpoch(testString));
 618                        else
 4619                            props.Add(property, BestGuessConvert(testString));
 5620                    }
 8621                } else {
 4622                    if (entry.TryGetByteProperty(property, out var testBytes)) {
 2623                        if (testBytes == null || testBytes.Length == 0) {
 0624                            continue;
 625                        }
 626
 627                        // SIDs
 2628                        try {
 2629                            var sid = new SecurityIdentifier(testBytes, 0);
 1630                            props.Add(property, sid.Value);
 1631                            continue;
 2632                        } catch {
 633                            /* Ignore */
 1634                        }
 635
 636                        // GUIDs
 1637                        try {
 1638                            var guid = new Guid(testBytes);
 1639                            props.Add(property, guid.ToString());
 1640                            continue;
 0641                        } catch {
 642                            /* Ignore */
 0643                        }
 0644                    }
 645
 0646                    if (entry.TryGetArrayProperty(property, out var arr) && arr.Length > 0) {
 0647                        props.Add(property, arr.Select(BestGuessConvert).ToArray());
 0648                    }
 0649                }
 6650            }
 651
 8652            return props;
 8653        }
 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,
 1662            bool hasUseLegacyProvider) {
 1663            if (applicationPolicies == null
 1664                || applicationPolicies.Length == 0
 1665                || schemaVersion == 1
 1666                || schemaVersion == 2
 2667                || (schemaVersion == 4 && hasUseLegacyProvider)) {
 1668                return applicationPolicies;
 0669            } 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
 0673                var entries = applicationPolicies[0].Split('`');
 0674                return Enumerable.Range(0, entries.Length / 3)
 0675                    .Select(i => entries.Skip(i * 3).Take(3).ToArray())
 0676                    .Where(parts => parts.Length == 3 && parts[0].Equals(LDAPProperties.ApplicationPolicies,
 0677                        StringComparison.OrdinalIgnoreCase))
 0678                    .Select(parts => parts[2])
 0679                    .ToArray();
 680            }
 1681        }
 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>
 4688        private static object BestGuessConvert(string value) {
 689            //Parse boolean values
 4690            if (bool.TryParse(value, out var boolResult)) return boolResult;
 691
 692            //A string ending with 0Z is likely a timestamp
 4693            if (value.EndsWith("0Z")) return Helpers.ConvertTimestampToUnixEpoch(value);
 694
 695            //This string corresponds to the max int, and is usually set in accountexpires
 4696            if (value == "9223372036854775807") return -1;
 697
 698            //Try parsing as an int
 4699            if (int.TryParse(value, out var num)) return num;
 700
 701            // If we have binary unicode, encode it
 293702            foreach (char c in value) {
 95703                if (char.IsControl(c)) return System.Text.Encoding.UTF8.GetBytes(value);
 93704            }
 705
 706            //Just return the property as a string
 3707            return value;
 4708        }
 709
 710        private static List<string> ConvertEncryptionTypes(string encryptionTypes)
 7711        {
 14712            if (encryptionTypes == null) {
 7713                return null;
 714            }
 715
 0716            int encryptionTypesInt = Int32.Parse(encryptionTypes);
 0717            List<string> supportedEncryptionTypes = new List<string>();
 0718            if (encryptionTypesInt == 0) {
 0719                supportedEncryptionTypes.Add("Not defined");
 0720            }
 721
 0722            if ((encryptionTypesInt & KerberosEncryptionTypes.DES_CBC_CRC) == KerberosEncryptionTypes.DES_CBC_CRC)
 0723            {
 0724                supportedEncryptionTypes.Add("DES-CBC-CRC");
 0725            }
 0726            if ((encryptionTypesInt & KerberosEncryptionTypes.DES_CBC_MD5) == KerberosEncryptionTypes.DES_CBC_MD5)
 0727            {
 0728                supportedEncryptionTypes.Add("DES-CBC-MD5");
 0729            }
 0730            if ((encryptionTypesInt & KerberosEncryptionTypes.RC4_HMAC_MD5) == KerberosEncryptionTypes.RC4_HMAC_MD5)
 0731            {
 0732                supportedEncryptionTypes.Add("RC4-HMAC-MD5");
 0733            }
 0734            if ((encryptionTypesInt & KerberosEncryptionTypes.AES128_CTS_HMAC_SHA1_96) == KerberosEncryptionTypes.AES128
 0735            {
 0736                supportedEncryptionTypes.Add("AES128-CTS-HMAC-SHA1-96");
 0737            }
 0738            if ((encryptionTypesInt & KerberosEncryptionTypes.AES256_CTS_HMAC_SHA1_96) == KerberosEncryptionTypes.AES256
 0739            {
 0740                supportedEncryptionTypes.Add("AES256-CTS-HMAC-SHA1-96");
 0741            }
 742
 0743            return supportedEncryptionTypes;
 7744        }
 745
 746        private static string ConvertNanoDuration(long duration)
 0747        {
 748            // In case duration is long.MinValue, Math.Abs will overflow.  Value represents Forever or Never
 0749            if (duration == long.MinValue) {
 0750                return "Forever";
 751            // And if the value is positive, it indicates an error code
 0752            } else if (duration > 0) {
 0753                return null;
 754            }
 755
 756            // duration is in 100-nanosecond intervals
 757            // Convert it to TimeSpan (which uses 1 tick = 100 nanoseconds)
 0758            TimeSpan durationSpan = TimeSpan.FromTicks(Math.Abs(duration));
 759
 760            // Create a list to hold non-zero time components
 0761            List<string> timeComponents = new List<string>();
 762
 763            // Add each time component if it's greater than zero
 0764            if (durationSpan.Days > 0)
 0765            {
 0766                timeComponents.Add($"{durationSpan.Days} {(durationSpan.Days == 1 ? "day" : "days")}");
 0767            }
 0768            if (durationSpan.Hours > 0)
 0769            {
 0770                timeComponents.Add($"{durationSpan.Hours} {(durationSpan.Hours == 1 ? "hour" : "hours")}");
 0771            }
 0772            if (durationSpan.Minutes > 0)
 0773            {
 0774                timeComponents.Add($"{durationSpan.Minutes} {(durationSpan.Minutes == 1 ? "minute" : "minutes")}");
 0775            }
 0776            if (durationSpan.Seconds > 0)
 0777            {
 0778                timeComponents.Add($"{durationSpan.Seconds} {(durationSpan.Seconds == 1 ? "second" : "seconds")}");
 0779            }
 780
 781            // Join the non-zero components into a single readable string
 0782            string readableDuration = string.Join(", ", timeComponents);
 783
 0784            return readableDuration;
 0785        }
 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
 2793        private static string ConvertPKIPeriod(byte[] bytes) {
 2794            if (bytes == null || bytes.Length == 0)
 2795                return "Unknown";
 796
 0797            try {
 0798                Array.Reverse(bytes);
 0799                var temp = BitConverter.ToString(bytes).Replace("-", "");
 0800                var value = Convert.ToInt64(temp, 16) * -.0000001;
 801
 0802                if (value % 31536000 == 0 && value / 31536000 >= 1) {
 0803                    if (value / 31536000 == 1) return "1 year";
 804
 0805                    return $"{value / 31536000} years";
 806                }
 807
 0808                if (value % 2592000 == 0 && value / 2592000 >= 1) {
 0809                    if (value / 2592000 == 1) return "1 month";
 810
 0811                    return $"{value / 2592000} months";
 812                }
 813
 0814                if (value % 604800 == 0 && value / 604800 >= 1) {
 0815                    if (value / 604800 == 1) return "1 week";
 816
 0817                    return $"{value / 604800} weeks";
 818                }
 819
 0820                if (value % 86400 == 0 && value / 86400 >= 1) {
 0821                    if (value / 86400 == 1) return "1 day";
 822
 0823                    return $"{value / 86400} days";
 824                }
 825
 0826                if (value % 3600 == 0 && value / 3600 >= 1) {
 0827                    if (value / 3600 == 1) return "1 hour";
 828
 0829                    return $"{value / 3600} hours";
 830                }
 831
 0832                return "";
 0833            } catch (Exception) {
 0834                return "Unknown";
 835            }
 2836        }
 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 {
 919        public Dictionary<string, object> Props { get; set; } = new();
 920        public TypedPrincipal[] AllowedToDelegate { get; set; } = Array.Empty<TypedPrincipal>();
 921        public TypedPrincipal[] AllowedToAct { get; set; } = Array.Empty<TypedPrincipal>();
 922        public TypedPrincipal[] SidHistory { get; set; } = Array.Empty<TypedPrincipal>();
 923        public TypedPrincipal[] DumpSMSAPassword { get; set; } = Array.Empty<TypedPrincipal>();
 924        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}