In part 1, we set the stage for the work we are about to do. We briefly went over the items that led up to our decisions. In the next parts, we’ll walk you through what we did. If you would like, you can go back and read Using ConfigMgr Compliance to Manage Security Configuration Baselines (Part 1) to get caught up.
Active Directory Group Policy
We need to get the settings that were already configured within the domain so that we can create the needed INF file templates for the non-registry policy settings.
To do this, let’s fire up an elevated PowerShell session and do the following:
If you know the name of the GPO you are looking for, you can simply export it to the desired location of your choice. Like this…
1 | Backup-GPO -Domain contoso.com -Name "Default Workstation Policy" -Path "C:\Temp\GPOExports\MyPolicy" |
If you don’t know the name of the policy you are looking for, you can get the names using the following…
1 | (Get-GPO -Domain contoso.com -All).DisplayName |
Or, if we only know part of the GPO name, we can search for all of those that have the portion of the name we remember in it. Example – to get all GPOs that contain the word ‘Default’ in the name…
1 | (Get-GPO -Domain contoso.com -All | ?{$_.DisplayName -like "*Default*"}).DisplayName |
But what if we want to have a choice of exporting ALL Group Policies, or just those with a specific word or term in their name? Well, we would script that. The script might look something like this (The script below is the same script we used for our customer. I’m just placing it here for others to use if they wish.) By the way, you can also copy the code below and save it as ‘Export-GroupPolicyObjects.ps1’. It can be used to backup GPOs in the future as well.
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | [CmdletBinding(DefaultParameterSetName = 'AllGPO')] param ( [Parameter(Mandatory = $true)] [string]$Domain, [Parameter(ParameterSetName = 'AllGPO', Mandatory = $false)] [switch]$AllGPO, [Parameter(ParameterSetName = 'SearchGPO', Mandatory = $false)] [switch]$SearchGPO, [Parameter(ParameterSetName = 'SearchGPO', Mandatory = $true)] [string]$SearchGPOName ) # Function to export GPOs from a domain Function Export-GPO ($GPOName, $TargetDomain) { # Define the GPO Export root $ExportPath = "$($env:SystemDrive)\Temp\GPOExports" Write-Verbose -Message "GPO Export root is '$($ExportPath)'" # Define the Export path for the GPO $GPOExportPath = Join-Path -Path $ExportPath -ChildPath $GPO.DisplayName Write-Verbose -Message "GPO Export destination path is '$($GPOExportPath)'" # Check that the export path exists, create it if it is not there $CheckPath = Test-Path -Path $GPOExportPath Write-Verbose -Message "GPO Export destination path exists: $($CheckPath)" If (-not ($CheckPath)) { Write-Verbose -Message "Creating path - '$($GPOExportPath)'" $CreatePath = New-Item -Path $ExportPath -Name $GPOName -ItemType directory -Force } # Export the GPO Write-Verbose -Message "Exporting GPO '$($GPOName)' to '$($GPOExportPath)'" $ExportGPO = Get-GPO -Domain $TargetDomain -Name "$($GPOName)" | Backup-GPO -Path "$($GPOExportPath)" } # If 'All' is specified get all of the GPOs, else get only the GPOs that have the search term in the name from '$SearchGPOName' If ($AllGPO) { # Get all of the GPOs in the domain Write-Verbose -Message "Getting all Group Policy Objects from the domain '$($Domain)'" $GPOs = Get-GPO -Domain $Domain -All Write-Verbose -Message "Found $($GPOs.count) Group Policy Objects in '$($Domain)'" } Else { # Get the GPOs that contain the value specified at the command-line for '$SearchGPOName' Write-Verbose -Message "Getting all Group Policy Objects with '$($SearchGPOName)' in the name from the domain '$($Domain)'" $GPOs = Get-GPO -Domain $Domain -All | ?{ $_.DisplayName -like "*$($SearchGPOName)*" } Write-Verbose -Message "Found $($GPOs.count) Group Policy Objects in '$($Domain)'" } # Process the list of GPOs Foreach ($GPO in $GPOs) { # Define the GPO Name for the function $GPOName = "$($GPO.DisplayName)" Write-Verbose -Message "Processing GPO '$($GPOName)'" # Call the export-gpo function Write-Verbose -Message "Calling function 'Export-GPO' for '$($GPOName)' in domain '$($Domain)'" $ProcessGPO = Export-GPO -GPOName $GPOName -TargetDomain $Domain } |
If we run the above script with the following command-line
1 | .\Export-GroupPolicyObjects.ps1 -Domain contoso.com -SearchGPO -SearchGPOName Workstation -Verbose |
We see this output…
OK, so now we have our GPO exported to “C:\Temp\GPOExports\Default Workstation Policy”. Let’s go take a look at the INF file. The file we need from the GPO is ‘GptTmpl.inf’. It should be located in “C:\Temp\GPOExports\<GPOName>\<GPOGuid>\DomainSysvol\GPO\Machine\Microsoft\Windows NT\SecEdit”.
First, let’s tackle the User Rights Assignments since they won’t be converted to ConfigMgr Configuration Items with the script.
Open the ‘GptTmpl.inf’ file with notepad to edit it. We need to remove all of the lines we don’t need.
You’ll notice that the INF is broken up into sections with each section header specified between the square brackets “[ ]”. There are three (3) sections we are interested in. They are:
- Unicode
- Version
- Privilege Rights
All of the other sections can be removed. Once this is done, you should have a file that looks similar to below.
Depending upon your organization’s security requirements, there may be more or less entries. Save the file as ‘UserRights.inf’ in the folder “C:\Temp\INF Files” so that we can create our individual custom INF files for use in our compliance remediation scripts.
Another way to create the custom INF files for our use is to leverage a script that will go through and generate them for us. The script code below will do just that. Just copy and save the code as “Generate-AllCustomINFFiles.ps1”
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | param ( [Parameter(Mandatory = $true)] [string]$GPOExportDir ) function parseInfFile { # Funtion code borrowed from https://www.dev4sys.com/2016/05/how-to-parse-ini-file-in-powershell.html and renamed for use with INF files [CmdletBinding()] param ( [Parameter(Position = 0)] [String]$Inputfile ) if ($Inputfile -eq "") { Write-Error "Inf File Parser: No file specified or selected to parse." Break } else { $ContentFile = Get-Content $Inputfile # commented Section $COMMENT_CHARACTERS = ";" # match section header $HEADER_REGEX = "\[+[A-Z0-9._ %<>/#+-]+\]" $OccurenceOfComment = 0 $ContentComment = $ContentFile | Where { ($_ -match "^\s*$COMMENT_CHARACTERS") -or ($_ -match "^$COMMENT_CHARACTERS") } | % { [PSCustomObject]@{ Comment = $_; Index = [Array]::IndexOf($ContentFile, $_) } $OccurenceOfComment++ } $COMMENT_INF = @() foreach ($COMMENT_ELEMENT in $ContentComment) { $COMMENT_OBJ = New-Object PSObject $COMMENT_OBJ | Add-Member -type NoteProperty -name Index -value $COMMENT_ELEMENT.Index $COMMENT_OBJ | Add-Member -type NoteProperty -name Comment -value $COMMENT_ELEMENT.Comment $COMMENT_INI += $COMMENT_OBJ } $CONTENT_USEFUL = $ContentFile | Where { ($_ -notmatch "^\s*$COMMENT_CHARACTERS") -or ($_ -notmatch "^$COMMENT_CHARACTERS") } $ALL_SECTION_HASHTABLE = $CONTENT_USEFUL | Where { $_ -match $HEADER_REGEX } | % { [PSCustomObject]@{ Section = $_; Index = [Array]::IndexOf($CONTENT_USEFUL, $_) } } #$ContentUncomment | Select-String -AllMatches $HEADER_REGEX | Select-Object -ExpandProperty Matches $SECTION_INF = @() foreach ($SECTION_ELEMENT in $ALL_SECTION_HASHTABLE) { $SECTION_OBJ = New-Object PSObject $SECTION_OBJ | Add-Member -type NoteProperty -name Index -value $SECTION_ELEMENT.Index $SECTION_OBJ | Add-Member -type NoteProperty -name Section -value $SECTION_ELEMENT.Section $SECTION_INF += $SECTION_OBJ } $INF_FILE_CONTENT = @() $NBR_OF_SECTION = $SECTION_INF.count $NBR_MAX_LINE = $CONTENT_USEFUL.count #********************************************* # select each lines and value of each section #********************************************* for ($i = 1; $i -le $NBR_OF_SECTION; $i++) { if ($i -ne $NBR_OF_SECTION) { if (($SECTION_INF[$i - 1].Index + 1) -eq ($SECTION_INF[$i].Index)) { $CONVERTED_OBJ = @() #There is nothing between the two section } else { $SECTION_STRING = $CONTENT_USEFUL | Select-Object -Index (($SECTION_INF[$i - 1].Index + 1) .. ($SECTION_INF[$i].Index - 1)) | Out-String $CONVERTED_OBJ = convertfrom-stringdata -stringdata $SECTION_STRING } } else { if (($SECTION_INF[$i - 1].Index + 1) -eq $NBR_MAX_LINE) { $CONVERTED_OBJ = @() #There is nothing between the two section } else { $SECTION_STRING = $CONTENT_USEFUL | Select-Object -Index (($SECTION_INF[$i - 1].Index + 1) .. ($NBR_MAX_LINE - 1)) | Out-String $CONVERTED_OBJ = convertfrom-stringdata -stringdata $SECTION_STRING } } $CURRENT_SECTION = New-Object PSObject $CURRENT_SECTION | Add-Member -Type NoteProperty -Name Section -Value $SECTION_INF[$i-1].Section $CURRENT_SECTION | Add-Member -Type NoteProperty -Name Content -Value $CONVERTED_OBJ $INF_FILE_CONTENT += $CURRENT_SECTION } return $INF_FILE_CONTENT } } # Function to write lines in the custom INF files Function INFWrite { Param ([String]$INFString) Add-Content $INFFile -Value $INFString } # Function to output the custom INF files Function Create-CustomINF ($INFPart, $GPOName) { $Section = $INFPart.Section Switch ($Section) { '[Registry Values]' { Write-Host " Processing INF section '$($Section)'..." $SectionName = $Section.Replace("[", "") $SectionName = $SectionName.Replace("]", "") $Contents = $INFPart.Content Foreach ($Key in $Contents.Keys) { $Setting = $Key $SettingValue = $Contents[$Key] $Names = $Key.Split('\') $Name = $Names[$Names.Count - 1] $INFFile = Join-Path -Path "$($CustomINFPath)\$($GPOShortName)" -ChildPath "$($SectionName)-$($Name).inf" INFWrite -INFString '[Unicode]' INFWrite -INFString 'Unicode=yes' INFWrite -INFString '[Version]' INFWrite -INFString 'signature="$CHICAGO$"' INFWrite -INFString 'Revision=1' INFWrite -INFString "$($Section)" INFWrite -INFString "$($Key) = $($SettingValue)" } } '[Service General Setting]' { Write-Host " Processing INF section '$($Section)'..." $Contents = $INFPart.Content $SectionName = $Section.Replace("[", "") $SectionName = $SectionName.Replace("]", "") Foreach ($Key in $Contents.Keys) { $Setting = $Key $SettingValue = $Contents[$Key] $INFFile = Join-Path -Path "$($CustomINFPath)\$($GPOShortName)" -ChildPath "$($SectionName)-$($Key).inf" INFWrite -INFString '[Unicode]' INFWrite -INFString 'Unicode=yes' INFWrite -INFString '[Version]' INFWrite -INFString 'signature="$CHICAGO$"' INFWrite -INFString 'Revision=1' INFWrite -INFString "$($Section)" INFWrite -INFString "$($Key) = $($SettingValue)" } } '[System Access]' { Write-Host " Processing INF section '$($Section)'..." $Contents = $INFPart.Content $SectionName = $Section.Replace("[", "") $SectionName = $SectionName.Replace("]", "") Foreach ($Key in $Contents.Keys) { $Setting = $Key $SettingValue = $Contents[$Key] $INFFile = Join-Path -Path "$($CustomINFPath)\$($GPOShortName)" -ChildPath "$($SectionName)-$($Key).inf" INFWrite -INFString '[Unicode]' INFWrite -INFString 'Unicode=yes' INFWrite -INFString '[Version]' INFWrite -INFString 'signature="$CHICAGO$"' INFWrite -INFString 'Revision=1' INFWrite -INFString "$($Section)" INFWrite -INFString "$($Key) = $($SettingValue)" } } '[Privilege Rights]' { Write-Host " Processing INF section '$($Section)'..." $Contents = $INFPart.Content $SectionName = $Section.Replace("[", "") $SectionName = $SectionName.Replace("]", "") Foreach ($Key in $Contents.Keys) { $Setting = $Key $SettingValue = $Contents[$Key] $INFFile = Join-Path -Path "$($CustomINFPath)\$($GPOShortName)" -ChildPath "$($SectionName)-$($Key).inf" INFWrite -INFString '[Unicode]' INFWrite -INFString 'Unicode=yes' INFWrite -INFString '[Version]' INFWrite -INFString 'signature="$CHICAGO$"' INFWrite -INFString 'Revision=1' INFWrite -INFString "$($Section)" INFWrite -INFString "$($Key) = $($SettingValue)" } } '[Registry Keys]' { Write-Host " Processing INF section '$($Section)'..." $Contents = $INFPart.Content $SectionName = $Section.Replace("[", "") $SectionName = $SectionName.Replace("]", "") Foreach ($Key in $Contents.Keys) { $Setting = $Key $SettingValue = $Contents[$Key] $INFFile = Join-Path -Path "$($CustomINFPath)\$($GPOShortName)" -ChildPath "$($SectionName)-$($Key).inf" INFWrite -INFString '[Unicode]' INFWrite -INFString 'Unicode=yes' INFWrite -INFString '[Version]' INFWrite -INFString 'signature="$CHICAGO$"' INFWrite -INFString 'Revision=1' INFWrite -INFString "$($Section)" INFWrite -INFString "$($Key) = $($SettingValue)" } } '[Kerberos Policy]' { Write-Host " Processing INF section '$($Section)'..." $Contents = $INFPart.Content $SectionName = $Section.Replace("[", "") $SectionName = $SectionName.Replace("]", "") Foreach ($Key in $Contents.Keys) { $Setting = $Key $SettingValue = $Contents[$Key] $INFFile = Join-Path -Path "$($CustomINFPath)\$($GPOShortName)" -ChildPath "$($SectionName)-$($Key).inf" INFWrite -INFString '[Unicode]' INFWrite -INFString 'Unicode=yes' INFWrite -INFString '[Version]' INFWrite -INFString 'signature="$CHICAGO$"' INFWrite -INFString 'Revision=1' INFWrite -INFString "$($Section)" INFWrite -INFString "$($Key) = $($SettingValue)" } } } } $CurrentPath = Split-Path -Parent $MyInvocation.MyCommand.Definition $CustomINFPath = Join-Path -Path $CurrentPath -ChildPath 'Custom-INF-Files' If (-not (Test-Path -Path $CustomINFPath)) { $CreateDir = New-Item -Path $CurrentPath -Name 'Custom-INF-Files' -ItemType directory } $GPOFiles = Get-ChildItem -Path "$($GPOExportDir)" -Filter "GptTmpl.inf" -Recurse Write-Host "$($GPOFiles.Count) files are available to process" -ForegroundColor Green Foreach ($GPOFile in $GPOFiles) { $PolName = ($GPOFile.FullName).Split('\') $GPOName = $PolName[$PolName.Count - 9] Write-Host " Processing INF file for GPO '$($GPOName)' if found..." -ForegroundColor Green $INFParts = parseInfFile -Inputfile $GPOFile.FullName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue $INFParts = $INFParts | Sort-Object Section -Descending Foreach ($INFPart in $INFParts) { $GPOShortName = $GPOName.Replace(" ", "_") If (-not (Test-Path -Path "$($CustomINFPath)\$($GPOShortName)")) { $CreateDir = New-Item -Path $CustomINFPath -Name "$($GPOShortName)" -ItemType directory } Create-CustomINF -INFPart $INFPart -GPOName $GPOName } } |
When you run the above script, the screen output should look something like this…
Now that we have this much ready to go, we can move on to generating our discovery scripts for the ConfigMgr Configuration Items.
Below are links to the other posts in this series.