Using ConfigMgr Compliance to Manage Security Configuration Baselines (Part 3) | Quisitive

If you haven’t already done so, below are links to the previous posts in this series to give you a chance to go back and read them to get caught up.

And now… on to part 3!

Where we are so far…

In the previous post (part 2) we did the following:

Next, we need to generate two things that we will need for the ConfigMgr Configuration Items.

Generating the Scripts

We have some choices here, we can manually create each PowerShell script by hand for each setting we need to monitor and enforce compliance on, or we can be smart about it and leverage a single script with functions in it to write the scrips we need for us. Since I dislike having to type so much, we opted for the latter.

For our script, it needs to do the following actions:

  1. Get all of the INF files we generated
  2. Process each one
  3. Output a PowerShell script for setting discovery
  4. Output a PowerShell script for setting remediation

If we ran the script from part 2, all of our INF files should reside in a directory structure in the same path as the script. This makes it very easy to get all of our INF files. We can simply add these three lines at the top of our script.

$CurrentPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$CustomINFPath = Join-Path -Path $CurrentPath -ChildPath 'Custom-INF-Files'
$CustomINFFiles = Get-ChildItem -Path $CustomINFPath -Filter "*.inf" -Recurse

This will get all of the files with the INF file extension under the directory ‘Custom-INF-Files’ and store them in the variable ‘$CustomINFFiles’.

Next, we need to process all of the custom INF files which means we will need a ‘Foreach’ loop… like this.

reach loop examplePowerShell
Foreach ($CustomINFFile in $CustomINFFiles){
    # Code to perform processing goes here
}

So far we have the INF files, and the ‘Foreach’ loop, now we need to add functionality that will actually do what we need. The easiest way would be to leverage functions (or snippets of code already produced and easily located on the internet. Other ways might be to see if people we know have already done something close to what we want to do. Thankfully, we didn’t need to reinvent the wheel in this area.

Since we can’t do file copies from within Configuration Items, we needed a way to copy the INF file for the setting we are checking. To do this we read the INF file into a base64 encoded string and write the string back to a file like this.


# Read the file to base64
$gptTmpl = "<FolderName\<FileName>.inf"
$b = [System.Text.Encoding]::UTF8.GetBytes($gptTmpl)
$b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($gptTmpl))
 
# write the file from base64
$newSettingsFile = "<FolderName\<FileName>.ini"
$b64 = "<EncodedString>"
$b = [System.Convert]::FromBase64String($b64)
$gptTmplString = [System.Text.Encoding]::UTF8.GetString($b)
$gptTmplString | Out-File $newSettingsFile -Force

With a little help from DR Scripto’s Microsoft Dev Blog post ‘Use PowerShell to Work with Any INI File‘ we get the following function.

function Get-IniContent ($filePath)
{
    $ini = @{}
    switch -regex -file $FilePath
    {
        “^\[(.+)\]” # Section
        {
            $section = $matches[1]
            $ini[$section] = @{}
            $CommentCount = 0
        }
        “^(;.*)$” # Comment
        {
            $value = $matches[1]
            $CommentCount = $CommentCount + 1
            $name = “Comment” + $CommentCount
            $ini[$section][$name] = $value
        }
        “(.+?)\s*=(.*)” # Key
        {
            $name,$value = $matches[1..2]
            $ini[$section][$name] = $value
        }
    }
    return $ini
}

With some assistance from Nathan Ziehnert (Twitter: @theznerd), we were able to modify the function from Dr Scripto to create the code needed for our discovery scripts. Sample below…

function Get-IniContent ($filePath)
{
    ## Borrowed from: https://devblogs.microsoft.com/scripting/use-powershell-to-work-with-any-ini-file/
    $ini = @{}
    switch -regex -file $filePath
    {
        '^\[(.+)\]' # Section
        {
            $section = $matches[1]
            $ini[$section] = @{}
            $CommentCount = 0
        }
        '^(;.*)$' # Comment
        {
            $value = $matches[1]
            $CommentCount = $CommentCount + 1
            $name = 'Comment' + $CommentCount
            $ini[$section][$name] = $value
        }
        '(.+?)\s*=(.*)' # Key
        {
            $name,$value = $matches[1..2]
            $ini[$section][$name] = $value
        }
        '^".*$' # Service Key
        {
            $name = $matches[0].Split(',')[0].Replace('"','')
            $value = $matches[0]
            $ini[$section][$name] = $value
        }
    }
    return $ini
}
$b64 = "<base64string>"
$newSettingsFile = "$ENV:Temp\NewSet.ini"
$existingSettingsFile = "$ENV:Temp\ExistingSet.ini"
$b = [System.Convert]::FromBase64String($b64)
$gptTmplString = [System.Text.Encoding]::UTF8.GetString($b)
$gptTmplString | Out-File $newSettingsFile -Force
secedit /export /cfg $existingSettingsFile /quiet
$existingSettings = Get-IniContent -filePath $existingSettingsFile
$newSettings = Get-IniContent -filePath $newSettingsFile
$matches = $true
foreach($key in $newSettings.Keys | Where-Object {$_ -ne "Unicode" -and $_ -ne "Version"})
{
    foreach($subKey in $newSettings[$key].Keys)
    {
        try
        {
            if($existingSettings[$key][$subKey] -and $newSettings[$key][$subKey] -ne $existingSettings[$key][$subKey])
            {
                if((@(Compare-Object ($newSettings[$key][$subKey].Split(",").Replace(" ","") | Sort-Object) ($existingSettings[$key][$subKey].Split(",").Replace(" ","") | Sort-Object)).Length -eq 0) -and $key -eq 'Privilege Rights')
                {}else{$matches = $false}
            }
        }
        catch
        {
            $matches = $false
        }
    }
}
[void](Remove-Item -Path $newSettingsFile -Force)
return $matches

Now we need to perform the remediation. Since we’ve chosen to use ‘secedit.exe’ that is what our PowerShell script will run to import the setting that we’ve determined to be “Compliant” with our organization’s security policy. We’ll first write the file from base64 and then run ‘secedit.exe’ to import it into the ‘secedit.sdb’ (local security policy database) on the target system.

$newSettingsFile = "$ENV:Temp\NewSet.ini"
$b64 = "<base64string>"
$b = [System.Convert]::FromBase64String($b64)
$gptTmplString = [System.Text.Encoding]::UTF8.GetString($b)
$gptTmplString | Out-File $newSettingsFile -Force
secedit /configure /db secedit.sdb /cfg $newSettingsFile /quiet
[void](Remove-Item -Path $newSettingsFile -Force)

Now we just need to pull the function together while being careful to escape special characters where needed so that our discovery and remediation scripts will be written and function properly. (In the interest of maintaining focus, I’m not going to get into the explanation of escaping special characters in PowerShell at this time.) Our final function for generation of our discover and remediation scripts looks like this. (Use the toolbar above the snippet to expand or copy the code)

function New-SecurityPolicyScripts ($gptTmpl)
{
    # Initial Settings to Compare and/or Remediate
    $b  = [System.Text.Encoding]::UTF8.GetBytes($gptTmpl)
    $b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($gptTmpl))
 
    # Create a detection script for the security policy
    # The script exports the current security configuration to a temporary file,
    # exports the expected security configuration to a temporary file, and then
    # compares the two for deviation from the expected security configuration.
    $powershellDetectionScript = @"
        function Get-IniContent (`$filePath)
        {
            ## Borrowed from: https://devblogs.microsoft.com/scripting/use-powershell-to-work-with-any-ini-file/
            `$ini = @{}
            switch -regex -file `$filePath
            {
                '^\[(.+)\]' # Section
                {
                    `$section = `$matches[1]
                    `$ini[`$section] = @{}
                    `$CommentCount = 0
                }
                '^(;.*)`$' # Comment
                {
                    `$value = `$matches[1]
                    `$CommentCount = `$CommentCount + 1
                    `$name = 'Comment' + `$CommentCount
                    `$ini[`$section][`$name] = `$value
                }
                '(.+?)\s*=(.*)' # Key
                {
                    `$name,`$value = `$matches[1..2]
                    `$ini[`$section][`$name] = `$value
                }
                '^".*`$' # Service Key
                {
                    `$name = `$matches[0].Split(',')[0].Replace('"','')
                    `$value = `$matches[0]
                    `$ini[`$section][`$name] = `$value
                }
            }
            return `$ini
        }
        `$b64 = "$b64"
        `$newSettingsFile = "`$ENV:Temp\NewSet.ini"
        `$existingSettingsFile = "`$ENV:Temp\ExistingSet.ini"
        `$b = [System.Convert]::FromBase64String(`$b64)
        `$gptTmplString = [System.Text.Encoding]::UTF8.GetString(`$b)
        `$gptTmplString | Out-File `$newSettingsFile -Force
        secedit /export /cfg `$existingSettingsFile /quiet
        `$existingSettings = Get-IniContent -filePath `$existingSettingsFile
        `$newSettings = Get-IniContent -filePath `$newSettingsFile
        `$matches = `$true
        foreach(`$key in `$newSettings.Keys | Where-Object {`$_ -ne "Unicode" -and `$_ -ne "Version"})
        {
            foreach(`$subKey in `$newSettings[`$key].Keys)
            {
                try
                {
                    if(`$existingSettings[`$key][`$subKey] -and `$newSettings[`$key][`$subKey] -ne `$existingSettings[`$key][`$subKey])
                    {
                        if((@(Compare-Object (`$newSettings[`$key][`$subKey].Split(",").Replace(" ","") | Sort-Object) (`$existingSettings[`$key][`$subKey].Split(",").Replace(" ","") | Sort-Object)).Length -eq 0) -and `$key -eq 'Privilege Rights')
                        {}else{`$matches = `$false}
                    }
                }
                catch
                {
                    `$matches = `$false
                }
            }
        }
        [void](Remove-Item -Path `$newSettingsFile -Force)
        return `$matches
"@
 
    # Create a remediation script for the security policy
    # The script exports the security settings to a temporary file and then
    # imports the security settings into the workstation.
    $powershellRemediationScript = @"
        `$newSettingsFile = "`$ENV:Temp\NewSet.ini"
        `$b64 = "$b64"
        `$b = [System.Convert]::FromBase64String(`$b64)
        `$gptTmplString = [System.Text.Encoding]::UTF8.GetString(`$b)
        `$gptTmplString | Out-File `$newSettingsFile -Force
        secedit /configure /db secedit.sdb /cfg `$newSettingsFile /quiet
        [void](Remove-Item -Path `$newSettingsFile -Force)
"@
    return ($powershellDetectionScript,$powershellRemediationScript)
}

We needed to actually output the generated script files now, so I just used a 3 line function that can be used to write text into any file that accepts text or strings as content.

Function PS1-Write ($PS1File,$PS1String){
    Add-Content $PS1File -Value $PS1String
}

Putting it together

All we need to do now is take our code snippets, add them to the Foreach loop and add a few things to make it all work for us, then save the file as ‘Generate-CMConfigItemScripts.ps1’

(Use the toolbar above the snippet to expand or copy the code)

# Take a security policy (GptTmpl.inf) file and convert it to a detection script
# and remediation script. The GptTmpl.inf is converted to base64 so that it can
# be stored as part of the configuration item.
function New-SecurityPolicyScripts ($gptTmpl)
{
    # Initial Settings to Compare and/or Remediate
    $b  = [System.Text.Encoding]::UTF8.GetBytes($gptTmpl)
    $b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($gptTmpl))
 
    # Create a detection script for the security policy
    # The script exports the current security configuration to a temporary file,
    # exports the expected security configuration to a temporary file, and then
    # compares the two for deviation from the expected security configuration.
    $powershellDetectionScript = @"
        function Get-IniContent (`$filePath)
        {
            ## Borrowed from: https://devblogs.microsoft.com/scripting/use-powershell-to-work-with-any-ini-file/
            `$ini = @{}
            switch -regex -file `$filePath
            {
                '^\[(.+)\]' # Section
                {
                    `$section = `$matches[1]
                    `$ini[`$section] = @{}
                    `$CommentCount = 0
                }
                '^(;.*)`$' # Comment
                {
                    `$value = `$matches[1]
                    `$CommentCount = `$CommentCount + 1
                    `$name = 'Comment' + `$CommentCount
                    `$ini[`$section][`$name] = `$value
                }
                '(.+?)\s*=(.*)' # Key
                {
                    `$name,`$value = `$matches[1..2]
                    `$ini[`$section][`$name] = `$value
                }
                '^".*`$' # Service Key
                {
                    `$name = `$matches[0].Split(',')[0].Replace('"','')
                    `$value = `$matches[0]
                    `$ini[`$section][`$name] = `$value
                }
            }
            return `$ini
        }
        `$b64 = "$b64"
        `$newSettingsFile = "`$ENV:Temp\NewSet.ini"
        `$existingSettingsFile = "`$ENV:Temp\ExistingSet.ini"
        `$b = [System.Convert]::FromBase64String(`$b64)
        `$gptTmplString = [System.Text.Encoding]::UTF8.GetString(`$b)
        `$gptTmplString | Out-File `$newSettingsFile -Force
        secedit /export /cfg `$existingSettingsFile /quiet
        `$existingSettings = Get-IniContent -filePath `$existingSettingsFile
        `$newSettings = Get-IniContent -filePath `$newSettingsFile
        `$matches = `$true
        foreach(`$key in `$newSettings.Keys | Where-Object {`$_ -ne "Unicode" -and `$_ -ne "Version"})
        {
            foreach(`$subKey in `$newSettings[`$key].Keys)
            {
                try
                {
                    if(`$existingSettings[`$key][`$subKey] -and `$newSettings[`$key][`$subKey] -ne `$existingSettings[`$key][`$subKey])
                    {
                        if((@(Compare-Object (`$newSettings[`$key][`$subKey].Split(",").Replace(" ","") | Sort-Object) (`$existingSettings[`$key][`$subKey].Split(",").Replace(" ","") | Sort-Object)).Length -eq 0) -and `$key -eq 'Privilege Rights')
                        {}else{`$matches = `$false}
                    }
                }
                catch
                {
                    `$matches = `$false
                }
            }
        }
        [void](Remove-Item -Path `$newSettingsFile -Force)
        return `$matches
"@
 
    # Create a remediation script for the security policy
    # The script exports the security settings to a temporary file and then
    # imports the security settings into the workstation.
    $powershellRemediationScript = @"
        `$newSettingsFile = "`$ENV:Temp\NewSet.ini"
        `$b64 = "$b64"
        `$b = [System.Convert]::FromBase64String(`$b64)
        `$gptTmplString = [System.Text.Encoding]::UTF8.GetString(`$b)
        `$gptTmplString | Out-File `$newSettingsFile -Force
        secedit /configure /db secedit.sdb /cfg `$newSettingsFile /quiet
        [void](Remove-Item -Path `$newSettingsFile -Force)
"@
    return ($powershellDetectionScript,$powershellRemediationScript)
}
 
Function PS1-Write ($PS1File,$PS1String){
    Add-Content $PS1File -Value $PS1String
}
 
$CurrentPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$CustomINFPath = Join-Path -Path $CurrentPath -ChildPath 'Custom-INF-Files'
 
$CustomINFFiles = Get-ChildItem -Path $CustomINFPath -Filter "*.inf" -Recurse # | Select -First 1
#$CustomINFFiles.Count
Foreach ($CustomINFFile in $CustomINFFiles){
    $DiscoveryPS1 = @()
    $RemediationPS1 = @()
    $INFFilePath = $CustomINFFile.FullName
    $INFDir = $INFFilePath.Replace("\$($CustomINFFile.Name)","")
    $ScriptsDir = Join-Path -Path $INFDir -ChildPath 'Scripts'
    If (-not(Test-Path -Path $ScriptsDir)){
        New-Item -Path $INFDir -Name 'Scripts' -ItemType directory -Force
    }
    $BaseScriptName = $CustomINFFile.Name
    $BaseScriptName = $BaseScriptName.Replace('.inf','.ps1')
    $DiscoveryScriptPath = Join-Path -Path $ScriptsDir -ChildPath "Discovery-$($BaseScriptName)"
    $RemediationScriptPath = Join-Path -Path $ScriptsDir -ChildPath "Remediation-$($BaseScriptName)"
    If (Test-Path -Path $DiscoveryScriptPath){
        Remove-Item -Path $DiscoveryScriptPath -Force
    }
        If (Test-Path -Path $RemediationScriptPath){
        Remove-Item -Path $RemediationScriptPath -Force
    }
    $CIScripts = New-SecurityPolicyScripts -gptTmpl "$($INFFilePath)"
    $DiscoveryPS1 = $CIScripts[0]
    $RemediationPS1 = $CIScripts[1]
    PS1-Write -PS1File $DiscoveryScriptPath -PS1String "$($DiscoveryPS1)"
    PS1-Write -PS1File $RemediationScriptPath -PS1String "$($RemediationPS1)"    
}

After running the script we should now have all of the pieces needed to create our ConfigMgr Configuration Items and the settings within them.

Generate Scripts

In the next and final part, we’ll go through creating the Configuration Items and the Configuration Baselines with remediation enabled…with PowerShell.

Have a great week! See you soon!

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…

Output example from Export-GroupPolicyObjects.ps1 script

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:

All of the other sections can be removed. Once this is done, you should have a file that looks similar to below.

GptTmpl.inf Example 1

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…

Generate-AllCustomINFFiles.ps1 script output screen

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.

You want me to do what?

The alarm clock is screaming at me to get the heck out of bed. I snooze it twice, then finally get up. Once I’ve dressed, I cruise to the kitchen for some much-needed coffee because I stayed up waaay too late the night before. After coffee, I walk to my home office where my laptop awaits, along with a likely plethora of emails to respond to before continuing work on a client’s project. It seems like just another day, nothing new… on cruise control.

As soon as I sit down, I check Teams to see what’s going on and if anybody needs any immediate assistance with some technical issue they are experiencing. Wouldn’t you know it, my manager is pinging me to discuss an upcoming project for one of our clients.

Manager: “Hey! We have a project slated to start next week, and I think it’s right up your alley!”

I’ve heard this before

Me: “Awesome! What needs to be done?”

Manager: “Our customer needs to have some ConfigMgr work done around Compliance Settings.”

Compliance Settings in ConfigMgr? This is going to be another quick and easy engagementnothing too difficult or strange to worry about

Me: “Great! I’m in.”

famous last words

There was much more conversation than that, but I will spare you the details and just cut to the kick-off call with the customer…

Customer: “We have a security audit approaching, and we need to apply security settings to our servers and workstations. As well as be able to document and convey our strategy moving forward according to our security specifications.”

Me: “No problem, what exactly are we looking at doing? Don’t you already have Active Directory GPO to set these?”

Customer: “We do… but moving forward as an organization we are moving away from GPO for many of the ‘STIG’ settings and want to leverage ConfigMgr to accomplish our goals.”

Me: “STIG settings?”

Customer: “Yup.”

I’ve not had to do much with STIG settings in the past because these were always handled by the various security teams. Surely I was selected for this project by mistake, right? But I like to think that I’m an intelligent guy, I can figure this out..let’s roll with it

Me: “OK, do you have the settings rules identified and your organizational settings documented?”

Customer: “We do, in total there are close to 3 to 5 HUNDRED security settings that need to be checked, monitored, and remediated when needed on an ongoing basis for Windows Server 2016 alone. We need the same done for Windows Server 2008, 2012, 2012 R2, 2016, 2019, and Windows 10.”

Did he just say “3 to 5 HUNDRED security settings for Windows Server 2016 alone”??… holy cow that’s a lot

Me (as if it didn’t even phase me): “No problem, when does it need to be done?”

Customer: “We’d like to have this done yesterday or as soon as possible, because the Federal audit begins in about a month.”

HmmmThat’s 3 to 5 hundred settings for each of 6 operating systemsThat means a potential of 3,000 settings to check, remediate, and report on in a month for a Federal auditWhat have I gotten myself into here?

Me: “I’m not sure that we can get up to 3,000 settings done in under 4 weeks, but we’re going to try. If we don’t make it by the time the audit starts, we definitely will have a documented strategy in place that we can communicate.”

Customer: “Excellent! That will work. Is Monday good for you as a start day?”

Me: “Works for me!”

The whole time we were on the call, I was searching the internet for everything that I could find about applying STIG settings with ConfigMgr Compliance. Let me just say this…there is a serious lack of documentation around this…

I made the commitment, I better deliver. Time to get to work.

What are some of the ways that I can do this?

Below are simply a few of the ways that we can get this job done. These are in no way intended to be the final word on the subject.

Active Directory Group Policy

We can also create a backup of the policies and then grab the settings we need from the ‘GptTmpl.inf’ file. With the settings we need, we can create INF files to be used to effect the settings desired using ‘secedit.exe’.

During my searches, I found the Microsoft Security Compliance Toolkit. The description of the toolkit from Microsoft says that it is a set of tools that allow administrators to “download, analyze, test, edit and store Microsoft-recommended security configuration baselines for Windows and other Microsoft products, while comparing them against other security configurations.”

After having downloaded the toolkit, I found that it is preconfigured Group Policy Objects that can be imported into the domain. Once imported, we are able to modify the policies to match what internal security teams have determined to be the best for their organizations. This is all well and good, except that many of these settings may not be tested in an environment and carry with them the potential to cause widespread issues upon deployment if they aren’t vetted first. Remember, each environment is different. Different applications, jurisdictions, requirements, work styles, people, and cultures. Also these preconfigured policies may have far more settings configured than what is needed by a particular organization.

Further, if an organization already has a large number of policy objects and filters within the domain that are deployed . The aggregation of these policies and filters can conflict (if they are not planned and modeled properly), and cause performance issues in applying the policies. This can result in policy processing timeouts that are seen by incomplete application of the settings, or policies not being applied at all; as reflected by the errors reported in client event logs.

Tattoo the Local Registry

The term is exactly as it sounds. It means to manual create or import registry settings that remain static on a destination system.

Registry settings will work for a great number of things except policies like renaming guest accounts, renaming administrator accounts, password complexity, and user rights assignments to name a few. If your organization decides that a change or modification is needed, all that needs to be done is to push out the settings with a deployment tool or script that applies the changes through PowerShell remoting, or WinRM. However, if the setting is determined to need removal, we might run into some difficulties if we don’t have a toolset that allows us to perform this action in bulk. The recommendation is to avoid registry tattooing; if it cannot be avoided, only tattoo settings that we know will NEVER change after they are initially set.

Manually Edit the Local Security Policy

Using the command ‘secpol.msc /s’ at the command prompt (elevated), will open up the “Local Security Policy” editor. From here we can go through each of the desired settings and make the changes necessary to effect the security posture as defined by our organization. Of note here is the ability to set a configuration as needed, then export that configuration from the command-line using ‘secedit.exe’. Once we have the configuration exported, we can then import that same configuration onto other systems using different command-line options for ‘secedit.exe’.

This is great if we only have a few dozen systems to manage. When we begin talking about hundreds or even thousands of machines, we need to be looking for a better way (or combining this with other methods).

Use PowerShell Desired State Configuration (DSC)

In Microsoft Docs, there is an article called “Quickstart: Convert Policy into DSC” that describes how to convert Group Policy backups into DSC. This works great if we are converting the canned policies from the Microsoft Security Compliance Toolkit. Mileage may vary when we try to use it on a GPO backup from and existing policy within our domain. The command ‘ConvertFrom-GPO’ within the ‘BaselineManagement’ PowerShell module doesn’t seem to handle the quotes and apostrophes within the legal banner and header text in the GPO correctly. As of January 2021, this has been an issue for almost three years with no movement toward getting it fixed. To get around the issue, you’ll need to modify the ‘GptTmpl.inf’ file within the GPO backup or remove the settings from a copy of the GPO prior to making the backup.

Once we are able to successfully run the ‘ConvertFrom-GPO’ command, a MOF file is created with all of the settings contained in the GPO. This MOF file can then be used to apply our DSC configuration to our target systems.

Please note that to successfully apply the configuration, two additional modules will be needed on the target systems:

Of course, this method is not without its potential issues. If your environment leverages FIPS policies as part of the security configuration, the PowerShell modules needed will not be able to be installed onto target systems. We’ll need to figure out a way to download and install those modules offline in order to have success.

OK, so what now? What’s my path?

Pick a Pony and Ride That Thing!

Ultimately, we chose to do a combination of the following:

We decided to do the work this way because:

With the decision made, we needed to get moving. Time was not our friend.

Continue the series…