Using ConfigMgr Compliance to Manage Security Configuration Baselines (Part 2) | Quisitive
Using ConfigMgr Compliance to Manage Security Configuration Baselines (Part 2)
February 2, 2021
Quisitive
This is the second part of the blog series where we'll walk you through how we used ConfigMgr to manage Security Configuration Baselines.

n 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…

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…

(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…

(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.

[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
}

p

.\Export-GroupPolicyObjects.ps1 -Domain contoso.com -SearchGPO -SearchGPOName Workstation -Verbose

p

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
	}
}

If we run the above script with the following command-line

.\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”

(
	[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.