< Summary

Class:SharpHoundCommonLib.Processors.GPOLocalGroupProcessor
Assembly:SharpHoundCommonLib
File(s):D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Processors\GPOLocalGroupProcessor.cs
Covered lines:304
Uncovered lines:46
Coverable lines:350
Total lines:577
Line coverage:86.8% (304 of 350)
Covered branches:197
Total branches:259
Branch coverage:76% (197 of 259)

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.cctor()100%10100%
.ctor(...)100%20100%
ReadGPOLocalGroups(...)0%400%
ReadGPOLocalGroups()74.15%89084.54%
ProcessGPOTemplateFile()90.62%64095%
GetSid()75%120100%
ProcessGPOXmlFile()76.38%72089.65%
ToTypedPrincipal()100%10100%
Equals(...)50%80100%
Equals(...)50%60100%
GetHashCode()0%200%
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    public class GPOLocalGroupProcessor {
 117        private static readonly Regex KeyRegex = new(@"(.+?)\s*=(.*)", RegexOptions.Compiled);
 18
 119        private static readonly Regex MemberRegex =
 120            new(@"\[Group Membership\](.*)(?:\[|$)", RegexOptions.Compiled | RegexOptions.Singleline);
 21
 122        private static readonly Regex MemberLeftRegex =
 123            new(@"(.*(?:S-1-5-32-544|S-1-5-32-555|S-1-5-32-562|S-1-5-32-580)__Members)", RegexOptions.Compiled |
 124                RegexOptions.IgnoreCase);
 25
 126        private static readonly Regex MemberRightRegex =
 127            new(@"(S-1-5-32-544|S-1-5-32-555|S-1-5-32-562|S-1-5-32-580)", RegexOptions.Compiled |
 128                                                                          RegexOptions.IgnoreCase);
 29
 130        private static readonly Regex ExtractRid =
 131            new(@"S-1-5-32-([0-9]{3})", RegexOptions.Compiled | RegexOptions.IgnoreCase);
 32
 133        private static readonly ConcurrentDictionary<string, List<GroupAction>> GpoActionCache = new();
 34
 135        private static readonly Dictionary<string, LocalGroupRids> ValidGroupNames =
 136            new(StringComparer.OrdinalIgnoreCase) {
 137                { "Administrators", LocalGroupRids.Administrators },
 138                { "Remote Desktop Users", LocalGroupRids.RemoteDesktopUsers },
 139                { "Remote Management Users", LocalGroupRids.PSRemote },
 140                { "Distributed COM Users", LocalGroupRids.DcomUsers }
 141            };
 42
 43        private readonly ILogger _log;
 44
 45        private readonly ILdapUtils _utils;
 46
 2247        public GPOLocalGroupProcessor(ILdapUtils utils, ILogger log = null) {
 1148            _utils = utils;
 1149            _log = log ?? Logging.LogProvider.CreateLogger("GPOLocalGroupProc");
 1150        }
 51
 052        public Task<ResultingGPOChanges> ReadGPOLocalGroups(IDirectoryObject entry) {
 053            if (entry.TryGetProperty(LDAPProperties.GPLink, out var links) && entry.TryGetDistinguishedName(out var dn))
 054                return ReadGPOLocalGroups(links, dn);
 55            }
 56
 057            return Task.FromResult(new ResultingGPOChanges());
 058        }
 59
 460        public async Task<ResultingGPOChanges> ReadGPOLocalGroups(string gpLink, string distinguishedName) {
 461            var ret = new ResultingGPOChanges();
 62            //If the gplink property is null, we don't need to process anything
 463            if (gpLink == null)
 164                return ret;
 65
 66            string domain;
 67            //If our dn is null, use our default domain
 568            if (string.IsNullOrEmpty(distinguishedName)) {
 369                if (!_utils.GetDomain(out var d)) {
 170                    return ret;
 71                }
 72
 173                domain = d.Name;
 274            } else {
 175                domain = Helpers.DistinguishedNameToDomain(distinguishedName);
 176            }
 77
 78            // First lets check if this OU actually has computers that it contains. If not, then we'll ignore it.
 79            // Its cheaper to fetch the affected computers from LDAP first and then process the GPLinks
 280            var affectedComputers = new List<TypedPrincipal>();
 1081            await foreach (var result in _utils.Query(new LdapQueryParameters() {
 282                               LDAPFilter = new LdapFilter().AddComputersNoMSAs().GetFilter(),
 283                               Attributes = CommonProperties.ObjectSID,
 284                               SearchBase = distinguishedName,
 285                               DomainName = domain
 486                           })) {
 287                if (!result.IsSuccess) {
 088                    break;
 89                }
 90
 291                var entry = result.Value;
 292                if (!entry.TryGetSecurityIdentifier(out var sid)) {
 093                    continue;
 94                }
 95
 296                affectedComputers.Add(new TypedPrincipal(sid, Label.Computer));
 297            }
 98
 99            //If there's no computers then we don't care about this OU
 2100            if (affectedComputers.Count == 0)
 0101                return ret;
 102
 2103            var enforced = new List<string>();
 2104            var unenforced = new List<string>();
 105
 106            // Split our link property up and remove disabled links
 14107            foreach (var link in Helpers.SplitGPLinkProperty(gpLink))
 4108                switch (link.Status) {
 109                    case "0":
 2110                        unenforced.Add(link.DistinguishedName);
 2111                        break;
 112                    case "2":
 2113                        enforced.Add(link.DistinguishedName);
 2114                        break;
 115                }
 116
 117            //Set up our links in the correct order.
 118            //Enforced links override unenforced, and also respect the order in which they are placed in the GPLink prop
 2119            var orderedLinks = new List<string>();
 2120            orderedLinks.AddRange(unenforced);
 2121            orderedLinks.AddRange(enforced);
 122
 2123            var data = new Dictionary<LocalGroupRids, GroupResults>();
 36124            foreach (var rid in Enum.GetValues(typeof(LocalGroupRids))) data[(LocalGroupRids)rid] = new GroupResults();
 125
 18126            foreach (var linkDn in orderedLinks) {
 8127                if (!GpoActionCache.TryGetValue(linkDn.ToLower(), out var actions)) {
 4128                    actions = new List<GroupAction>();
 129
 4130                    var gpoDomain = Helpers.DistinguishedNameToDomain(linkDn);
 4131                    var result = await _utils.Query(new LdapQueryParameters() {
 4132                        LDAPFilter = new LdapFilter().AddAllObjects().GetFilter(),
 4133                        SearchScope = SearchScope.Base,
 4134                        Attributes = CommonProperties.GPCFileSysPath,
 4135                        SearchBase = linkDn,
 4136                        DomainName = gpoDomain
 4137                    }).DefaultIfEmpty(LdapResult<IDirectoryObject>.Fail()).FirstOrDefaultAsync();
 138
 7139                    if (!result.IsSuccess) {
 3140                        continue;
 141                    }
 142
 1143                    if (!result.Value.TryGetProperty(LDAPProperties.GPCFileSYSPath, out var filePath)) {
 0144                        GpoActionCache.TryAdd(linkDn, actions);
 0145                        continue;
 146                    }
 147
 148                    //Add the actions for each file. The GPO template file actions will override the XML file actions
 9149                    await foreach (var item  in ProcessGPOXmlFile(filePath, gpoDomain)) actions.Add(item);
 3150                    await foreach (var item in ProcessGPOTemplateFile(filePath, gpoDomain)) actions.Add(item);
 1151                }
 152
 153                //Cache the actions for this GPO for later
 1154                GpoActionCache.TryAdd(linkDn.ToLower(), actions);
 155
 156                //If there are no actions, then we can move on from this GPO
 1157                if (actions.Count == 0)
 0158                    continue;
 159
 160                //First lets process restricted members
 3161                var restrictedMemberSets = actions.Where(x => x.Target == GroupActionTarget.RestrictedMember)
 1162                    .GroupBy(x => x.TargetRid);
 163
 3164                foreach (var set in restrictedMemberSets) {
 0165                    var results = data[set.Key];
 0166                    var members = set.Select(x => x.ToTypedPrincipal()).ToList();
 0167                    results.RestrictedMember = members;
 0168                    data[set.Key] = results;
 0169                }
 170
 171                //Next add in our restricted MemberOf sets
 3172                var restrictedMemberOfSets = actions.Where(x => x.Target == GroupActionTarget.RestrictedMemberOf)
 1173                    .GroupBy(x => x.TargetRid);
 174
 3175                foreach (var set in restrictedMemberOfSets) {
 0176                    var results = data[set.Key];
 0177                    var members = set.Select(x => x.ToTypedPrincipal()).ToList();
 0178                    results.RestrictedMemberOf = members;
 0179                    data[set.Key] = results;
 0180                }
 181
 182                // Now work through the LocalGroup targets
 3183                var localGroupSets = actions.Where(x => x.Target == GroupActionTarget.LocalGroup)
 3184                    .GroupBy(x => x.TargetRid);
 185
 6186                foreach (var set in localGroupSets) {
 1187                    var results = data[set.Key];
 9188                    foreach (var temp in set) {
 2189                        var res = temp.ToTypedPrincipal();
 2190                        var newMembers = results.LocalGroups;
 2191                        switch (temp.Action) {
 192                            case GroupActionOperation.Add:
 0193                                newMembers.Add(res);
 0194                                break;
 195                            case GroupActionOperation.Delete:
 0196                                newMembers.RemoveAll(x => x.ObjectIdentifier == res.ObjectIdentifier);
 0197                                break;
 198                            case GroupActionOperation.DeleteUsers:
 1199                                newMembers.RemoveAll(x => x.ObjectType == Label.User);
 1200                                break;
 201                            case GroupActionOperation.DeleteGroups:
 1202                                newMembers.RemoveAll(x => x.ObjectType == Label.Group);
 1203                                break;
 204                        }
 205
 2206                        data[set.Key].LocalGroups = newMembers;
 2207                    }
 1208                }
 1209            }
 210
 2211            ret.AffectedComputers = affectedComputers.ToArray();
 212
 213            //At this point, we've resolved individual add/substract methods for each linked GPO.
 214            //Now we need to actually squish them together into the resulting set of changes
 36215            foreach (var kvp in data) {
 10216                var key = kvp.Key;
 10217                var val = kvp.Value;
 10218                var rm = val.RestrictedMember;
 10219                var rmo = val.RestrictedMemberOf;
 10220                var gm = val.LocalGroups;
 221
 10222                var final = new List<TypedPrincipal>();
 223
 224                // If we're setting RestrictedMembers, it overrides LocalGroups due to order of operations. Restricted M
 10225                final.AddRange(rmo);
 10226                final.AddRange(rm.Count > 0 ? rm : gm);
 227
 10228                var finalArr = final.Distinct().ToArray();
 229
 10230                switch (key) {
 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>
 5255        internal async IAsyncEnumerable<GroupAction> ProcessGPOTemplateFile(string basePath, string gpoDomain) {
 5256            var templatePath = Path.Combine(basePath, "MACHINE", "Microsoft", "Windows NT", "SecEdit", "GptTmpl.inf");
 257
 5258            if (!File.Exists(templatePath))
 1259                yield break;
 260
 261            FileStream fs;
 4262            try {
 4263                fs = new FileStream(templatePath, FileMode.Open, FileAccess.Read);
 4264            }
 0265            catch {
 0266                yield break;
 267            }
 268
 4269            using var reader = new StreamReader(fs);
 4270            var content = await reader.ReadToEndAsync();
 4271            var memberMatch = MemberRegex.Match(content);
 272
 4273            if (!memberMatch.Success)
 1274                yield break;
 275
 276            //We've got a match! Lets figure out whats going on
 3277            var memberText = memberMatch.Groups[1].Value.Trim();
 278            //Split our text into individual lines
 3279            var memberLines = Regex.Split(memberText, @"\r\n|\r|\n");
 280
 36281            foreach (var memberLine in memberLines) {
 282                //Check if the Key regex matches (S-1-5.*_memberof=blah)
 9283                var keyMatch = KeyRegex.Match(memberLine);
 284
 9285                if (!keyMatch.Success)
 0286                    continue;
 287
 9288                var key = keyMatch.Groups[1].Value.Trim();
 9289                var val = keyMatch.Groups[2].Value.Trim();
 290
 9291                var leftMatch = MemberLeftRegex.Match(key);
 9292                var rightMatches = MemberRightRegex.Matches(val);
 293
 294                //If leftmatch is a success, the members of a group are being explicitly set
 12295                if (leftMatch.Success) {
 3296                    var extracted = ExtractRid.Match(leftMatch.Value);
 3297                    var rid = int.Parse(extracted.Groups[1].Value);
 298
 3299                    if (Enum.IsDefined(typeof(LocalGroupRids), rid))
 300                        //Loop over the members in the match, and try to convert them to SIDs
 18301                        foreach (var member in val.Split(',')) {
 4302                            if (await GetSid(member.Trim('*'), gpoDomain) is (true, var res)) {
 1303                                yield return new GroupAction {
 1304                                    Target = GroupActionTarget.RestrictedMember,
 1305                                    Action = GroupActionOperation.Add,
 1306                                    TargetSid = res.ObjectIdentifier,
 1307                                    TargetType = res.ObjectType,
 1308                                    TargetRid = (LocalGroupRids)rid
 1309                                };
 1310                            }
 3311                        }
 3312                }
 313
 314                //If right match is a success, a group has been set as a member of one of our local groups
 9315                var index = key.IndexOf("MemberOf", StringComparison.CurrentCultureIgnoreCase);
 12316                if (rightMatches.Count > 0 && index > 0) {
 3317                    var account = key.Trim('*').Substring(0, index - 3).ToUpper();
 318
 4319                    if (await GetSid(account, gpoDomain) is (true, var res)) {
 6320                        foreach (var match in rightMatches) {
 1321                            var rid = int.Parse(ExtractRid.Match(match.ToString()).Groups[1].Value);
 1322                            if (!Enum.IsDefined(typeof(LocalGroupRids), rid)) continue;
 323
 1324                            var targetGroup = (LocalGroupRids)rid;
 1325                            yield return new GroupAction {
 1326                                Target = GroupActionTarget.RestrictedMemberOf,
 1327                                Action = GroupActionOperation.Add,
 1328                                TargetRid = targetGroup,
 1329                                TargetSid = res.ObjectIdentifier,
 1330                                TargetType = res.ObjectType
 1331                            };
 1332                        }
 1333                    }
 3334                }
 9335            }
 5336        }
 337
 338        /// <summary>
 339        ///     Resolves a SID to its type
 340        /// </summary>
 341        /// <param name="account"></param>
 342        /// <param name="domainName"></param>
 343        /// <returns></returns>
 20344        private async Task<(bool Success, TypedPrincipal Principal)> GetSid(string account, string domainName) {
 37345            if (!account.StartsWith("S-1-", StringComparison.CurrentCulture)) {
 346                string user;
 347                string domain;
 29348                if (account.Contains('\\')) {
 349                    //The account is in the format DOMAIN\\username
 12350                    var split = account.Split('\\');
 12351                    domain = split[0];
 12352                    user = split[1];
 12353                }
 5354                else {
 355                    //The account is just a username, so try with the current domain
 5356                    domain = domainName;
 5357                    user = account;
 5358                }
 359
 17360                user = user.ToUpper();
 361
 362                //Try to resolve as a user object first
 17363                var (success, res) = await _utils.ResolveAccountName(user, domain);
 17364                if (success)
 8365                    return (true, res);
 366
 9367                return await _utils.ResolveAccountName($"{user}$", domain);
 368            }
 369
 370            //The element is just a sid, so return it straight
 3371            return await _utils.ResolveIDAndType(account, domainName);
 20372        }
 373
 374        /// <summary>
 375        ///     Parses a GPO Groups.xml file and pulls group membership changes out
 376        /// </summary>
 377        /// <param name="basePath"></param>
 378        /// <param name="gpoDomain"></param>
 379        /// <returns>A list of GPO "Actions"</returns>
 4380        internal async IAsyncEnumerable<GroupAction> ProcessGPOXmlFile(string basePath, string gpoDomain) {
 4381            var xmlPath = Path.Combine(basePath, "MACHINE", "Preferences", "Groups", "Groups.xml");
 382
 383            //If the file doesn't exist, then just return
 4384            if (!File.Exists(xmlPath))
 1385                yield break;
 386
 387            //Create an XPathDocument to let us navigate the XML
 388            XPathDocument doc;
 3389            try {
 3390                doc = new XPathDocument(xmlPath);
 3391            }
 0392            catch (Exception e) {
 0393                _log.LogError(e, "error reading GPO XML file {File}", xmlPath);
 0394                yield break;
 395            }
 396
 3397            var navigator = doc.CreateNavigator();
 398            //Grab all the Groups nodes
 3399            var groupsNodes = navigator.Select("/Groups");
 400
 9401            while (groupsNodes.MoveNext()) {
 3402                var current = groupsNodes.Current;
 403                //If disable is set to 1, then this Group wont apply
 3404                if (current.GetAttribute("disabled", "") is "1")
 1405                    continue;
 406
 2407                var groupNodes = current.Select("Group");
 22408                while (groupNodes.MoveNext()) {
 409                    //Grab the properties for each Group node. Current path is /Groups/Group
 10410                    var groupProperties = groupNodes.Current.Select("Properties");
 30411                    while (groupProperties.MoveNext()) {
 10412                        var currentProperties = groupProperties.Current;
 10413                        var action = currentProperties.GetAttribute("action", "");
 414
 415                        //The only action that works for built in groups is Update.
 10416                        if (!action.Equals("u", StringComparison.OrdinalIgnoreCase))
 2417                            continue;
 418
 8419                        var groupSid = currentProperties.GetAttribute("groupSid", "")?.Trim();
 8420                        var groupName = currentProperties.GetAttribute("groupName", "")?.Trim();
 421
 422                        //Next is to determine what group is being updated.
 423
 8424                        var targetGroup = LocalGroupRids.None;
 12425                        if (!string.IsNullOrWhiteSpace(groupSid)) {
 426                            //Use a regex to match and attempt to extract the RID
 4427                            var s = ExtractRid.Match(groupSid);
 8428                            if (s.Success) {
 4429                                var rid = int.Parse(s.Groups[1].Value);
 4430                                if (Enum.IsDefined(typeof(LocalGroupRids), rid))
 4431                                    targetGroup = (LocalGroupRids)rid;
 4432                            }
 4433                        }
 434
 8435                        if (!string.IsNullOrWhiteSpace(groupName) && targetGroup == LocalGroupRids.None)
 4436                            ValidGroupNames.TryGetValue(groupName, out targetGroup);
 437
 438                        //If targetGroup is still None, we've failed to resolve a group target. No point in continuing
 8439                        if (targetGroup == LocalGroupRids.None)
 2440                            continue;
 441
 6442                        var deleteUsers = currentProperties.GetAttribute("deleteAllUsers", "") == "1";
 6443                        var deleteGroups = currentProperties.GetAttribute("deleteAllGroups", "") == "1";
 444
 6445                        if (deleteUsers)
 2446                            yield return new GroupAction {
 2447                                Action = GroupActionOperation.DeleteUsers,
 2448                                Target = GroupActionTarget.LocalGroup,
 2449                                TargetRid = targetGroup
 2450                            };
 451
 6452                        if (deleteGroups)
 2453                            yield return new GroupAction {
 2454                                Action = GroupActionOperation.DeleteGroups,
 2455                                Target = GroupActionTarget.LocalGroup,
 2456                                TargetRid = targetGroup
 2457                            };
 458
 459                        //Get all the actual members being added
 6460                        var members = currentProperties.Select("Members/Member");
 34461                        while (members.MoveNext()) {
 14462                            var memberAction = members.Current.GetAttribute("action", "")
 14463                                .Equals("ADD", StringComparison.OrdinalIgnoreCase)
 14464                                ? GroupActionOperation.Add
 14465                                : GroupActionOperation.Delete;
 466
 14467                            var memberName = members.Current.GetAttribute("name", "");
 14468                            var memberSid = members.Current.GetAttribute("sid", "");
 469
 14470                            var ga = new GroupAction {
 14471                                Action = memberAction
 14472                            };
 473
 474                            //If we have a memberSid, this is the best case scenario
 24475                            if (!string.IsNullOrWhiteSpace(memberSid)) {
 10476                                if (await _utils.ResolveIDAndType(memberSid, gpoDomain) is (true, var res)) {
 0477                                    ga.Target = GroupActionTarget.LocalGroup;
 0478                                    ga.TargetSid = memberSid;
 0479                                    ga.TargetType = res.ObjectType;
 0480                                    ga.TargetRid = targetGroup;
 481
 0482                                    yield return ga;
 0483                                }
 10484                            }
 485
 486                            //If we have a memberName, we need to resolve it to a SID/Type
 28487                            if (!string.IsNullOrWhiteSpace(memberName)) {
 21488                                if (await GetSid(memberName, gpoDomain) is (true, var res)) {
 7489                                    ga.Target = GroupActionTarget.LocalGroup;
 7490                                    ga.TargetSid = res.ObjectIdentifier;
 7491                                    ga.TargetType = res.ObjectType;
 7492                                    ga.TargetRid = targetGroup;
 7493                                    yield return ga;
 7494                                }
 14495                            }
 14496                        }
 6497                    }
 10498                }
 2499            }
 4500        }
 501
 502        /// <summary>
 503        ///     Represents an action from a GPO
 504        /// </summary>
 505        internal class GroupAction {
 26506            internal GroupActionOperation Action { get; set; }
 23507            internal GroupActionTarget Target { get; set; }
 16508            internal string TargetSid { get; set; }
 16509            internal Label TargetType { get; set; }
 19510            internal LocalGroupRids TargetRid { get; set; }
 511
 3512            public TypedPrincipal ToTypedPrincipal() {
 3513                return new TypedPrincipal {
 3514                    ObjectIdentifier = TargetSid,
 3515                    ObjectType = TargetType
 3516                };
 3517            }
 518
 1519            protected bool Equals(GroupAction other) {
 1520                return Action == other.Action && Target == other.Target && TargetSid == other.TargetSid && TargetType ==
 1521            }
 522
 1523            public override bool Equals(object obj) {
 1524                if (ReferenceEquals(null, obj)) return false;
 1525                if (ReferenceEquals(this, obj)) return true;
 1526                if (obj.GetType() != this.GetType()) return false;
 1527                return Equals((GroupAction)obj);
 1528            }
 529
 0530            public override int GetHashCode() {
 0531                unchecked {
 0532                    var hashCode = (int)Action;
 0533                    hashCode = (hashCode * 397) ^ (int)Target;
 0534                    hashCode = (hashCode * 397) ^ (TargetSid != null ? TargetSid.GetHashCode() : 0);
 0535                    hashCode = (hashCode * 397) ^ (int)TargetType;
 0536                    hashCode = (hashCode * 397) ^ (int)TargetRid;
 0537                    return hashCode;
 538                }
 0539            }
 540
 1541            public override string ToString() {
 1542                return
 1543                    $"{nameof(Action)}: {Action}, {nameof(Target)}: {Target}, {nameof(TargetSid)}: {TargetSid}, {nameof(
 1544            }
 545        }
 546
 547        /// <summary>
 548        ///     Storage for each different group type
 549        /// </summary>
 550        public class GroupResults {
 10551            public List<TypedPrincipal> LocalGroups = new();
 10552            public List<TypedPrincipal> RestrictedMember = new();
 10553            public List<TypedPrincipal> RestrictedMemberOf = new();
 554        }
 555
 556        internal enum GroupActionOperation {
 557            Add,
 558            Delete,
 559            DeleteUsers,
 560            DeleteGroups
 561        }
 562
 563        internal enum GroupActionTarget {
 564            RestrictedMemberOf,
 565            RestrictedMember,
 566            LocalGroup
 567        }
 568
 569        internal enum LocalGroupRids {
 570            None = 0,
 571            Administrators = 544,
 572            RemoteDesktopUsers = 555,
 573            DcomUsers = 562,
 574            PSRemote = 580
 575        }
 576    }
 577}