< Summary

Class:SharpHoundCommonLib.Processors.GPOLocalGroupProcessor
Assembly:SharpHoundCommonLib
File(s):D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Processors\GPOLocalGroupProcessor.cs
Covered lines:320
Uncovered lines:68
Coverable lines:388
Total lines:627
Line coverage:82.4% (320 of 388)
Covered branches:122
Total branches:156
Branch coverage:78.2% (122 of 156)

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.cctor()100%10100%
.ctor(...)100%20100%
ReadGPOLocalGroups(...)100%100%
ReadGPOLocalGroups()56.14%57069.52%
ProcessGPOTemplateFile()95.55%45092.85%
GetSid(...)83.33%6080.76%
ProcessGPOXmlFile()86.95%46091.66%
ToTypedPrincipal()100%10100%
ToString()100%10100%
.ctor()100%10100%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.DirectoryServices.Protocols;
 5using System.IO;
 6using System.Linq;
 7using System.Text.RegularExpressions;
 8using System.Threading.Tasks;
 9using System.Xml.XPath;
 10using Microsoft.Extensions.Logging;
 11using SharpHoundCommonLib.Enums;
 12using SharpHoundCommonLib.LDAPQueries;
 13using SharpHoundCommonLib.OutputTypes;
 14
 15namespace SharpHoundCommonLib.Processors
 16{
 17    public class GPOLocalGroupProcessor
 18    {
 119        private static readonly Regex KeyRegex = new(@"(.+?)\s*=(.*)", RegexOptions.Compiled);
 20
 121        private static readonly Regex MemberRegex =
 122            new(@"\[Group Membership\](.*)(?:\[|$)", RegexOptions.Compiled | RegexOptions.Singleline);
 23
 124        private static readonly Regex MemberLeftRegex =
 125            new(@"(.*(?:S-1-5-32-544|S-1-5-32-555|S-1-5-32-562|S-1-5-32-580)__Members)", RegexOptions.Compiled |
 126                RegexOptions.IgnoreCase);
 27
 128        private static readonly Regex MemberRightRegex =
 129            new(@"(S-1-5-32-544|S-1-5-32-555|S-1-5-32-562|S-1-5-32-580)", RegexOptions.Compiled |
 130                                                                          RegexOptions.IgnoreCase);
 31
 132        private static readonly Regex ExtractRid =
 133            new(@"S-1-5-32-([0-9]{3})", RegexOptions.Compiled | RegexOptions.IgnoreCase);
 34
 135        private static readonly ConcurrentDictionary<string, List<GroupAction>> GpoActionCache = new();
 36
 137        private static readonly Dictionary<string, LocalGroupRids> ValidGroupNames =
 138            new(StringComparer.OrdinalIgnoreCase)
 139            {
 140                {"Administrators", LocalGroupRids.Administrators},
 141                {"Remote Desktop Users", LocalGroupRids.RemoteDesktopUsers},
 142                {"Remote Management Users", LocalGroupRids.PSRemote},
 143                {"Distributed COM Users", LocalGroupRids.DcomUsers}
 144            };
 45
 46        private readonly ILogger _log;
 47
 48        private readonly ILDAPUtils _utils;
 49
 1150        public GPOLocalGroupProcessor(ILDAPUtils utils, ILogger log = null)
 1151        {
 1152            _utils = utils;
 1153            _log = log ?? Logging.LogProvider.CreateLogger("GPOLocalGroupProc");
 1154        }
 55
 56        public Task<ResultingGPOChanges> ReadGPOLocalGroups(ISearchResultEntry entry)
 057        {
 058            var links = entry.GetProperty(LDAPProperties.GPLink);
 059            var dn = entry.DistinguishedName;
 060            return ReadGPOLocalGroups(links, dn);
 061        }
 62
 63        public async Task<ResultingGPOChanges> ReadGPOLocalGroups(string gpLink, string distinguishedName)
 464        {
 465            var ret = new ResultingGPOChanges();
 66            //If the gplink property is null, we don't need to process anything
 467            if (gpLink == null)
 168                return ret;
 69
 70            // First lets check if this OU actually has computers that it contains. If not, then we'll ignore it.
 71            // Its cheaper to fetch the affected computers from LDAP first and then process the GPLinks
 372            var options = new LDAPQueryOptions
 373            {
 374                Filter = new LDAPFilter().AddComputersNoMSAs().GetFilter(),
 375                Scope = SearchScope.Subtree,
 376                Properties = CommonProperties.ObjectSID,
 377                AdsPath = distinguishedName
 378            };
 79
 380            var affectedComputers = _utils.QueryLDAP(options)
 281                .Select(x => x.GetSid())
 282                .Where(x => x != null)
 583                .Select(x => new TypedPrincipal
 584                {
 585                    ObjectIdentifier = x,
 586                    ObjectType = Label.Computer
 587                }).ToArray();
 88
 89            //If there's no computers then we don't care about this OU
 390            if (affectedComputers.Length == 0)
 191                return ret;
 92
 293            var enforced = new List<string>();
 294            var unenforced = new List<string>();
 95
 96            // Split our link property up and remove disabled links
 1497            foreach (var link in Helpers.SplitGPLinkProperty(gpLink))
 498                switch (link.Status)
 99                {
 100                    case "0":
 2101                        unenforced.Add(link.DistinguishedName);
 2102                        break;
 103                    case "2":
 2104                        enforced.Add(link.DistinguishedName);
 2105                        break;
 106                }
 107
 108            //Set up our links in the correct order.
 109            // Enforced links override unenforced, and also respect the order in which they are placed in the GPLink pro
 2110            var orderedLinks = new List<string>();
 2111            orderedLinks.AddRange(unenforced);
 2112            orderedLinks.AddRange(enforced);
 113
 2114            var data = new Dictionary<LocalGroupRids, GroupResults>();
 36115            foreach (var rid in Enum.GetValues(typeof(LocalGroupRids))) data[(LocalGroupRids) rid] = new GroupResults();
 116
 14117            foreach (var linkDn in orderedLinks)
 4118            {
 4119                if (!GpoActionCache.TryGetValue(linkDn.ToLower(), out var actions))
 2120                {
 2121                    actions = new List<GroupAction>();
 122
 2123                    var gpoDomain = Helpers.DistinguishedNameToDomain(linkDn);
 124
 2125                    var opts = new LDAPQueryOptions
 2126                    {
 2127                        Filter = new LDAPFilter().AddAllObjects().GetFilter(),
 2128                        Scope = SearchScope.Base,
 2129                        Properties = CommonProperties.GPCFileSysPath,
 2130                        AdsPath = linkDn
 2131                    };
 2132                    var filePath = _utils.QueryLDAP(opts).FirstOrDefault()?
 2133                        .GetProperty(LDAPProperties.GPCFileSYSPath);
 134
 2135                    if (filePath == null)
 2136                    {
 2137                        GpoActionCache.TryAdd(linkDn, actions);
 2138                        continue;
 139                    }
 140
 141                    //Add the actions for each file. The GPO template file actions will override the XML file actions
 0142                    actions.AddRange(ProcessGPOXmlFile(filePath, gpoDomain).ToList());
 0143                    await foreach (var item in ProcessGPOTemplateFile(filePath, gpoDomain)) actions.Add(item);
 0144                }
 145
 146                //Cache the actions for this GPO for later
 2147                GpoActionCache.TryAdd(linkDn.ToLower(), actions);
 148
 149                //If there are no actions, then we can move on from this GPO
 2150                if (actions.Count == 0)
 2151                    continue;
 152
 153                //First lets process restricted members
 0154                var restrictedMemberSets = actions.Where(x => x.Target == GroupActionTarget.RestrictedMember)
 0155                    .GroupBy(x => x.TargetRid);
 156
 0157                foreach (var set in restrictedMemberSets)
 0158                {
 0159                    var results = data[set.Key];
 0160                    var members = set.Select(x => x.ToTypedPrincipal()).ToList();
 0161                    results.RestrictedMember = members;
 0162                    data[set.Key] = results;
 0163                }
 164
 165                //Next add in our restricted MemberOf sets
 0166                var restrictedMemberOfSets = actions.Where(x => x.Target == GroupActionTarget.RestrictedMemberOf)
 0167                    .GroupBy(x => x.TargetRid);
 168
 0169                foreach (var set in restrictedMemberOfSets)
 0170                {
 0171                    var results = data[set.Key];
 0172                    var members = set.Select(x => x.ToTypedPrincipal()).ToList();
 0173                    results.RestrictedMemberOf = members;
 0174                    data[set.Key] = results;
 0175                }
 176
 177                // Now work through the LocalGroup targets
 0178                var localGroupSets = actions.Where(x => x.Target == GroupActionTarget.LocalGroup)
 0179                    .GroupBy(x => x.TargetRid);
 180
 0181                foreach (var set in localGroupSets)
 0182                {
 0183                    var results = data[set.Key];
 0184                    foreach (var temp in set)
 0185                    {
 0186                        var res = temp.ToTypedPrincipal();
 0187                        var newMembers = results.LocalGroups;
 0188                        switch (temp.Action)
 189                        {
 190                            case GroupActionOperation.Add:
 0191                                newMembers.Add(res);
 0192                                break;
 193                            case GroupActionOperation.Delete:
 0194                                newMembers.RemoveAll(x => x.ObjectIdentifier == res.ObjectIdentifier);
 0195                                break;
 196                            case GroupActionOperation.DeleteUsers:
 0197                                newMembers.RemoveAll(x => x.ObjectType == Label.User);
 0198                                break;
 199                            case GroupActionOperation.DeleteGroups:
 0200                                newMembers.RemoveAll(x => x.ObjectType == Label.Group);
 0201                                break;
 202                        }
 203
 0204                        data[set.Key].LocalGroups = newMembers;
 0205                    }
 0206                }
 0207            }
 208
 2209            ret.AffectedComputers = affectedComputers;
 210
 211            //At this point, we've resolved individual add/substract methods for each linked GPO.
 212            //Now we need to actually squish them together into the resulting set of changes
 26213            foreach (var kvp in data)
 10214            {
 10215                var key = kvp.Key;
 10216                var val = kvp.Value;
 10217                var rm = val.RestrictedMember;
 10218                var rmo = val.RestrictedMemberOf;
 10219                var gm = val.LocalGroups;
 220
 10221                var final = new List<TypedPrincipal>();
 222
 223                // If we're setting RestrictedMembers, it overrides LocalGroups due to order of operations. Restricted M
 10224                final.AddRange(rmo);
 10225                final.AddRange(rm.Count > 0 ? rm : gm);
 226
 10227                var finalArr = final.Distinct().ToArray();
 228
 10229                switch (key)
 230                {
 231                    case LocalGroupRids.Administrators:
 2232                        ret.LocalAdmins = finalArr;
 2233                        break;
 234                    case LocalGroupRids.RemoteDesktopUsers:
 2235                        ret.RemoteDesktopUsers = finalArr;
 2236                        break;
 237                    case LocalGroupRids.DcomUsers:
 2238                        ret.DcomUsers = finalArr;
 2239                        break;
 240                    case LocalGroupRids.PSRemote:
 2241                        ret.PSRemoteUsers = finalArr;
 2242                        break;
 243                }
 10244            }
 245
 2246            return ret;
 4247        }
 248
 249        /// <summary>
 250        ///     Parses a GPO GptTmpl.inf file and pulls group membership changes out
 251        /// </summary>
 252        /// <param name="basePath"></param>
 253        /// <param name="gpoDomain"></param>
 254        /// <returns></returns>
 255        internal async IAsyncEnumerable<GroupAction> ProcessGPOTemplateFile(string basePath, string gpoDomain)
 4256        {
 4257            var templatePath = Path.Combine(basePath, "MACHINE", "Microsoft", "Windows NT", "SecEdit", "GptTmpl.inf");
 258
 4259            if (!File.Exists(templatePath))
 1260                yield break;
 261
 262            FileStream fs;
 263            try
 3264            {
 3265                fs = new FileStream(templatePath, FileMode.Open, FileAccess.Read);
 3266            }
 0267            catch
 0268            {
 0269                yield break;
 270            }
 271
 3272            using var reader = new StreamReader(fs);
 3273            var content = await reader.ReadToEndAsync();
 3274            var memberMatch = MemberRegex.Match(content);
 275
 3276            if (!memberMatch.Success)
 1277                yield break;
 278
 279            //We've got a match! Lets figure out whats going on
 2280            var memberText = memberMatch.Groups[1].Value.Trim();
 281            //Split our text into individual lines
 2282            var memberLines = Regex.Split(memberText, @"\r\n|\r|\n");
 283
 18284            foreach (var memberLine in memberLines)
 6285            {
 286                //Check if the Key regex matches (S-1-5.*_memberof=blah)
 6287                var keyMatch = KeyRegex.Match(memberLine);
 288
 6289                if (!keyMatch.Success)
 0290                    continue;
 291
 6292                var key = keyMatch.Groups[1].Value.Trim();
 6293                var val = keyMatch.Groups[2].Value.Trim();
 294
 6295                var leftMatch = MemberLeftRegex.Match(key);
 6296                var rightMatches = MemberRightRegex.Matches(val);
 297
 298                //If leftmatch is a success, the members of a group are being explicitly set
 6299                if (leftMatch.Success)
 2300                {
 2301                    var extracted = ExtractRid.Match(leftMatch.Value);
 2302                    var rid = int.Parse(extracted.Groups[1].Value);
 303
 2304                    if (Enum.IsDefined(typeof(LocalGroupRids), rid))
 305                        //Loop over the members in the match, and try to convert them to SIDs
 10306                        foreach (var member in val.Split(','))
 2307                        {
 2308                            var res = GetSid(member.Trim('*'), gpoDomain);
 2309                            if (res == null)
 1310                                continue;
 1311                            yield return new GroupAction
 1312                            {
 1313                                Target = GroupActionTarget.RestrictedMember,
 1314                                Action = GroupActionOperation.Add,
 1315                                TargetSid = res.ObjectIdentifier,
 1316                                TargetType = res.ObjectType,
 1317                                TargetRid = (LocalGroupRids) rid
 1318                            };
 1319                        }
 2320                }
 321
 322                //If right match is a success, a group has been set as a member of one of our local groups
 6323                var index = key.IndexOf("MemberOf", StringComparison.CurrentCultureIgnoreCase);
 6324                if (rightMatches.Count > 0 && index > 0)
 2325                {
 2326                    var account = key.Trim('*').Substring(0, index - 3).ToUpper();
 327
 2328                    var res = GetSid(account, gpoDomain);
 2329                    if (res == null)
 0330                        continue;
 331
 10332                    foreach (var match in rightMatches)
 2333                    {
 2334                        var rid = int.Parse(ExtractRid.Match(match.ToString()).Groups[1].Value);
 2335                        if (!Enum.IsDefined(typeof(LocalGroupRids), rid)) continue;
 336
 2337                        var targetGroup = (LocalGroupRids) rid;
 2338                        yield return new GroupAction
 2339                        {
 2340                            Target = GroupActionTarget.RestrictedMemberOf,
 2341                            Action = GroupActionOperation.Add,
 2342                            TargetRid = targetGroup,
 2343                            TargetSid = res.ObjectIdentifier,
 2344                            TargetType = res.ObjectType
 2345                        };
 2346                    }
 2347                }
 6348            }
 4349        }
 350
 351        /// <summary>
 352        ///     Resolves a SID to its type
 353        /// </summary>
 354        /// <param name="account"></param>
 355        /// <param name="domainName"></param>
 356        /// <returns></returns>
 357        private TypedPrincipal GetSid(string account, string domainName)
 4358        {
 4359            if (!account.StartsWith("S-1-", StringComparison.CurrentCulture))
 2360            {
 361                string user;
 362                string domain;
 2363                if (account.Contains('\\'))
 0364                {
 365                    //The account is in the format DOMAIN\\username
 0366                    var split = account.Split('\\');
 0367                    domain = split[0];
 0368                    user = split[1];
 0369                }
 370                else
 2371                {
 372                    //The account is just a username, so try with the current domain
 2373                    domain = domainName;
 2374                    user = account;
 2375                }
 376
 2377                user = user.ToUpper();
 378
 379                //Try to resolve as a user object first
 2380                var res = _utils.ResolveAccountName(user, domain);
 2381                if (res != null)
 1382                    return res;
 383
 1384                res = _utils.ResolveAccountName($"{user}$", domain);
 1385                return res;
 386            }
 387
 388            //The element is just a sid, so return it straight
 2389            var lType = _utils.LookupSidType(account, domainName);
 2390            return new TypedPrincipal
 2391            {
 2392                ObjectIdentifier = account,
 2393                ObjectType = lType
 2394            };
 4395        }
 396
 397        /// <summary>
 398        ///     Parses a GPO Groups.xml file and pulls group membership changes out
 399        /// </summary>
 400        /// <param name="basePath"></param>
 401        /// <param name="gpoDomain"></param>
 402        /// <returns>A list of GPO "Actions"</returns>
 403        internal IEnumerable<GroupAction> ProcessGPOXmlFile(string basePath, string gpoDomain)
 3404        {
 3405            var xmlPath = Path.Combine(basePath, "MACHINE", "Preferences", "Groups", "Groups.xml");
 406
 407            //If the file doesn't exist, then just return
 3408            if (!File.Exists(xmlPath))
 1409                yield break;
 410
 411            //Create an XPathDocument to let us navigate the XML
 412            XPathDocument doc;
 413            try
 2414            {
 2415                doc = new XPathDocument(xmlPath);
 2416            }
 0417            catch (Exception e)
 0418            {
 0419                _log.LogError(e, "error reading GPO XML file {File}", xmlPath);
 0420                yield break;
 421            }
 422
 2423            var navigator = doc.CreateNavigator();
 424            //Grab all the Groups nodes
 2425            var groupsNodes = navigator.Select("/Groups");
 426
 4427            while (groupsNodes.MoveNext())
 2428            {
 2429                var current = groupsNodes.Current;
 430                //If disable is set to 1, then this Group wont apply
 2431                if (current.GetAttribute("disabled", "") is "1")
 1432                    continue;
 433
 1434                var groupNodes = current.Select("Group");
 6435                while (groupNodes.MoveNext())
 5436                {
 437                    //Grab the properties for each Group node. Current path is /Groups/Group
 5438                    var groupProperties = groupNodes.Current.Select("Properties");
 10439                    while (groupProperties.MoveNext())
 5440                    {
 5441                        var currentProperties = groupProperties.Current;
 5442                        var action = currentProperties.GetAttribute("action", "");
 443
 444                        //The only action that works for built in groups is Update.
 5445                        if (!action.Equals("u", StringComparison.OrdinalIgnoreCase))
 1446                            continue;
 447
 4448                        var groupSid = currentProperties.GetAttribute("groupSid", "")?.Trim();
 4449                        var groupName = currentProperties.GetAttribute("groupName", "")?.Trim();
 450
 451                        //Next is to determine what group is being updated.
 452
 4453                        var targetGroup = LocalGroupRids.None;
 4454                        if (!string.IsNullOrWhiteSpace(groupSid))
 2455                        {
 456                            //Use a regex to match and attempt to extract the RID
 2457                            var s = ExtractRid.Match(groupSid);
 2458                            if (s.Success)
 2459                            {
 2460                                var rid = int.Parse(s.Groups[1].Value);
 2461                                if (Enum.IsDefined(typeof(LocalGroupRids), rid))
 2462                                    targetGroup = (LocalGroupRids) rid;
 2463                            }
 2464                        }
 465
 4466                        if (!string.IsNullOrWhiteSpace(groupName) && targetGroup == LocalGroupRids.None)
 2467                            ValidGroupNames.TryGetValue(groupName, out targetGroup);
 468
 469                        //If targetGroup is still None, we've failed to resolve a group target. No point in continuing
 4470                        if (targetGroup == LocalGroupRids.None)
 1471                            continue;
 472
 3473                        var deleteUsers = currentProperties.GetAttribute("deleteAllUsers", "") == "1";
 3474                        var deleteGroups = currentProperties.GetAttribute("deleteAllGroups", "") == "1";
 475
 3476                        if (deleteUsers)
 1477                            yield return new GroupAction
 1478                            {
 1479                                Action = GroupActionOperation.DeleteUsers,
 1480                                Target = GroupActionTarget.LocalGroup,
 1481                                TargetRid = targetGroup
 1482                            };
 483
 3484                        if (deleteGroups)
 1485                            yield return new GroupAction
 1486                            {
 1487                                Action = GroupActionOperation.DeleteGroups,
 1488                                Target = GroupActionTarget.LocalGroup,
 1489                                TargetRid = targetGroup
 1490                            };
 491
 492                        //Get all the actual members being added
 3493                        var members = currentProperties.Select("Members/Member");
 10494                        while (members.MoveNext())
 7495                        {
 7496                            var memberAction = members.Current.GetAttribute("action", "")
 7497                                .Equals("ADD", StringComparison.OrdinalIgnoreCase)
 7498                                ? GroupActionOperation.Add
 7499                                : GroupActionOperation.Delete;
 500
 7501                            var memberName = members.Current.GetAttribute("name", "");
 7502                            var memberSid = members.Current.GetAttribute("sid", "");
 503
 7504                            var ga = new GroupAction
 7505                            {
 7506                                Action = memberAction
 7507                            };
 508
 509                            //If we have a memberSid, this is the best case scenario
 7510                            if (!string.IsNullOrWhiteSpace(memberSid))
 5511                            {
 5512                                var memberType =
 5513                                    _utils.LookupSidType(memberSid, _utils.GetDomainNameFromSid(memberSid));
 5514                                ga.Target = GroupActionTarget.LocalGroup;
 5515                                ga.TargetSid = memberSid;
 5516                                ga.TargetType = memberType;
 5517                                ga.TargetRid = targetGroup;
 518
 5519                                yield return ga;
 5520                                continue;
 521                            }
 522
 523                            //If we have a memberName, we need to resolve it to a SID/Type
 2524                            if (!string.IsNullOrWhiteSpace(memberName))
 2525                            {
 526                                //Check if the name is domain prefixed
 2527                                if (memberName.Contains("\\"))
 1528                                {
 1529                                    var s = memberName.Split('\\');
 1530                                    var name = s[1];
 1531                                    var domain = s[0];
 532
 1533                                    var res = _utils.ResolveAccountName(name, domain);
 1534                                    if (res == null)
 0535                                    {
 0536                                        _log.LogWarning("Failed to resolve member {memberName}", memberName);
 0537                                        continue;
 538                                    }
 1539                                    ga.Target = GroupActionTarget.LocalGroup;
 1540                                    ga.TargetSid = res.ObjectIdentifier;
 1541                                    ga.TargetType = res.ObjectType;
 1542                                    ga.TargetRid = targetGroup;
 1543                                    yield return ga;
 1544                                }
 545                                else
 1546                                {
 1547                                    var res = _utils.ResolveAccountName(memberName, gpoDomain);
 1548                                    if (res == null)
 0549                                    {
 0550                                        _log.LogWarning("Failed to resolve member {memberName}", memberName);
 0551                                        continue;
 552                                    }
 1553                                    ga.Target = GroupActionTarget.LocalGroup;
 1554                                    ga.TargetSid = res.ObjectIdentifier;
 1555                                    ga.TargetType = res.ObjectType;
 1556                                    ga.TargetRid = targetGroup;
 1557                                    yield return ga;
 1558                                }
 2559                            }
 2560                        }
 3561                    }
 5562                }
 1563            }
 2564        }
 565
 566        /// <summary>
 567        ///     Represents an action from a GPO
 568        /// </summary>
 569        internal class GroupAction
 570        {
 13571            internal GroupActionOperation Action { get; set; }
 13572            internal GroupActionTarget Target { get; set; }
 12573            internal string TargetSid { get; set; }
 12574            internal Label TargetType { get; set; }
 13575            internal LocalGroupRids TargetRid { get; set; }
 576
 577            public TypedPrincipal ToTypedPrincipal()
 1578            {
 1579                return new TypedPrincipal
 1580                {
 1581                    ObjectIdentifier = TargetSid,
 1582                    ObjectType = TargetType
 1583                };
 1584            }
 585
 586            public override string ToString()
 1587            {
 1588                return
 1589                    $"{nameof(Action)}: {Action}, {nameof(Target)}: {Target}, {nameof(TargetSid)}: {TargetSid}, {nameof(
 1590            }
 591        }
 592
 593        /// <summary>
 594        ///     Storage for each different group type
 595        /// </summary>
 596        public class GroupResults
 597        {
 10598            public List<TypedPrincipal> LocalGroups = new();
 10599            public List<TypedPrincipal> RestrictedMember = new();
 10600            public List<TypedPrincipal> RestrictedMemberOf = new();
 601        }
 602
 603        internal enum GroupActionOperation
 604        {
 605            Add,
 606            Delete,
 607            DeleteUsers,
 608            DeleteGroups
 609        }
 610
 611        internal enum GroupActionTarget
 612        {
 613            RestrictedMemberOf,
 614            RestrictedMember,
 615            LocalGroup
 616        }
 617
 618        internal enum LocalGroupRids
 619        {
 620            None = 0,
 621            Administrators = 544,
 622            RemoteDesktopUsers = 555,
 623            DcomUsers = 562,
 624            PSRemote = 580
 625        }
 626    }
 627}