Private/Set-GPOConfigSection.ps1
function Set-GPOConfigSection { <# .SYNOPSIS Configures a specific section and key in a GPT template (GptTmpl.inf) file with specified members. .DESCRIPTION This function creates a new key or updates an existing key within a specified section of a GPT template (GptTmpl.inf represented by [IniFileHandler.IniFile] class) file. It processes and merges existing values (if the key exists) with new members, ensuring correct resolution of SIDs and avoiding duplicates. A single Null-string is considered a valid value. The function properly handles cases where privilege right keys appear as values in the GPT template and avoids attempting to resolve them as SIDs. It also gracefully handles empty/null values and properly formats the output for the GPT template format. 1.- Check if the provided section (Parameter CurrentSection) exist on the [IniFileHandler.IniFile]$GptTmpl variable (Parameter GptTmpl) 2.- Section exists (GptTmpl does contains the section) 3.- Check if Key exist. If key does not exist, just create it and continue with step 4 A.- If key exist, get the values contained. Value can be a single $null string comma delimited string being each item a member represented by its SID and * prefix (for example, Administrators would be *S-1-5-32-544, Event Log Readers would be *S-1-5-32-573, Server Operators would be *S-1-5-32-549... full string value would be *S-1-5-32-544,*S-1-5-32-573,*S-1-5-32-549 ). E.- Get value as array and strip prefix "*", just having pure SID. F.- Iterate through all members, except if just 1 value and this is null. G.- Each member or iteration has to be resolved (first remove * prefix, otherwise will throw an error), either a Well-Known SID or a "normal" SID. Excluding WellKnownSids, have SID translated to an account to ensure that it continues to exist on ActiveDirectory. If the account is successfully translated, meaning it does exist in AD, and it can be added to the OK list with an * prefix. Skip duplicated. WellKnownSids can be added directly with * prefix. H.- If the account does not exist, it should be skipped and a warning should be displayed. 4.- Key did not exist, so no values exist either. Key was created earlier. A.- Get new members from Parameter Members (This parameter can accept $null and should be treated as a single null string) B.- Each member or iteration has to be resolved, either a Well-Known SID or a "normal" SID. Having SID translated to an account to ensure that it continues to exist on ActiveDirectory. If the account is successfully translated, meaning it does exist in AD, it can be added to the OK list with an * prefix. Skip duplicated. WellKnownSids can be added directly with * prefix. 5.- Convert the List to a comma-delimited string (except if nullString single instance). trim end comma, period or space. 6. Add key and value the $GptTmpl 7.- Return updated $GptTmpl .PARAMETER CurrentSection The section in the GPT template file to be configured (e.g., "Privilege Rights" or "Registry Values"). This section is assumed to exist. .PARAMETER CurrentKey The key within the given section (e.g., "SeAuditPrivilege" or "SeBatchLogonRight"). .PARAMETER Members An array of members to be added to the key. Can be null, which will be treated as a single null string. .PARAMETER GptTmpl The GPT template object representing the GptTmpl.inf file of type [IniFileHandler.IniFile]. .OUTPUTS [IniFileHandler.IniFile] .EXAMPLE Set-GPOConfigSection -CurrentSection "User Rights Assignment" ` -CurrentKey "SeDenyNetworkLogonRight" ` -Members @("TheUgly", "SG_AdAdmins") ` -GptTmpl $GptTmpl .EXAMPLE Set-GPOConfigSection -CurrentSection "Privilege Rights" ` -CurrentKey "SeAuditPrivilege" ` -Members @("TheGood", "SG_InfraAdmins") ` -GptTmpl $GptTmpl .NOTES Required modules/prerequisites: - ActiveDirectory - GroupPolicy - EguibarIT - EguibarIT.DelegationPS Used Functions: Name ║ Module/Namespace ════════════════════════════════════════════╬══════════════════════════════ Write-Verbose ║ Microsoft.PowerShell.Utility Write-Warning ║ Microsoft.PowerShell.Utility Write-Error ║ Microsoft.PowerShell.Utility Get-FunctionDisplay ║ EguibarIT.DelegationPS Convert-SidToName ║ EguibarIT.DelegationPS Get-AdObjectType ║ EguibarIT.DelegationPS IniFileHandler.IniFile ║ EguibarIT.DelegationPS .NOTES Version: 1.4 DateModified: 27/May/2025 LastModifiedBy: Vicente Rodriguez Eguibar vicente@eguibar.com Eguibar IT http://www.eguibarit.com #> [CmdletBinding( SupportsShouldProcess = $true, ConfirmImpact = 'Medium' )] [OutputType([IniFileHandler.IniFile])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'The section in the GPT template file to be configured (ex. [Privilege Rights] or [Registry Values]).', Position = 0)] [ValidateNotNullOrEmpty()] [string] $CurrentSection, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'The KEY within given section (ex. SeAuditPrivilege or SeBatchLogonRight).', Position = 1)] [ValidateNotNullOrEmpty()] [string] $CurrentKey, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'Member of given KEY. This value can be Empty or Null', Position = 2)] [AllowNull()] [AllowEmptyString()] [AllowEmptyCollection()] [System.Collections.Generic.List[object]] $Members, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'Object representing the INI file values.', Position = 3)] [ValidateNotNullOrEmpty()] [IniFileHandler.IniFile] $GptTmpl ) Begin { Set-StrictMode -Version Latest # Display function header if variables exist if ($null -ne $Variables -and $null -ne $Variables.HeaderDelegation) { $txt = ($Variables.HeaderDelegation -f (Get-Date).ToString('dd/MMM/yyyy'), $MyInvocation.Mycommand, (Get-FunctionDisplay -HashTable $PsBoundParameters -Verbose:$False) ) Write-Verbose -Message $txt } #end if ############################## # Module imports ############################## # Variables Definition $resolvedMembers = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) # Regular expression to validate SID format - Fixed to allow well-known SIDs $sidRegex = '^S-\d+-\d+(-\d+)*$' # Regular expression to detect privilege right keys that might appear as values $privilegeKeyRegex = '^Se[A-Za-z]+Privilege$|^Se[A-Za-z]+Right$' } #end Begin Process { try { # Ensure Members is properly initialized if ($null -eq $Members) { Write-Debug -Message 'Members is null, creating new empty list' $Members = [System.Collections.Generic.List[object]]::new() } elseif ($Members -is [hashtable]) { Write-Debug -Message 'Members is hashtable, converting to list' $tempList = [System.Collections.Generic.List[object]]::new() foreach ($key in $Members.Keys) { $tempList.Add($Members[$key]) } #end foreach $Members = $tempList } elseif ($Members -is [string] -or $Members -isnot [System.Collections.IEnumerable]) { # If Members is a single string or not enumerable, wrap in a list Write-Debug -Message 'Members is single string or not enumerable, creating wrapper list' $tmpList = [System.Collections.Generic.List[object]]::new() if ($null -ne $Members) { $tmpList.Add($Members) } #end if $Members = $tmpList } elseif ($Members -isnot [System.Collections.Generic.List[object]]) { # If Members is not a List, convert it to one Write-Debug -Message 'Members is not a List, converting to List' $tempList = [System.Collections.Generic.List[object]]::new() try { foreach ($item in $Members) { if ($null -ne $item) { $tempList.Add($item) } #end if } #end foreach } catch { Write-Debug -Message ('Error iterating Members collection: {0}' -f $_.Exception.Message) } #end try-catch $Members = $tempList } #end if-elseif-else # Check if Members is empty or has only empty strings $hasValidMembers = $false if ($Members.Count -gt 0) { foreach ($member in $Members) { if (-not [string]::IsNullOrWhiteSpace($member)) { $hasValidMembers = $true break } #end if } #end foreach } #end if # If no valid members, use empty list if (-not $hasValidMembers) { Write-Debug -Message 'No valid members found, using empty list' $Members = [System.Collections.Generic.List[object]]::new() } #end if # Ensure section exists (e.g., "Privilege Rights" or "Registry Values") if (-not $GptTmpl.SectionExists($CurrentSection)) { Write-Debug -Message ('Creating missing section: {0}' -f $CurrentSection) $GptTmpl.AddSection($CurrentSection) } #end if # Get existing Value from current Key from $GptTmpl. # Value can ONLY be Null, or String (*S-1-5-32-546,*S-1-5-21-1913705174-2885708358-485712852-2125) $currentValue = $GptTmpl.GetKeyValue($CurrentSection, $CurrentKey) Write-Debug -Message ('Current value for {0}.{1}: {2}' -f $CurrentSection, $CurrentKey, $(if ([string]::IsNullOrEmpty($currentValue)) { '<empty>' } else { $currentValue }) ) # Parse existing members. $existingMembers = [System.Collections.Generic.List[string]]::new() if (-not [string]::IsNullOrEmpty($currentValue)) { # Check if the value is the same as the key (e.g., "SeAuditPrivilege") if ($currentValue -eq $CurrentKey) { Write-Debug -Message 'Current value matches key name, treating as empty' } elseif ($currentValue -match $privilegeKeyRegex) { Write-Debug -Message ('Value matches privilege key pattern: {0}, treating as empty' -f $currentValue) } else { # Split by comma and add non-empty entries to existingMembers $valueItems = $currentValue.Split(',', [System.StringSplitOptions]::RemoveEmptyEntries) foreach ($item in $valueItems) { # remove leading asterix '*' $sid = $item.TrimStart('*') # add to existingMembers $existingMembers.Add($sid.Trim()) } #end foreach Write-Debug -Message ('Parsed {0} existing member(s) from value' -f $existingMembers.Count) } #end if-elseif-else } #end if # Process existing members. Verify if they are valid SIDs. # add to resolvedMembers HashSet foreach ($member in $existingMembers) { # Skip if member is empty if ([string]::IsNullOrWhiteSpace($member)) { continue } #end if # Remove leading asterisk if present $sid = $member.TrimStart('*') # Skip if member name matches a privilege key pattern if ($sid -match $privilegeKeyRegex) { Write-Debug -Message ('Skipping member that looks like a privilege key: {0}' -f $sid) continue } #end if # Validate SID format using regex pattern if it's not empty if (-not [string]::IsNullOrEmpty($sid) -and -not ($sid -match $sidRegex)) { Write-Warning -Message ('Value does not match SID format: {0}. Skipping.' -f $sid) continue } #end if # Try to resolve the SID to confirm it's valid try { $resolvedAccount = $null # Only try to resolve non-empty SIDs if (-not [string]::IsNullOrEmpty($sid)) { $resolvedAccount = Convert-SidToName -SID $sid -ErrorAction SilentlyContinue } #end if # If resolved successfully, add to resolvedMembers with asterisk prefix if ($resolvedAccount) { # Add with asterisk prefix for GptTmpl format [void]$resolvedMembers.Add('*' + $sid) Write-Debug -Message ('Resolved existing member: {0} to SID: {1}' -f $resolvedAccount, $sid) } else { Write-Warning -Message ('Could not resolve existing SID: {0}. It may not exist anymore.' -f $sid) } #end if } catch { Write-Warning -Message ('Failed to process existing member {0}: {1}' -f $sid, $_.Exception.Message) } #end try-catch } #end foreach # Process new members from parameter Members foreach ($member in $Members) { # Skip if member is empty if ([string]::IsNullOrWhiteSpace($member)) { continue } #end if try { # Get AD object information $adObject = Get-AdObjectType -Identity $member -ErrorAction SilentlyContinue if ($null -eq $adObject) { Write-Warning -Message ('Could not resolve member: {0}' -f $member) continue } #end if # Extract SID based on type $sid = $null # Handle different object types to extract SID if ($adObject -is [string] -and $adObject -match $sidRegex) { # Already a SID string $sid = $adObject Write-Debug -Message ('Member {0} is a SID string: {1}' -f $member, $sid) } elseif ($adObject -is [System.Security.Principal.SecurityIdentifier]) { # SecurityIdentifier object $sid = $adObject.Value Write-Debug -Message ('Member {0} is a SecurityIdentifier: {1}' -f $member, $sid) } elseif ($null -ne $adObject.SID -and $adObject.SID -is [System.Security.Principal.SecurityIdentifier]) { # AD object with SID property that is a SecurityIdentifier $sid = $adObject.SID.Value Write-Debug -Message ('Member {0} has ObjectSID: {1}' -f $member, $sid) } elseif ($null -ne $adObject.ObjectSID -and $adObject.ObjectSID -is [Microsoft.ActiveDirectory.Management.ADPropertyValueCollection]) { # Handle AD objects with ObjectSID as ADPropertyValueCollection if ($adObject.ObjectSID.Count -gt 0) { $sidValue = $adObject.ObjectSID[0] if ($sidValue -is [System.Security.Principal.SecurityIdentifier]) { $sid = $sidValue.Value } else { $sid = $sidValue.ToString() } #end if-else Write-Debug -Message ('Member {0} has ADPropertyValueCollection ObjectSID: {1}' -f $member, $sid) } else { Write-Warning -Message ('Member {0} has empty ObjectSID collection' -f $member) continue } #end if-else } elseif ($null -ne $adObject.ObjectSID) { # AD object with ObjectSID property that is a string $sid = $adObject.ObjectSID Write-Debug -Message ('Member {0} has string ObjectSID: {1}' -f $member, $sid) } elseif ($null -ne $adObject.PSObject -and $adObject.PSObject.Properties -and $adObject.PSObject.Properties['Value'] -and $adObject.PSObject.Properties['Value'].Value -match $sidRegex) { # Object with Value property that is a SID $sid = $adObject.Value Write-Debug -Message ('Member {0} has Value property with SID: {1}' -f $member, $sid) } else { Write-Warning -Message ('Could not extract SID from member: {0}' -f $member) continue } #end if-elseif-else # Validate extracted SID if (-not [string]::IsNullOrEmpty($sid) -and $sid -match $sidRegex) { # Add to collection (with * prefix for GptTmpl format) [void]$resolvedMembers.Add('*' + $sid) Write-Debug -Message ('Added member {0} with SID: {1}' -f $member, $sid) } else { Write-Warning -Message ('Invalid SID extracted for member {0}: {1}' -f $member, $sid) } #end if } catch { Write-Warning -Message ('Error processing member {0}: {1}' -f $member, $_.Exception.Message) } #end try-catch } #end foreach # Create the final value for GptTmpl.inf format $finalValue = [string]::Empty if ($resolvedMembers.Count -gt 0) { $finalValue = $resolvedMembers -join ',' } #end if # Update the GPO template if ($PSCmdlet.ShouldProcess("$CurrentSection -> $CurrentKey", 'Updating GptTmpl')) { $GptTmpl.SetKeyValue($CurrentSection, $CurrentKey, $finalValue) $memberDisplay = if ([string]::IsNullOrEmpty($finalValue)) { '<empty>' } else { $finalValue } Write-Verbose -Message ( 'GPO section updated: Section: {0} Key: {1} Members: {2}' -f $CurrentSection, $CurrentKey, $memberDisplay ) } #end if } catch { Write-Error -Message ('Error in Set-GPOConfigSection processing {0}: {1}' -f $CurrentKey, $_.Exception.Message) Write-Debug -Message ('Stack trace: {0}' -f $_.Exception.StackTrace) } #end try-catch } #end Process End { if ($null -ne $Variables -and $null -ne $Variables.FooterDelegation) { $txt = ($Variables.FooterDelegation -f $MyInvocation.InvocationName, 'configuration of GptTmpl object section (Private Function).') Write-Verbose -Message $txt } #end if return $GptTmpl } #end End } #end function Set-GPOConfigSection |