# Convert MS security baselines to Azure ARC Guest Configuration packages

In this post, I will show you how to easily convert official [Microsoft Windows Security Baselines](https://techcommunity.microsoft.com/blog/microsoft-security-baselines/windows-server-2025-security-baseline/4358733) into Azure ARC Guest Configuration (DSC) packages. So you can easily deploy them to your ARC-managed clients or any DSC-managed client.

In general, you can use this guideline to convert any GPO to DSC!

---

1. Start with downloading [Microsoft Security Compliance Toolkit 1.0](https://www.microsoft.com/en-us/download/details.aspx?id=55319) ZIP file which contains the mentioned baseline files.
    
2. Extract the downloaded ZIP to the `C:\ExtractedBaselines` folder
    
3. Install required PowerShell modules: 'GPRegistryPolicyParser', 'BaselineManagement', 'GPRegistryPolicyDsc', 'SecurityPolicyDsc', 'AuditPolicyDsc', 'PSDesiredStateConfiguration', ‘GuestConfiguration’
    
    1. Use **PowerShell Core**!
        
    2. ```powershell
        # download & import essential modules
        'GPRegistryPolicyParser', 'BaselineManagement', 'GPRegistryPolicyDsc', 'SecurityPolicyDsc', 'AuditPolicyDsc', 'PSDesiredStateConfiguration', 'GuestConfiguration' | % {
            Install-Module $_
        }
        # make sure essential modules are in some of the path mentioned in the $env:PSModulePath!
        ```
        
4. Extract and organize baseline GPOs for better visibility
    
    1. ```powershell
        # organize GPOs for better visibility
        $extractedBaseline = "C:\ExtractedBaselines"
        $gpoBaselineDirectory = "C:\BaselineGpos"
        
        Remove-Item $gpoBaselineDirectory -Recurse -Force -ErrorAction SilentlyContinue
        [Void][System.IO.Directory]::CreateDirectory($gpoBaselineDirectory)
        
        foreach ($gpo in (Get-ChildItem "$extractedBaseline\GPOs" -Directory)) {
            $gpoName = ([regex]"<GPODisplayName><!\[CDATA\[(.+)\]\]></GPODisplayName>").Matches((gc "$($gpo.fullname)\bkupInfo.xml" -Raw)).captures.groups[1].value
            
            New-Item -Path $gpoBaselineDirectory -Name $gpoName -ItemType Directory -Force
            Copy-Item $gpo.fullname "$gpoBaselineDirectory\$gpoName" -Recurse -Force
        }
        ```
        
5. Create DSC configurations (`mof` and `ps1`) from the baseline GPOs
    
    1. ```powershell
        # create DSC configuration from the baseline (mof and ps1)
        $dscDirectory = "C:\ConvertedBaselineGpos"
        
        Remove-Item $dscDirectory -Recurse -Force -ErrorAction SilentlyContinue
        [Void][System.IO.Directory]::CreateDirectory($gpoBaselineDirectory)
        
        Get-ChildItem $gpoBaselineDirectory | % {
            $confName = $_.Name -replace "-", "_" -replace "\s*"
            $confName
        
            # !BEWARE! creating of some localhost.mof can (probably will) end with an error https://github.com/microsoft/BaselineManagement?tab=readme-ov-file#known-gaps-in-capability
            # problematic ps1 parts have to be commented otherwise you will not be able to create DSC from it!
            try {
                $ErrorActionPreference = 'stop'
                $convertedGpo = ConvertFrom-GPO -Path $_.FullName -OutputConfigurationScript -OutputPath $dscDirectory -ConfigName $confName
            } catch {
                if ($_ -like "Invalid MOF definition*") {
                    Write-Warning "In '$($convertedGpo.ConfigurationScript)' comment setting that contains property mentioned in this error '$_'.`n`nOtherwise you will not be able to generate guest configuration from it!"
                } else {
                    Write-Error $_
                }
            }
            $ErrorActionPreference = 'continue'
        
            # disable mof compilation part (last line) I will compile it by myself later
            $modifiedContent = Get-Content $convertedGpo.ConfigurationScript | ? { $_ -notlike "$confName -OutputPath *" }
            $modifiedContent -join "`n" | Set-Content $convertedGpo.ConfigurationScript -Force
        }
        ```
        
    2. For converting of GPOs to DSC we are using the [BaselineManagement](https://github.com/microsoft/BaselineManagement) module. But because DSC has some [limitations](https://github.com/microsoft/BaselineManagement?tab=readme-ov-file#known-gaps-in-capability), we have to comment on some of the settings in the generated `ps1` DSC scripts, to get working DSC `mof` file later!
        
    3. This is also the time when you should modify those `ps1` files to suit your environmental needs
        
6. Now when we have the final `ps1` DSC scripts, we can generate DSC `mof` files from them (if you want to use them outside of Azure ARC)
    
    1. ```powershell
        # generate MOF file 
        # dot source the configuration
        . <pathToGeneratedPs1Script>
        # call the configuration
        <nameOfTheConfigurationStoredInPs1Script>
        # result will be localhost.mof file
        ```
        
7. Or we can generate Guest Configuration packages. There is [official documentation](https://learn.microsoft.com/en-us/azure/governance/machine-configuration/how-to/develop-custom-package/overview) regarding this topic so just a simplified overview
    
    1. ```powershell
        # created ps1 DSC scripts saved in $dscDirectory have to be converted to guest configuration packages now
        # https://learn.microsoft.com/en-us/azure/governance/machine-configuration/how-to/develop-custom-package/overview
        
        # generate guest configuration package
        $param = @{
            Name             = $guestConfigurationName
            Path             = $guestConfigTempDirectory
            Configuration    = "$guestConfigTempDirectory\localhost.mof"
            FrequencyMinutes = 15
            Type             = $definedGuestConfigurationType # AuditAndSet, Audit
            FilesToInclude   = '<pathTo_AuditPolicyDsc_Module>', '<pathTo_GPRegistryPolicyDsc_Module>', '<pathTo_SecurityPolicyDsc_Module>' # modules required by generated package to work correctly on destination machine
            Force            = $true
        }
        "Generate guest configuration package '$guestConfigurationName'"
        New-GuestConfigurationPackage @param -ErrorAction continue
        
        # Upload package to Azure storage
        $param = @{
            Container = $guestConfigContainerName
            File      = $guestConfigFile
            Force     = $true
            Blob      = $policyBlobName
        }
        
        "Upload blob to Azure Storage"
        $guestConfigBlob = Set-AzStorageBlobContent @param
        
        # set package SAS URL
        $param = @{
            StartTime  = $startTime
            ExpiryTime = $endTime
            Container  = $guestConfigContainerName
            Blob       = $policyBlobName
            Permission = 'r'
            Context    = $storageAccount.Context
            FullUri    = $true
        }
        "Set blob SAS Url"
        $contentUri = New-AzStorageBlobSASToken @param
        
        # generate Azure policy template of guest configuration type locally
        $policyConfig = @{
            PolicyId      = (New-Guid).guid
            ContentUri    = $contentUri
            DisplayName   = $guestConfigurationName
            Description   = $description
            Path          = $guestConfigTempDirectory
            Platform      = 'Windows'
            Mode          = $azurePolicyType # ApplyAndMonitor, ApplyAndAutoCorrect, Audit
            PolicyVersion = $newPolicyVersion.tostring()
        }
        
        "Generate locally Azure policy template of the 'Guest Configuration' type ('$azurePolicyType' ver. $($newPolicyVersion.tostring()))"
        New-GuestConfigurationPolicy @policyConfig
        ```
