< Summary

Class:SharpHoundCommonLib.Extensions
Assembly:SharpHoundCommonLib
File(s):D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Extensions.cs
Covered lines:91
Uncovered lines:124
Coverable lines:215
Total lines:427
Line coverage:42.3% (91 of 215)
Covered branches:40
Total branches:104
Branch coverage:38.4% (40 of 104)

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.cctor()100%10100%
ToListAsync()50%60100%
PrintEntry(...)0%400%
LdapValue(...)100%100%
LdapValue(...)100%100%
GetSid(...)0%600%
IsComputerCollectionSet(...)100%100%
IsLocalGroupCollectionSet(...)100%100%
Rid(...)100%10100%
GetProperty(...)62.5%8081.81%
GetGuid(...)0%200%
GetSid(...)50%8057.89%
GetPropertyAsArray(...)50%4085.71%
GetPropertyAsArrayOfBytes(...)0%400%
GetPropertyAsBytes(...)0%800%
GetPropertyAsInt(...)50%2066.66%
GetPropertyAsArrayOfCertificates(...)0%200%
GetObjectIdentifier(...)50%20100%
IsDeleted(...)100%100%
GetLabel(...)50%48055.84%

File(s)

D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Extensions.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.DirectoryServices;
 4using System.DirectoryServices.Protocols;
 5using System.Linq;
 6using System.Security.Cryptography.X509Certificates;
 7using System.Security.Principal;
 8using System.Text;
 9using System.Threading.Tasks;
 10using Microsoft.Extensions.Logging;
 11using SharpHoundCommonLib.Enums;
 12
 13namespace SharpHoundCommonLib
 14{
 15    public static class Extensions
 16    {
 17        private const string GMSAClass = "msds-groupmanagedserviceaccount";
 18        private const string MSAClass = "msds-managedserviceaccount";
 19        private static readonly ILogger Log;
 20
 21        static Extensions()
 122        {
 123            Log = Logging.LogProvider.CreateLogger("Extensions");
 124        }
 25
 26        internal static async Task<List<T>> ToListAsync<T>(this IAsyncEnumerable<T> items)
 427        {
 428            var results = new List<T>();
 1829            await foreach (var item in items
 430                               .ConfigureAwait(false))
 331                results.Add(item);
 432            return results;
 433        }
 34
 35        /// <summary>
 36        ///     Helper function to print attributes of a SearchResultEntry
 37        /// </summary>
 38        /// <param name="searchResultEntry"></param>
 39        public static string PrintEntry(this SearchResultEntry searchResultEntry)
 040        {
 041            var sb = new StringBuilder();
 042            if (searchResultEntry.Attributes.AttributeNames == null) return sb.ToString();
 043            foreach (var propertyName in searchResultEntry.Attributes.AttributeNames)
 044            {
 045                var property = propertyName.ToString();
 046                sb.Append(property).Append("\t").Append(searchResultEntry.GetProperty(property)).Append("\n");
 047            }
 48
 049            return sb.ToString();
 050        }
 51
 52        public static string LdapValue(this SecurityIdentifier s)
 053        {
 054            var bytes = new byte[s.BinaryLength];
 055            s.GetBinaryForm(bytes, 0);
 56
 057            var output = $"\\{BitConverter.ToString(bytes).Replace('-', '\\')}";
 058            return output;
 059        }
 60
 61        public static string LdapValue(this Guid s)
 062        {
 063            var bytes = s.ToByteArray();
 064            var output = $"\\{BitConverter.ToString(bytes).Replace('-', '\\')}";
 065            return output;
 066        }
 67
 68        public static string GetSid(this DirectoryEntry result)
 069        {
 70            try
 071            {
 072                if (!result.Properties.Contains(LDAPProperties.ObjectSID))
 073                    return null;
 074            }
 075            catch
 076            {
 077                return null;
 78            }
 79
 080            var s = result.Properties[LDAPProperties.ObjectSID][0];
 081            return s switch
 082            {
 083                byte[] b => new SecurityIdentifier(b, 0).ToString(),
 084                string st => new SecurityIdentifier(Encoding.ASCII.GetBytes(st), 0).ToString(),
 085                _ => null
 086            };
 087        }
 88
 89        /// <summary>
 90        ///     Returns true if any computer collection methods are set
 91        /// </summary>
 92        /// <param name="methods"></param>
 93        /// <returns></returns>
 94        public static bool IsComputerCollectionSet(this ResolvedCollectionMethod methods)
 095        {
 096            return (methods & ResolvedCollectionMethod.ComputerOnly) != 0;
 097        }
 98
 99        /// <summary>
 100        ///     Returns true if any local group collections are set
 101        /// </summary>
 102        /// <param name="methods"></param>
 103        /// <returns></returns>
 104        public static bool IsLocalGroupCollectionSet(this ResolvedCollectionMethod methods)
 0105        {
 0106            return (methods & ResolvedCollectionMethod.LocalGroups) != 0;
 0107        }
 108
 109        /// <summary>
 110        ///     Gets the relative identifier for a SID
 111        /// </summary>
 112        /// <param name="securityIdentifier"></param>
 113        /// <returns></returns>
 114        public static int Rid(this SecurityIdentifier securityIdentifier)
 5115        {
 5116            var value = securityIdentifier.Value;
 5117            var rid = int.Parse(value.Substring(value.LastIndexOf("-", StringComparison.Ordinal) + 1));
 5118            return rid;
 5119        }
 120
 121        #region SearchResultEntry
 122
 123        /// <summary>
 124        ///     Gets the specified property as a string from the SearchResultEntry
 125        /// </summary>
 126        /// <param name="entry"></param>
 127        /// <param name="property">The LDAP name of the property you want to get</param>
 128        /// <returns>The string value of the property if it exists or null</returns>
 129        public static string GetProperty(this SearchResultEntry entry, string property)
 3130        {
 3131            if (!entry.Attributes.Contains(property))
 2132                return null;
 133
 1134            var collection = entry.Attributes[property];
 135            //Use GetValues to auto-convert to the proper type
 1136            var lookups = collection.GetValues(typeof(string));
 1137            if (lookups.Length == 0)
 0138                return null;
 139
 1140            if (lookups[0] is not string prop || prop.Length == 0)
 0141                return null;
 142
 1143            return prop;
 3144        }
 145
 146        /// <summary>
 147        ///     Get's the string representation of the "objectguid" property from the SearchResultEntry
 148        /// </summary>
 149        /// <param name="entry"></param>
 150        /// <returns>The string representation of the object's GUID if possible, otherwise null</returns>
 151        public static string GetGuid(this SearchResultEntry entry)
 0152        {
 0153            if (entry.Attributes.Contains(LDAPProperties.ObjectGUID))
 0154            {
 0155                var guidBytes = entry.GetPropertyAsBytes(LDAPProperties.ObjectGUID);
 156
 0157                return new Guid(guidBytes).ToString().ToUpper();
 158            }
 159
 0160            return null;
 0161        }
 162
 163        /// <summary>
 164        ///     Gets the "objectsid" property as a string from the SearchResultEntry
 165        /// </summary>
 166        /// <param name="entry"></param>
 167        /// <returns>The string representation of the object's SID if possible, otherwise null</returns>
 168        public static string GetSid(this SearchResultEntry entry)
 2169        {
 2170            if (!entry.Attributes.Contains(LDAPProperties.ObjectSID)) return null;
 171
 172            object[] s;
 173            try
 2174            {
 2175                s = entry.Attributes[LDAPProperties.ObjectSID].GetValues(typeof(byte[]));
 2176            }
 0177            catch (NotSupportedException)
 0178            {
 0179                return null;
 180            }
 181
 2182            if (s.Length == 0)
 0183                return null;
 184
 2185            if (s[0] is not byte[] sidBytes || sidBytes.Length == 0)
 0186                return null;
 187
 188            try
 2189            {
 2190                var sid = new SecurityIdentifier(sidBytes, 0);
 2191                return sid.Value.ToUpper();
 192            }
 0193            catch (ArgumentNullException)
 0194            {
 0195                return null;
 196            }
 2197        }
 198
 199        /// <summary>
 200        ///     Gets the specified property as a string array from the SearchResultEntry
 201        /// </summary>
 202        /// <param name="entry"></param>
 203        /// <param name="property">The LDAP name of the property you want to get</param>
 204        /// <returns>The specified property as an array of strings if possible, else an empty array</returns>
 205        public static string[] GetPropertyAsArray(this SearchResultEntry entry, string property)
 2206        {
 2207            if (!entry.Attributes.Contains(property))
 0208                return Array.Empty<string>();
 209
 2210            var values = entry.Attributes[property];
 2211            var strings = values.GetValues(typeof(string));
 212
 2213            return strings is not string[] result ? Array.Empty<string>() : result;
 2214        }
 215
 216        /// <summary>
 217        ///     Gets the specified property as an array of byte arrays from the SearchResultEntry
 218        ///     Used for SIDHistory
 219        /// </summary>
 220        /// <param name="entry"></param>
 221        /// <param name="property">The LDAP name of the property you want to get</param>
 222        /// <returns>The specified property as an array of bytes if possible, else an empty array</returns>
 223        public static byte[][] GetPropertyAsArrayOfBytes(this SearchResultEntry entry, string property)
 0224        {
 0225            if (!entry.Attributes.Contains(property))
 0226                return Array.Empty<byte[]>();
 227
 0228            var values = entry.Attributes[property];
 0229            var bytes = values.GetValues(typeof(byte[]));
 230
 0231            return bytes is not byte[][] result ? Array.Empty<byte[]>() : result;
 0232        }
 233
 234        /// <summary>
 235        ///     Gets the specified property as a byte array
 236        /// </summary>
 237        /// <param name="searchResultEntry"></param>
 238        /// <param name="property">The LDAP name of the property you want to get</param>
 239        /// <returns>An array of bytes if possible, else null</returns>
 240        public static byte[] GetPropertyAsBytes(this SearchResultEntry searchResultEntry, string property)
 0241        {
 0242            if (!searchResultEntry.Attributes.Contains(property))
 0243                return null;
 244
 0245            var collection = searchResultEntry.Attributes[property];
 0246            var lookups = collection.GetValues(typeof(byte[]));
 247
 0248            if (lookups.Length == 0)
 0249                return Array.Empty<byte>();
 250
 0251            if (lookups[0] is not byte[] bytes || bytes.Length == 0)
 0252                return Array.Empty<byte>();
 253
 0254            return bytes;
 0255        }
 256
 257        /// <summary>
 258        ///     Gets the specified property as an int
 259        /// </summary>
 260        /// <param name="entry"></param>
 261        /// <param name="property"></param>
 262        /// <param name="value"></param>
 263        /// <returns></returns>
 264        public static bool GetPropertyAsInt(this SearchResultEntry entry, string property, out int value)
 1265        {
 1266            var prop = entry.GetProperty(property);
 2267            if (prop != null) return int.TryParse(prop, out value);
 0268            value = 0;
 0269            return false;
 1270        }
 271
 272        /// <summary>
 273        ///     Gets the specified property as an array of X509 certificates.
 274        /// </summary>
 275        /// <param name="searchResultEntry"></param>
 276        /// <param name="property"></param>
 277        /// <returns></returns>
 278        public static X509Certificate2[] GetPropertyAsArrayOfCertificates(this SearchResultEntry searchResultEntry,
 279            string property)
 0280        {
 0281            if (!searchResultEntry.Attributes.Contains(property))
 0282                return null;
 283
 0284            return searchResultEntry.GetPropertyAsArrayOfBytes(property).Select(x => new X509Certificate2(x)).ToArray();
 0285        }
 286
 287
 288        /// <summary>
 289        ///     Attempts to get the unique object identifier as used by BloodHound for the Search Result Entry. Tries to
 290        ///     objectsid first, and then objectguid next.
 291        /// </summary>
 292        /// <param name="entry"></param>
 293        /// <returns>String representation of the entry's object identifier or null</returns>
 294        public static string GetObjectIdentifier(this SearchResultEntry entry)
 2295        {
 2296            return entry.GetSid() ?? entry.GetGuid();
 2297        }
 298
 299        /// <summary>
 300        ///     Checks the isDeleted LDAP property to determine if an entry has been deleted from the directory
 301        /// </summary>
 302        /// <param name="entry"></param>
 303        /// <returns></returns>
 304        public static bool IsDeleted(this SearchResultEntry entry)
 0305        {
 0306            var deleted = entry.GetProperty(LDAPProperties.IsDeleted);
 0307            return bool.TryParse(deleted, out var isDeleted) && isDeleted;
 0308        }
 309
 310        /// <summary>
 311        ///     Extension method to determine the BloodHound type of a SearchResultEntry using LDAP properties
 312        ///     Requires ldap properties objectsid, samaccounttype, objectclass
 313        /// </summary>
 314        /// <param name="entry"></param>
 315        /// <returns></returns>
 316        public static Label GetLabel(this SearchResultEntry entry)
 2317        {
 2318            var objectId = entry.GetObjectIdentifier();
 319
 2320            if (objectId == null)
 0321            {
 0322                Log.LogWarning("Failed to get an object identifier for {DN}", entry.DistinguishedName);
 0323                return Label.Base;
 324            }
 325
 2326            if (objectId.StartsWith("S-1") &&
 2327                WellKnownPrincipal.GetWellKnownPrincipal(objectId, out var commonPrincipal))
 0328            {
 0329                Log.LogDebug("GetLabel - {ObjectID} is a WellKnownPrincipal with {Type}", objectId,
 0330                    commonPrincipal.ObjectType);
 0331                return commonPrincipal.ObjectType;
 332            }
 333
 334
 2335            var objectType = Label.Base;
 2336            var samAccountType = entry.GetProperty(LDAPProperties.SAMAccountType);
 2337            var objectClasses = entry.GetPropertyAsArray(LDAPProperties.ObjectClass);
 338
 339            //Override object class for GMSA/MSA accounts
 2340            if (objectClasses != null && (objectClasses.Contains(MSAClass, StringComparer.OrdinalIgnoreCase) ||
 2341                                          objectClasses.Contains(GMSAClass, StringComparer.OrdinalIgnoreCase)))
 0342            {
 0343                Log.LogDebug("GetLabel - {ObjectID} is an MSA/GMSA, returning User", objectId);
 0344                Cache.AddConvertedValue(entry.DistinguishedName, objectId);
 0345                Cache.AddType(objectId, objectType);
 0346                return Label.User;
 347            }
 348
 349
 350            //Its not a common principal. Lets use properties to figure out what it actually is
 2351            if (samAccountType != null) objectType = Helpers.SamAccountTypeToType(samAccountType);
 352
 2353            Log.LogDebug("GetLabel - SamAccountTypeToType returned {Label}", objectType);
 2354            if (objectType != Label.Base)
 0355            {
 0356                Cache.AddConvertedValue(entry.DistinguishedName, objectId);
 0357                Cache.AddType(objectId, objectType);
 0358                return objectType;
 359            }
 360
 361
 2362            if (objectClasses == null)
 0363            {
 0364                Log.LogDebug("GetLabel - ObjectClasses for {ObjectID} is null", objectId);
 0365                objectType = Label.Base;
 0366            }
 367            else
 2368            {
 2369                Log.LogDebug("GetLabel - ObjectClasses for {ObjectID}: {Classes}", objectId,
 2370                    string.Join(", ", objectClasses));
 2371                if (objectClasses.Contains(GroupPolicyContainerClass, StringComparer.InvariantCultureIgnoreCase))
 0372                    objectType = Label.GPO;
 2373                else if (objectClasses.Contains(OrganizationalUnitClass, StringComparer.InvariantCultureIgnoreCase))
 0374                    objectType = Label.OU;
 2375                else if (objectClasses.Contains(DomainClass, StringComparer.InvariantCultureIgnoreCase))
 0376                    objectType = Label.Domain;
 2377                else if (objectClasses.Contains(ContainerClass, StringComparer.InvariantCultureIgnoreCase))
 0378                    objectType = Label.Container;
 2379                else if (objectClasses.Contains(ConfigurationClass, StringComparer.InvariantCultureIgnoreCase))
 0380                    objectType = Label.Configuration;
 2381                else if (objectClasses.Contains(PKICertificateTemplateClass, StringComparer.InvariantCultureIgnoreCase))
 0382                    objectType = Label.CertTemplate;
 2383                else if (objectClasses.Contains(PKIEnrollmentServiceClass, StringComparer.InvariantCultureIgnoreCase))
 0384                    objectType = Label.EnterpriseCA;
 2385                else if (objectClasses.Contains(CertificationAuthorityClass, StringComparer.InvariantCultureIgnoreCase))
 0386                {
 0387                    if (entry.DistinguishedName.Contains(DirectoryPaths.RootCALocation))
 0388                        objectType = Label.RootCA;
 0389                    else if (entry.DistinguishedName.Contains(DirectoryPaths.AIACALocation))
 0390                        objectType = Label.AIACA;
 0391                    else if (entry.DistinguishedName.Contains(DirectoryPaths.NTAuthStoreLocation))
 0392                        objectType = Label.NTAuthStore;
 2393                }else if (objectClasses.Contains(OIDContainerClass, StringComparer.InvariantCultureIgnoreCase))
 2394                {
 2395                    if (entry.DistinguishedName.StartsWith(DirectoryPaths.OIDContainerLocation,
 2396                            StringComparison.InvariantCultureIgnoreCase))
 1397                        objectType = Label.Container;
 398                    else
 1399                    {
 1400                        if (entry.GetPropertyAsInt(LDAPProperties.Flags, out var flags) && flags == 2)
 1401                        {
 1402                            objectType = Label.IssuancePolicy;
 1403                        }
 1404                    }
 2405                }
 2406            }
 407
 2408            Log.LogDebug("GetLabel - Final label for {ObjectID}: {Label}", objectId, objectType);
 409
 2410            Cache.AddConvertedValue(entry.DistinguishedName, objectId);
 2411            Cache.AddType(objectId, objectType);
 2412            return objectType;
 2413        }
 414
 415        private const string GroupPolicyContainerClass = "groupPolicyContainer";
 416        private const string OrganizationalUnitClass = "organizationalUnit";
 417        private const string DomainClass = "domain";
 418        private const string ContainerClass = "container";
 419        private const string ConfigurationClass = "configuration";
 420        private const string PKICertificateTemplateClass = "pKICertificateTemplate";
 421        private const string PKIEnrollmentServiceClass = "pKIEnrollmentService";
 422        private const string CertificationAuthorityClass = "certificationAuthority";
 423        private const string OIDContainerClass = "msPKI-Enterprise-Oid";
 424
 425        #endregion
 426    }
 427}