Get Previous and Scheduled Evaluation Times for ConfigMgr Compliance Baselines with PowerShell

I was testing a compliance baseline recently and wanted to verify if the schedule defined in the baseline deployment is actually honored on the client. I set the schedule to run every hour, but it was clear that it did not run every hour and that some randomization was being used.

To review the most recent evaluation times and the next scheduled evaluation time, I had to read the scheduler.log in the CCM\Logs directory, because I could only find a single last evaluation time recorded in WMI.

The following PowerShell script reads which baselines are currently deployed to the local machine, displays a window for you to choose one, then basically reads the Scheduler log to find when the most recent evaluations were and when the next one is scheduled.

Select a baseline
Baseline evaluations

Inventory Local Administrator Privileges with PowerShell and ConfigMgr

Any security-conscious enterprise will want to have visibility of which users have local administrator privilege on any given system, and if you are an SCCM administrator then the job of gathering this information will likely be handed to you!

However, this task may not be as simple as it seems. Gathering the membership of the local administrators group is one thing, but perhaps more important to know is whether the primary user of a system has administrator privileges. If that user is a member of a group that has been added to the local administrators group, then it isn’t immediately obvious whether they actually have administrator rights without also checking the membership of that group. And what if there are further nested groups – ie the user is a member of a group that’s a member of a group that’s a member of the local administrators group?! Obviously things can get complicated here, making reporting and compliance checking a challenge.

Thankfully, PowerShell can handle complication quite nicely, and ConfigMgr is more than capable as a both a delivery vehicle and a reporting mechanism, so the good news is – we can do this!

The following solution uses PowerShell to gather local administrator information and stamp it to the local registry. A Compliance item in SCCM is used as the delivery vehicle for the script and then RegKeyToMof is used to update the hardware inventory classes in SCCM to gather this information from the client’s registry into the SCCM database, where we can query and report on it.

Gathering Local Administrator Information with PowerShell

To start with, let’s have a look at some of the PowerShell code and the information we will gather with it.

First, we need to identify who is the primary user of the system. Since the script is running locally on the client computer, we will not use User Device Affinity. True, UDA information is stored in WMI in the CCM_UserAffinity class, in the ROOT\CCM\Policy\Machine\ActualConfig namespace.  But this class can contain multiple instances so you can’t always determine the primary user that way.

A better way is to use the SMS_SystemConsoleUsage class in the ROOT\cimv2\sms namespace and query the TopConsoleUser property. This will give you the user account who has had the most interactive logons on the system and for the most part will indicate who the primary user is.


$TopConsoleUser = Get-WmiObject -Namespace ROOT\cimv2\sms -Class SMS_SystemConsoleUsage -Property TopConsoleUser -ErrorAction Stop | Select -ExpandProperty TopConsoleUser

Next, to find if the user is a local admin or not, we will not simply query the local administrator group membership and check if the user is in there. Instead we will create a WindowsIdentity object in .Net and run a method called HasClaim(). I describe this more in a previous blog, but using this method we can determine if the user has local administrator privilege whether through direct membership or through a nested group.


$ID = New-Object Security.Principal.WindowsIdentity -ArgumentList $TopConsoleUser
$IsLocalAdmin = $ID.HasClaim('http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid','S-1-5-32-544')
$ID.Dispose()

The SID for the local admin group (S-1-5-32-544) is used as this is the same across all systems. This will only work for domain accounts as it uses kerberos to create the identity.

Now we will also get the local administrator group membership using the following code (more .Net stuff), and filter just the SamAccountNames.


Add-Type -AssemblyName System.DirectoryServices.AccountManagement -ErrorAction Stop
$ContextType = [System.DirectoryServices.AccountManagement.ContextType]::Machine
$PrincipalContext = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList $ContextType, $($env:COMPUTERNAME) -ErrorAction Stop
$IdentityType = [System.DirectoryServices.AccountManagement.IdentityType]::Name
$GroupPrincipal = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($PrincipalContext, $IdentityType, “Administrators”)
$LocalAdminMembers = $GroupPrincipal.Members | select -ExpandProperty SamAccountName | Sort-Object
$PrincipalContext.Dispose()
$GroupPrincipal.Dispose()

Next, if the user is a local admin through nested group membership, I will call a custom function which will check the nested group membership within the local admin group, for the user account. Let’s say that Group B is a member of Group A, which is a member of the local administrators group. We will check the membership of both Groups B and A to see which ones the user is a member of, and therefore which group/s is effectively giving the user administrator privilege. We do this by querying the $GroupPrincipal object created in the previous code. The custom function will query nested membership up to 3 levels deep.

Now I will query the Install Date for the operating system, since in some cases where a machine is newly built, the TopConsoleUser may not yet be the primary user of the system, but the admin who built the machine, for example. This date helps to identify any such systems.


[datetime]$InstallDate = [System.Management.ManagementDateTimeConverter]::ToDateTime($(Get-WmiObject win32_OperatingSystem -Property InstallDate -ErrorAction Stop | Select -ExpandProperty InstallDate)) | Get-date -Format 'yyyy-MM-dd HH:mm:ss'

Now we gather all this information into a datatable, and call another custom function to write it to the local registry. I use the following registry key, but you can change this in the script if you wish:

HKLM:SOFTWARE\IT_Local\LocalAdminInfo

The script will create the key if it doesn’t exist.

Here’s an example of the kind of data that will be gathered:

localadmin

You can see in this example, that my user account is a local administrator both by direct membership and through nested groups. The actual groups that grant this right are listed in the NestedGroupMembership property.

Create a Compliance Item

Now lets go ahead and create a compliance item in SCCM to run this script.

In the Console, navigate Assets and Compliance > Compliance Settings > Configuration Items.

Click Create Configuration Item

config1

Click Next and select which OS’s you will target.  Remember the Windows XP and Server 2003 may not have PowerShell installed.

Click Next again, then click New to create a new setting.

Choose Script as the setting type, and String as the data type.

config2

Now we need to add the scripts.  You can download both the discovery and remediation scripts from my Github repo here:

https://github.com/SMSAgentSoftware/ConfigMgr/tree/master/PowerShell%20Scripts/Compliance%20Settings/LocalAdministratorInfo

Click Add Script and paste or open the relevant script for each. Make sure Windows Powershell is selected as the script language.

The discovery script simply checks whether the script has been run in the last 15 minutes, and if not returns non-compliant.  This allows the script to run according to the schedule you define for it, ie once a day or once a week etc, to keep the information up-to-date in the registry.

The remediation script does the hard work 🙂

Click OK to close the Create Setting window.

Click Next, then click New to create a new Compliance Rule as follows:

config3

Click OK to close, then Next, Next and Close to finish.

Create a Configuration Baseline

Click on Configuration Baselines and Create Configuration Baseline to create a new baseline.

Give it a name, click Add and add the Configuration Item you just created.

config4

Click OK to close.

Deploy the Baseline

Right-click the baseline and choose Deploy. Make sure to remediate noncompliance and select the collection you wish to target.

config5

Update SCCM Hardware Inventory

Creating the MOF Files

For this part you will need the excellent RegKeyToMOF utility, which you can download from here:

https://gallery.technet.microsoft.com/RegKeyToMof-28e84c28

You will also need to do this on a machine that has either run the remediation script to create the registry keys, or has run the configuration baseline.

Open RegKeyToMOF and browse to the registry key:

HKLM:SOFTWARE\IT_Local\LocalAdminInfo

You can deselect the ‘Enable 64bits …’ option as the registry key is not located in the WOW6432Node.

Click Save MOF to save the required files.

regkey

Copy the SMSDEF.mof and the CM12Import.mof to your SCCM site server.

Update Client Settings

In the SCCM console, navigate Administration > Site Configuration > Client Settings. Open your default client settings and go to the Hardware Inventory page.

Click Set Classes…, then Import…

Browse to the CM12Import.mof and click Import.

import

Close the Client Settings windows.

Update Configuration.mof

Now open your configuration.mof file at <ConfigMgr Installation Directory> \inboxes\clifiles.src\hinv.

In the section at the bottom for adding extensions, which starts like this…

//========================
// Added extensions start
//========================

…paste the contents of the SMSDEF.mof file.  Save and close the file.

Reporting

Now that you’ve deployed the configuration item and updated the SCCM hardware inventory, a new view called dbo.v_GS_LocalAdminInfo0 has been added to the SCCM database. Note that initially there will be no data here until your clients have updated their policies, ran the configuration baseline, and ran the hardware inventory cycle.

You can query using the Queries node in the SCCM console…

query

…or create yourself a custom SCCM report, create an Excel report with a SQL data connection, query the SCCM database with PowerShell – whatever method you need or prefer.

Here is a sample SQL query that will query the view and add some client health data and the chassis type to help distinguish between desktop, laptops, servers etc.


Select
  ComputerName0 as 'ComputerName',
  Case When enc.ChassisTypes0 = 1 then 'Other'
    when enc.ChassisTypes0 = 2 then 'Unknown'
    when enc.ChassisTypes0 = 3 then 'Desktop'
    when enc.ChassisTypes0 = 4 then 'Low Profile Desktop'
    when enc.ChassisTypes0 = 5 then 'Pizza Box'
    when enc.ChassisTypes0 = 6 then 'Mini Tower'
    when enc.ChassisTypes0 = 7 then 'Tower'
    when enc.ChassisTypes0 = 8 then 'Portable'
    when enc.ChassisTypes0 = 9 then 'Laptop'
    when enc.ChassisTypes0 = 10 then 'Notebook'
    when enc.ChassisTypes0 = 11 then 'Hand Held'
    when enc.ChassisTypes0 = 12 then 'Docking Station'
    when enc.ChassisTypes0 = 13 then 'All in One'
    when enc.ChassisTypes0 = 14 then 'Sub Notebook'
    when enc.ChassisTypes0 = 15 then 'Space-Saving'
    when enc.ChassisTypes0 = 16 then 'Lunch Box'
    when enc.ChassisTypes0 = 17 then 'Main System Chassis'
    when enc.ChassisTypes0 = 18 then 'Expansion Chassis'
    when enc.ChassisTypes0 = 19 then 'SubChassis'
    when enc.ChassisTypes0 = 20 then 'Bus Expansion Chassis'
    when enc.ChassisTypes0 = 21 then 'Peripheral Chassis'
    when enc.ChassisTypes0 = 22 then 'Storage Chassis'
    when enc.ChassisTypes0 = 23 then 'Rack Mount Chassis'
    when enc.ChassisTypes0 = 24 then 'Sealed-Case PC'
    else 'Unknown'
  End as 'Chassis Type',
  TopConsoleUser0 as 'Primary User',
  TopConsoleUserIsAdmin0 as 'Primary User is Admin?',
  AdminGroupMembershipType0 as 'Primary User Local Admin Group Membership Type',
  LocalAdminGroupMembership0 as 'Local Admin Group Membership',
  NestedGroupMembership0 as 'Primary User Local Admin Nested Group Membership',
  OSAgeInDays0 as 'OS Age (days)',
  OSInstallDate0 as 'OS Installation Date',
  LastUpdated0 as 'Last Updated Date',
  la.TimeStamp as 'HW Inventory Date',
  ch.ClientStateDescription,
  ch.LastActiveTime
from dbo.v_GS_LocalAdminInfo0 la
join v_R_System sys on la.ComputerName0 = sys.Name0
left join v_GS_SYSTEM_ENCLOSURE enc on sys.ResourceID = enc.ResourceID
left join v_CH_ClientSummary ch on sys.ResourceID = ch.ResourceID
where ComputerName0 is not null
  and enc.ChassisTypes0 <> 12

 

New Free Tool: ConfigMgr Remote Compliance

Remote Compliance

Today I released a new free tool for ConfigMgr administrators and support staff.

ConfigMgr Remote Compliance can be used to view, evaluate and report on System Center Configuration Manager Compliance Baselines on a remote computer. It provides similar functionality to the Configurations tab of the Configuration Manager Control Panel, but for remote computers. It is a useful troubleshooting tool for remotely viewing client compliance, evaluating baselines, viewing the evaluation report or opening DCM log files from the client, without needing to access the client computer directly.

ConfigMgr Remote Compliance can be downloaded from here.

Source code for this application is available on GitHub and code contributions are welcome.

Deploying Custom Microsoft Office Templates with System Center Configuration Manager

Some time ago a wrote a blog describing a way to deploy custom templates for Microsoft Office applications using SCCM Compliance Settings. Since then, I have re-written the solution into something much more manageable as the previous incarnation was not very clearly defined in how to update templates, and involved some considerable admin overhead. This updated solution is much improved and better manages the lifecycle of your custom templates, including updating, adding and retiring templates. Much of this process is now automated using PowerShell, and I have removed the need to manually specify all the template file names in the scripts so it is also much easier to set up and deploy.

It is relatively detailed, so instead of writing a blog I put this into a free PDF guide which you can download from here:

Deploying Custom Microsoft Office Templates with System Center Configuration Manager

pdfimg

Export / Backup Compliance Setting Scripts with PowerShell

In my SCCM environment I have a number of Compliance Settings that use custom scripts for discovery and remediation, and recently it dawned on me that a lot of time has been spent on these and it would be good to create a backup of those scripts. It would also be useful to be able to export the scripts so they could be edited and tested before being updated in the Configuration Item itself. So I put to together this PowerShell script which does just that!

The Configuration Item scripts are stored in an XML definition, and this can be read from the SCCM database directly and parsed with PowerShell, so that’s what this script does. It will load all the Configuration Items into a datatable from a SQL query, then go through each one looking for any settings that have scripts defined. These scripts will be exported in their native file format.

You could then edit those scripts, or add the export location to your file/folder backup for an extra level of protection for your hard work!

Here you can see an example of the output for my “Java Settings” Configuration item. A subdirectory is created for the current package version, then subdirectories under that for each Configuration setting, then the discovery and remediation scripts for that setting.

cis
Exported Configuration Item Scripts

Note that the script will only process Compliance Items with a CIType_ID of 3, which equates to the Operating System type you will see in the SCCM console for the Configuration Item, which is the type that may use a script as the discovery source.

Export-CMConfigurationItemScripts.ps1

Disabling Java Content in all Browsers with ConfigMgr Compliance Settings

Some organisations like to disable Java applets from running in a web browser for tighter security.  This can be done with group policy, but in our organisation I already manage Java settings across the enterprise with Configuration Manager’s Compliance Settings (as documented in my solution guide for Java), so I decided to use a Compliance Setting for this also.

The best way to disable Java in the browser is simply to deselect the “Enable Java content in the browser” setting in the Java Control Panel:

Capture

Doing that will change a fair number of registry keys, too many to manage or set individually. Thankfully, you can achieve the same result using the following command from your Java installation files:

“C:\Program Files\Java\<java version>\bin\ssvagent.exe” -disablewebjava

When Java in the browser is disabled, the following key is set in the registry, which we can use as a way of programatically detecting whether Java in the browser has been enabled or not:

Key: HKLM:\SOFTWARE\Oracle\JavaDeploy
Name: WebDeployJava
Value: disabled

Now I create a new setting in my “Java Settings” configuration item, which I’ll call “Java WebDeploy”:

Capture

In this setting I use two PowerShell scripts, one for discovery, and one for remediation, which you can find below.  The discovery script will use the registry key above to determine whether Java has been disabled in the browser or not, and the remediation script will run the command that disables web Java for all installed Java versions.

Capture

For my compliance rule, I simply use the value “Compliant” which is outputted by the script:

Capture

Once this setting has been deployed in a baseline to your computers, Java will be disabled in web browsers for each machine that has Java installed in the default locations.  Should a user manually enable it from the Java control panel, the ConfigMgr client will disable it again according to the compliance evaluation schedule you have defined.

Discovery Script


$key = "HKLM:\SOFTWARE\Oracle\JavaDeploy"
if (Test-Path $key)
    {
        if ((Get-ItemProperty -Path $key -Name WebDeployJava -ErrorAction SilentlyContinue | Select -ExpandProperty WebDeployJava) -ne "disabled")
            {
                Write-Host "Not Compliant"
            }
        Else {write-host "Compliant"}
    }
else {write-host "Compliant"}

Remediation Script


$JavaInstallPaths = @()

if (${env:ProgramFiles(x86)})
    {
        $path = "${env:ProgramFiles(x86)}\Java"
        if (Test-Path $path)
            {
                $JavaInstall = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Java" | select -ExpandProperty FullName
                $JavaInstallPaths += $JavaInstall
            }
    }

if ($env:ProgramFiles)
    {
        $path = "$env:ProgramFiles\Java"
        if (Test-Path $path)
            {
                $JavaInstall = Get-ChildItem -Path "$env:ProgramFiles\Java" | select -ExpandProperty FullName
                $JavaInstallPaths += $JavaInstall
            }
    }

$JavaInstallPaths

foreach ($JavaInstallPath in $JavaInstallPaths)
    {
        Start-Process -FilePath "$JavaInstallPath\bin\ssvagent.exe" -ArgumentList "-disablewebjava" -Wait -Verb Runas
    }

Create a Database of Error Codes and Descriptions for Windows and ConfigMgr

In a recent post, I described different ways to translate error codes for Windows and Configuration Manager into their friendly descriptions.  In this post, I will show you how to create a SQL database of known error codes and descriptions that you can join to in your SQL queries, to help simplify your troubleshooting, and I will also give some example queries you can use with Configuration Manager.

Windows and system error codes are standard and are published by Microsoft on MSDN, but there is no published resource of error codes for Configuration Manager 2012 onwards that I know of.  To have a database of all these codes is quite useful as they are not stored either in WMI or in the ConfigMgr database – only the error codes themselves are stored.  These codes are translated to their descriptions by the ConfigMgr console and the ConfigMgr SSRS Reports probably utilizing dll files.

I extracted a list of 11,839 error codes and descriptions using the SrsResource.dll, as described in the previous post, and exported them into a csv file.  Using the PowerShell function below, I converted each error code to give the hex and decimal codes for each.  In Configuration Manager, the log files and reports tend to use the hexadecimal value or the ‘signed integer’ decimal value for the error code, however WMI stores the codes as ‘unsigned integers’ (always positive or zero), therefore I have included all three for easy referencing.


function Convert-Number {
[CmdletBinding()]
    param
        (
        [Parameter(Mandatory=$True)]
            $Number,
        [Parameter(Mandatory=$True,ParameterSetName='Binary')]
            [switch]$ToBinary,
        [Parameter(Mandatory=$True,ParameterSetName='Hex')]
            [switch]$ToHexadecimal,
        [Parameter(Mandatory=$True,ParameterSetName='Signed')]
            [switch]$ToSignedInteger,
        [Parameter(Mandatory=$True,ParameterSetName='Unsigned')]
            [switch]$ToUnSignedInteger
        )

$binary = [Convert]::ToString($Number,2)

if ($ToBinary)
    {
        $binary
    }

if ($ToHexadecimal)
    {
        $hex = "0x" + [Convert]::ToString($Number,16)
        $hex
    }

if ($ToSignedInteger)
    {
        $int32 = [Convert]::ToInt32($binary,2)
        $int32
    }
if ($ToUnSignedInteger)
    {
        $Uint64 = [Convert]::ToUInt64($binary,2)
        $Uint64
    }
}

Using this function, you can convert between binary, hexadecimal, signed and unsigned integers:

CaptureTo import those codes into a SQL database, first download the attached XLSX file which contains all the codes, and save it in CSV format.  The error descriptions have had any line breaks removed so that they will import correctly.

ErrorCodes_Final.xlsx

Now run the following T-SQL code against your SQL instance.  It will create a new database called ‘ErrorCodes’ and import all the entries from the CSV into a new table called ‘WindowsErrorCodes’.  Change the path to the CSV file as needed.

I’m using the same SQL instance as my Configuration Manager database so I can easily reference the two.


Create Database ErrorCodes
Go
USE ErrorCodes;
CREATE TABLE WindowsErrorCodes (
Hexadecimal VARCHAR(10) NOT NULL,
SignedInteger BIGINT NOT NULL,
UnSignedInteger BIGINT NOT NULL,
ErrorDescription NVARCHAR(MAX)
);

BULK
INSERT WindowsErrorCodes
FROM '<mycomputer>\C$\temp\ErrorCodes_Final.csv'
WITH
(
FIRSTROW = 2,
FIELDTERMINATOR = ',',
ROWTERMINATOR = 'n'
)
GO

Now let’s run a quick query to find a Configuration Manager error description:

Capture3

If I want to query for application deployment errors, similar to the PowerShell script in my last post, then I can use the following query entering the AssignmentID of the application deployment, which you can find from the ConfigMgr Console in the additional columns.  I will join the app deployment errors by their error code to my new database to return the error descriptions for each.  Join the ErrorCode field from the ConfigMgr database views with the SignedInteger field from the error code database.


select  app.ApplicationName, ass.CollectionName,
sys.Name0 as 'Computer Name',
det.ResourceID, det.CIVersion, det.ErrorCode, det.Errortype,
err.Hexadecimal, err.ErrorDescription
from v_CIErrorDetails det
inner join V_R_System sys on det.ResourceID = sys.ResourceID
inner join v_CIAssignmentToCI ci on det.CI_ID = ci.CI_ID
inner join v_CIAssignment ass on ci.AssignmentID = ass.AssignmentID
inner join v_ApplicationAssignment app on ci.AssignmentID = app.AssignmentID
left join ErrorCodes.dbo.WindowsErrorCodes err on det.ErrorCode = err.SignedInteger
where ci.AssignmentID = 16777540
order by sys.Name0

Results:

capture2

Cool 🙂

I can also get summary data categorized by the error code, for that deployment, again using the AssignmentID:


Select
sum.CollectionName,
sum.Description,
err.DTCI,
err.StatusType,
err.EnforcementState,
err.ErrorCode,
code.Hexadecimal,
code.ErrorDescription,
err.Total
from vAppDeploymentErrorStatus err
inner join v_CIAssignment ass on err.AssignmentUniqueID = ass.Assignment_UniqueID
inner join vAppDTDeploymentSummary sum on err.DTCI = sum.DTCI
left join ErrorCodes.dbo.WindowsErrorCodes code on err.ErrorCode = code.SignedInteger
where err.assignmentID = 16777540
and sum.assignmentID = err.AssignmentID
and err.ErrorCode <> 0
order by Description, Total desc

Results:

capture5

Both of these queries together roughly equate to what you can see in the ConfigMgr Console > Deployments node:

capture4Since we now don’t depend on the Console or the SSRS reports to translate the error descriptions for us, we can go ahead and more easily create custom reports or SQL queries or PowerShell scripts to report this information for us 🙂

 

Deploying Custom Office Templates with ConfigMgr Compliance Settings

Note: This blog post is out of date and has been replaced with a new solution which you can find here.

If you use Custom Office templates in your organisation, you can deploy them to your computers using Compliance Settings in ConfigMgr.  This also provides a mechanism for updating those templates if necessary, and ensuring that the templates remain installed.  In this example, I will deploy a custom PowerPoint template to all computers with a version of Microsoft Office 2010/2013 installed, using ConfigMgr 2012.

Overview

The high-level steps are as follows:

  1. Create your custom template
  2. Create preview and thumbnail image files
  3. Create XML configuration files
  4. Place them on a network share
  5. Create a Compliance setting
  6. Create a Compliance item for machine-based settings using a Powershell script for both discovery and remediation
  7. Create a Compliance item for user-based settings using a Powershell script for both discovery and remediation
  8. Create a Compliance baseline, and deploy it to a Collection containing computers with MS Office installed

You can create custom templates for Word, Powerpoint or Excel and the process for deploying them is nicely described in this technet article.  I will cover the main points of the process and the additional steps for ConfigMgr.

https://technet.microsoft.com/en-us/library/cc178976(v=office.14).aspx

Create your Custom Template

Create your template and save it as a .potx template file.

Create Thumb and Preview Image Files

This step is optional, but it gives a nice visual indicator of the template when you open the custom templates location, so I include it.

Contoso_Thumb

To create the files, use the Windows Snipping tool or something similar and take a snip of the first page of the template.  Save it in a supported format (.jpg, .png, .bmp, .gif) and give it a name, something like ‘Contoso_PP_Thumb_2015‘, where Contoso represents your organisation name, or the name of the folder you will be placing your custom templates into.  I append the current year to the file name as now and then we will update the templates with new graphics etc, so we need to distinguish it from newer templates in the future.

Copy the file you just created, and rename it ‘Contoso_PP_Preview_2015‘.

Open both image files with an Image editor like MS Paint, and resize them according to the MS recommendations (ymmv):

Preview type Width Height
Thumbnail 100 120
Preview 256 350

Create XML Configuration Files

You need to create a configuration xml file that tells Office how to display the template and where to find the relevant files.  In my organisation, I am deploying to both x86 and x64 Operating Systems, so I will create one file for each architecture, the only difference being the path to the template folder, eg

x86: C:\Program Files\Microsoft Office\Templates\Contoso
x64: C:\Program Files (x86)\Microsoft Office\Templates\Contoso

This is an example xml file for x64:


<?xml version="1.0" encoding="utf-8"?>
<o:featuredcontent lcid="1033" xmlns:o="urn:schemas-microsoft-com:office:office">
<o:application id="PP">
<o:featuredtemplates startdate="2011-03-01" enddate="2099-03-01">

<!-- TEMPLATE 1 -->
<o:featuredtemplate title="Contoso PowerPoint Template" source="C:\Program Files (x86)\Microsoft Office\Templates\Contoso\Contoso_PP_Template_2015.potx" >
<o:media mediatype="png" filename="Contoso_PP_Thumb_2015.PNG" source="C:\Program Files (x86)\Microsoft Office\Templates\Contoso\Contoso_PP_Thumb_2015.PNG" />
<o:preview filename="Contoso_PP_Preview_2015.PNG" source="C:\Program Files (x86)\Microsoft Office\Templates\Contoso\Contoso_PP_Preview_2015.PNG" />
</o:featuredtemplate>

</o:featuredtemplates>
</o:application>
</o:featuredcontent>

I’m going to copy my template files from a remote file share to the local path indicated above using the Compliance settings.  This path is a place you can store all your custom Office templates, and means they will always be available even offline, although you could use a remote location if you wanted.

You can have several templates in one xml file.  The application is specified by the following line: (PP=Powerpoint, WD=Word, XL=Excel).

<o:application id=”PP”>

Save the xml files, something like ‘Contoso_PP_x64_2015.xml‘.

Place the Template Files in a Network Share

Place all your templates files, (PP template, image files and xml files) in a central location that anyone can access.  The Compliance settings will copy the files from this location to the local machine.

Create a Configuration Item

In the ConfigMgr Console, navigate to Assets and Compliance > Compliance Settings.  Right-click Configuration Items and create a new Configuration Item.

Let’s call it Contoso PowerPoint Template.

Comp1

On the Settings page, click New to create a new setting.  We will create two settings, one for machine-based activities, ie activities that can run with the default ConfigMgr security context, and one for user-based activities, in this case we need to set a registry key in the HKCU branch, so this must run with the logged-on user’s security context.

Machine-Based Settings

Enter a Name for the setting and set the Setting type as Script, and the Data type as String.

Comp4

Discovery Script

In the Discovery Script section, click Add Script, choose Windows Powershell as the Script language, and paste the following PowerShell script.


# Determine OS Architecture
$OSArch = Get-WmiObject -Class win32_operatingsystem | select -ExpandProperty OSArchitecture

# Set local template location
$ProviderName = "Contoso" # Change
$x86RootPath = "C:\Program Files\Microsoft Office\Templates\$ProviderName"
$x64RootPath = "C:\Program Files (x86)\Microsoft Office\Templates\$ProviderName"

$Compliance = 0

$x86FilesToCheck = @(
    "Contoso_PP_Template_2015.potx", # Change
    "Contoso_PP_Preview_2015.PNG", # Change
    "Contoso_PP_Thumb_2015.PNG", # Change
    "Contoso_PP_x86_2015.xml" # Change
    )

$x64FilesToCheck = @(
    "Contoso_PP_Template_2015.potx", # Change
    "Contoso_PP_Preview_2015.PNG", # Change
    "Contoso_PP_Thumb_2015.PNG", # Change
    "Contoso_PP_x64_2015.xml" # Change
    )

if ($OSArch -eq "64-bit")
    {
        foreach ($file in $x64FilesToCheck)
            {
                if (test-path -Path $x64RootPath\$file)
                    {$Compliance ++}
            }
    }

if ($OSArch -eq "32-bit")
    {
        foreach ($file in $x86FilesToCheck)
            {
                if (test-path -Path $x86RootPath\$file)
                    {$Compliance ++}
            }
    }

if ($Compliance -eq 4)
    {write-host "Compliant"}
else {write-host "Not Compliant"}

Variables to Change

The discovery script contains some variables that you will need to change for your environment, and these have a comment at the end ‘# Change‘.

Variable Name Value
$ProviderName The name of the folder you will create that contains your custom template/s.
$x86FilesToCheck This is the list of template filenames relevant to the x86 architecture. The only unique file is the xml file.
$x64FilesToCheck This is the list of template filenames relevant to the x64 architecture.  The only unique file is the xml file.

What Does the Script Do?

The discovery script is used to determine the compliance state.  In this case, we check whether the template files have been copied to the local machine and all the files are present and correctly named.  SInce the path is different for x86 and x64 OSes, we need to determine the architecture of the machine and then check for the files in the relevant path.

Remediation Script

In the Remediation Script section, click Add Script, choose Windows Powershell as the Script language, and paste the following PowerShell script.


# Determine OS Architecture
$OSArch = Get-WmiObject -Class win32_operatingsystem | select -ExpandProperty OSArchitecture

# Set local template location
$ProviderName = "Contoso" # Change
$x86RootPath = "C:\Program Files\Microsoft Office\Templates\$ProviderName"
$x64RootPath = "C:\Program Files (x86)\Microsoft Office\Templates\$ProviderName"

# Set old  file names
$oldtemplate = "Contoso_PP_Template_2014.potx" # Change
$oldPreviewPNG = "Consoto_PP_Preview_2014.png" # Change
$oldThumbPNG = "Contoso_PP_Thumb_2014.png" # Change
$oldx86xml = "Contoso_PP_x86_2014.xml" # Change
$oldx64xml = "Contoso_PP_x64_2014.xml" # Change

# Set new file names
$newTemplateRootPath = "\\<server>\<fileshare>\Contoso" # Change
$newtemplate = "Contoso_PP_Template_2015.potx" # Change
$newPreviewPNG = "Contoso_PP_Preview_2015.PNG" # Change
$newThumbPNG = "Contoso_PP_Thumb_2015.PNG" # Change
$newx86xml  = "Contoso_PP_x86_2015.xml" # Change
$newx64xml  = "Contoso_PP_x64_2015.xml" # Change

$oldArray = @($oldtemplate,$oldPreviewPNG,$oldThumbPNG,$oldx86xml,$oldx64xml)
$newx86Array = @($newtemplate,$newPreviewPNG,$newThumbPNG,$newx86xml)
$newx64Array = @($newtemplate,$newPreviewPNG,$newThumbPNG,$newx64xml)

# Function to copy the template files
function CopyTemplateFiles {

param($rootpath,$templatearray,$newTemplateRootPath,$oldArray)

        # If template directory doesn't exist, create it
        if (!(test-path -Path $rootpath))
            {New-Item -Path $rootpath -ItemType Directory -Force}
        # Otherwise delete old template files if they exist
        Else
            {
                foreach ($item in $oldarray)
                    {
                        if (Test-path -Path $rootpath\$item)
                            {
                                Remove-Item $rootpath\$item -Force
                            }
                    }
            }
        #  Copy new template files
        foreach ($item in $templatearray)
            {
                Copy-Item -Path $newTemplateRootPath\$item -Destination $rootpath -Force
            }

}

# Let's do it!
if ($OSArch -eq "32-bit")
    {
        CopyTemplateFiles -rootpath $x86RootPath -templatearray $newx86Array -newTemplateRootPath $newTemplateRootPath -oldArray $oldArray
    }

if ($OSArch -eq "64-bit")
    {
        CopyTemplateFiles -rootpath $x64RootPath -templatearray $newx64Array -newTemplateRootPath $newTemplateRootPath -oldArray $oldArray
    }

Variables to Change

The remediation script is the one that will copy the template files to the local machine.  It is setup so that you can also update an existing custom template by removing the existing template files and replacing them with the new ones.  If there is no existing template, then you don’t need to do anything with the variables that start with ‘old‘.  If you are updating an existing template, use the old variables to specify the names of the old template files, and the new variables for the new template files.

Variable Name Value
$ProviderName The name of the folder you will create that contains your custom template/s.
$oldtemplate The name of the template file you want to replace, if you are updating the template
$oldPreviewPNG The name of the Preview image file you want to replace, if you are updating the template
$oldThumbPNG The name of the Thumbnail image file you want to replace, if you are updating the template
$oldx86xml The name of the x86 xml file you want to replace, if you are updating the template
$oldx64xml The name of the x64 xml file you want to replace, if you are updating the template
$newTemplateRootPath The UNC path to the network share you will copy the template files from
$newtemplate The name of the template file
$newPreviewPNG The name of the Preview image file
$newThumbPNG The name of the Thumbnail image file
$newx86xml The name of the x86 xml file
$newx64xml The name of the x64 xml file

What Does the Script Do?

We check if there are any existing template files with the names specified in the old variables, and remove them. Then we copy the new template files to the relevant local path according to the OS architecture.

Compliance Rule

On the Compliance Rules tab, click New to create a new rule. If the discovery script determines that the machine is compliant, it will output the value ‘Compliant‘, so we will use that in the Compliance rule.  We will also check the box to run the remediation script if the setting is non-compliant.

Comp5

User-Based Settings

In the same Compliance item, add a new Compliance setting for the user-based settings.  In this setting, we must check the box ‘Run scripts by using the logged on user credentials‘.

Comp6

Discovery Script

In the Discovery Script section, click Add Script, choose Windows Powershell as the Script language, and paste the following PowerShell script

# Set PowerPoint exe locations
$x64_PP_2013 ="C:\Program Files (x86)\Microsoft Office\Office15\POWERPNT.EXE"
$x86_PP_2013 ="C:\Program Files\Microsoft Office\Office15\POWERPNT.EXE"
$x64_PP_2010 ="C:\Program Files (x86)\Microsoft Office\Office14\POWERPNT.EXE"
$x86_PP_2010 ="C:\Program Files\Microsoft Office\Office14\POWERPNT.EXE"

# Set Registry key paths
$Office_2010_path = "HKCU:\Software\Microsoft\Office\14.0\Common\Spotlight\Providers"
$Office_2013_path = "HKCU:\Software\Microsoft\Office\15.0\Common\Spotlight\Providers"
$Provider_Name = "Contoso" # Change

# Set Registry key values
$x64_key = "C:\\Program Files (x86)\\Microsoft Office\\Templates\\$Provider_Name\\"
$x86_key = "C:\\Program Files\\Microsoft Office\\Templates\\$Provider_Name\\"
$x64_xml = "Contoso_PP_x64_2015.xml" # Change
$x86_xml = "Contoso_PP_x86_2015.xml" # Change

# Function to check the registry key value
function CheckRegKey {

param ($office_path,$Branch_Name,$key_value,$xml)
        $key = Get-ItemProperty -Path $office_path\$Branch_Name -Name ServiceURL -ErrorAction SilentlyContinue | Select -ExpandProperty ServiceURL
        if (!$key)
            {write-host "Not Compliant"; break}
        if ($key -eq "$key_value" + "$xml")
            {write-host "Compliant"}
        if ($key -ne "$key_value" + "$xml")
            {write-host "Not Compliant"}
}

# Let's do it!
if (Test-Path $x64_PP_2013)
    { CheckRegKey -office_path $Office_2013_path -Branch_Name $Provider_Name -key_value $x64_key -xml $x64_xml }

if (Test-Path $x86_PP_2013)
    { CheckRegKey -office_path $Office_2013_path -Branch_Name $Provider_Name -key_value $x86_key -xml $x86_xml }

if (Test-Path $x64_PP_2010)
    { CheckRegKey -office_path $Office_2010_path -Branch_Name $Provider_Name -key_value $x64_key -xml $x64_xml }

if (Test-Path $x86_PP_2010)
    { CheckRegKey -office_path $Office_2010_path -Branch_Name $Provider_Name -key_value $x86_key -xml $x86_xml }

Variables to Change

The $Provider_Name variable should be the same as the $ProviderName variable you used in the scripts for the machine settings.

Variable Name Value
$Provider_Name The name of the folder you will create that contains your custom template/s.
$x64_xml The name of the x64 xml file
$x86_xml The name of the x86 xml file

What Does the Script Do?

The discovery script will check whether a registry key ‘ServiceURL‘ has been set and contains the correct value. Since the script may run on either x86 or x64 OSes, and it may be used for either Office 2010 or Office 2013, we need to define the appropriate locations of the PowerPoint executable and registry key paths in the script.

Remediation Script

In the Remediation Script section, click Add Script, choose Windows Powershell as the Script language, and paste the following PowerShell script.

# Set PowerPoint exe locations
$x64_PP_2013 ="C:\Program Files (x86)\Microsoft Office\Office15\POWERPNT.EXE"
$x86_PP_2013 ="C:\Program Files\Microsoft Office\Office15\POWERPNT.EXE"
$x64_PP_2010 ="C:\Program Files (x86)\Microsoft Office\Office14\POWERPNT.EXE"
$x86_PP_2010 ="C:\Program Files\Microsoft Office\Office14\POWERPNT.EXE"

# Set Reg branch paths / names
$reg_root_path_2010 = "HKCU:\Software\Microsoft\Office\14.0\Common"
$reg_root_path_2013 = "HKCU:\Software\Microsoft\Office\15.0\Common"
$reg_branch_1 = "Spotlight"
$reg_branch_2 = "Providers"
$reg_Key_name = "Contoso" # Change
$x64_xml = "Contoso_PP_x64_2015.xml" # Change
$x86_xml = "Contoso_PP_x86_2015.xml" # Change

# Set Reg key values
$newx86xml = "C:\\Program Files\\Microsoft Office\\Templates\\$reg_Key_name\\$x86_xml"
$newx64xml = "C:\\Program Files (x86)\\Microsoft Office\\Templates\\$reg_Key_name\\$x64_xml"

# Function to add the registry key
function AddRegKey {

param ($reg_root_path,$reg_key_Name,$xml,$reg_branch_1,$reg_branch_2)

# Does reg branch exist?  If not, create it
if (!(Test-path $reg_root_path\$reg_branch_1))
    {
        New-Item -Path $reg_root_path -Name $reg_branch_1
    }
if (!(Test-path $reg_root_path\$reg_branch_1\$reg_branch_2))
    {
        New-Item -Path $reg_root_path\$reg_branch_1 -Name $reg_branch_2
    }
if (!(Test-path $reg_root_path\$reg_branch_1\$reg_branch_2\$reg_Key_name))
    {
        New-Item -Path $reg_root_path\$reg_branch_1\$reg_branch_2 -Name $reg_Key_name
    }
# Add reg key
    New-ItemProperty -Path $reg_root_path\$reg_branch_1\$reg_branch_2\$reg_Key_name -Name ServiceURL -Value $xml -Force
}

# X64 OS and Office 2013
if (test-path $x64_PP_2013)
    { AddRegKey -reg_root_path $reg_root_path_2013 -reg_key_Name $reg_Key_name -xml $newx64xml -reg_branch_1 $reg_branch_1 -reg_branch_2 $reg_branch_2 }

# X64 OS and Office 2010
if (test-path $x64_PP_2010)
    { AddRegKey -reg_root_path $reg_root_path_2010 -reg_key_Name $reg_Key_name -xml $newx64xml -reg_branch_1 $reg_branch_1 -reg_branch_2 $reg_branch_2 }

# X86 OS and Office 2013
if (test-path $x86_PP_2013)
    { AddRegKey -reg_root_path $reg_root_path_2013 -reg_key_Name $reg_Key_name -xml $newx86xml -reg_branch_1 $reg_branch_1 -reg_branch_2 $reg_branch_2 }

# X86 OS and Office 2010
if (test-path $x86_PP_2010)
    { AddRegKey -reg_root_path $reg_root_path_2010 -reg_key_Name $reg_Key_name -xml $newx86xml -reg_branch_1 $reg_branch_1 -reg_branch_2 $reg_branch_2 }

Variables to Change

The $reg_Key_name variable should be the same as the $provider_name variable you have set in the previous scripts

Variable Name Value
$reg_Key_name The name of the folder you will create that contains your custom template/s.
$x64_xml The name of the x64 xml file
$x86_xml The name of the x86 xml file

What Does the Script Do?

The remediation script sets the following registry key in the current user hive.  The key defines the location of the xml file, which contains the locations of the custom template files.

Branch: HKCU:\Software\Microsoft\Office\<office_version>\Common\Spotlight\Providers
Key: ServiceURL

In some cases, the ..\Spotlight\Providers branch may not yet have been created, so the script will create the full branch if necessary.

Like the discovery script, it may run on either x86 or x64 OSes, and it may be used for either Office 2010 or Office 2013, so we need to define the appropriate locations of the PowerPoint executable and registry key paths in the script.

Compliance Rule

Add a Compliance Rule to this setting in the same way you did for the machine-based settings.

Complete the Configuration Item wizard to finish.

Create a Configuration Baseline

In the ConfigMgr Console, right-click Configuration Baselines and create a new Baseline.  Add the Configuration Item you created.

Comp7

Create a Collection of Computers with MS Office Installed

I use the following query to create a collection of all computers that have any variant of Microsoft Office 2010/2013 Installed:

select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResourceDomainORWorkgroup,SMS_R_SYSTEM.Client from SMS_R_System inner join SMS_G_System_INSTALLED_SOFTWARE on SMS_G_System_INSTALLED_SOFTWARE.ResourceID = SMS_R_System.ResourceId where SMS_G_System_INSTALLED_SOFTWARE.ProductName like "Microsoft Office Enterprise%" or SMS_G_System_INSTALLED_SOFTWARE.ProductName like "Microsoft Office Home and Business%" or SMS_G_System_INSTALLED_SOFTWARE.ProductName like "Microsoft Office Professional%" or SMS_G_System_INSTALLED_SOFTWARE.ProductName like "Microsoft Office Home and Student%" or SMS_G_System_INSTALLED_SOFTWARE.ProductName like "Microsoft Office Standard%"

To use this query, you will need to enable the Installed Software class in your hardware inventory (Client settings > Hardware Inventory > Set classes)

Comp8

Deploy the Baseline

Finally, deploy the Compliance Baseline to your collection.  Remember to check the option to remediate non-compliant rules:

Comp9

You can create multiple Configuration Items and settings deploying templates for multiple applications using the same baseline.

Updating the Template

To update the template to a new version, you need to do the following:

  1. Copy the new template file to the network share and make sure it has a different name to the old one, eg append ‘_v2’ to the filename for example.
  2. Copy the two xml files in the network share, and also append the next version number to the filenames.
  3. Edit the xml files, changing the filename of the template in the featuredtemplate line to the new version.
  4. Update the Discovery script for the Machine settings.  Update lines 12,15, 19, 22 with the filenames of the new template and xml files
  5. Update the Remediate script for the Machine settings.  Update lines 10, 13, 14 with filenames of the old template and xml files (ie the ones you want to replace).  Update lines 18, 21, 22 with the filenames of the new template and xml files.
  6. Update the Discovery script for the User settings.  Update lines 15, 16 with the new xml filenames.
  7. Update the Remediate script for the User settings.  Update lines 13, 14 with the new xml filenames.
  8. Finally you will need to deploy an additional User configuration setting using the same process described previously, but using the discovery and remediation scripts below. These will check and update another registry key where the content location of the template file is stored.

Discovery Script

# Set PowerPoint exe locations
$x64_PP_2013 ="C:\Program Files (x86)\Microsoft Office\Office15\POWERPNT.EXE"
$x86_PP_2013 ="C:\Program Files\Microsoft Office\Office15\POWERPNT.EXE"
$x64_PP_2010 ="C:\Program Files (x86)\Microsoft Office\Office14\POWERPNT.EXE"
$x86_PP_2010 ="C:\Program Files\Microsoft Office\Office14\POWERPNT.EXE"

# Set Reg branch paths / names
$provider_name = "Contoso"
$reg_root_path_2010 = "HKCU:\Software\Microsoft\Office\14.0\Common\Spotlight\Content\$provider_name\PP1033\FeaturedTemplates\1\1"
$reg_root_path_2013 = "HKCU:\Software\Microsoft\Office\15.0\Common\Spotlight\Content\$provider_name\PP1033\FeaturedTemplates\1\0000001"

# The value of the 'source' registry key, which is the location of the template
$new_template_name = "Contoso_PowerPoint_Template_2015_v2.potx"
$x64_Template = "C:\Program Files (x86)\Microsoft Office\Templates\$provider_name\$new_template_name"
$x86_Template = "C:\Program Files\Microsoft Office\Templates\$provider_name\$new_template_name"

# Function to check the registry key
function CheckRegKey {

param ($Branch_Name, $key_value)
        $key = Get-ItemProperty -Path $Branch_Name -Name source -ErrorAction SilentlyContinue | Select -ExpandProperty source
        if (!$key)
            {write-host "Not Compliant"; break}
        if ($key -eq "$key_value")
            {write-host "Compliant"}
        if ($key -ne "$key_value")
            {write-host "Not Compliant"}
}

# Function to check the registry path
function CheckRegPath {

param ($reg_root_path)
if (!(Test-path $reg_root_path))
    {
        Write-host "Compliant"
        break
    }
}

# X64 OS and Office 2013
if (test-path $x64_PP_2013)
    {
        CheckRegPath -reg_root_path $reg_root_path_2013
        CheckRegKey -Branch_Name $reg_root_path_2013 -key_value $x64_Template
     }

# X64 OS and Office 2010
if (test-path $x64_PP_2010)
    {
        CheckRegPath -reg_root_path $reg_root_path_2010
        CheckRegKey -Branch_Name $reg_root_path_2010 -key_value $x64_Template
     }

# X86 OS and Office 2013
if (test-path $x86_PP_2013)
    {
        CheckRegPath -reg_root_path $reg_root_path_2013
        CheckRegKey -Branch_Name $reg_root_path_2013 -key_value $x86_Template
     }

# X86 OS and Office 2010
if (test-path $x86_PP_2010)
    {
        CheckRegPath -reg_root_path $reg_root_path_2010
        CheckRegKey -Branch_Name $reg_root_path_2010 -key_value $x86_Template
     }

Variables to Change

The $Provider_Name variable should be the same as the $ProviderName variable you used in the scripts for the machine settings.

Variable Name Value
$Provider_Name The name of the folder you created that contains your custom template/s.
$new_template_name The filename of the new template document

What Does the Script Do?

The discovery script will check whether the ‘source’ registry key in the branch indicated contains the correct filename and location for the new template file.

Remediation Script

# Set PowerPoint exe locations
$x64_PP_2013 ="C:\Program Files (x86)\Microsoft Office\Office15\POWERPNT.EXE"
$x86_PP_2013 ="C:\Program Files\Microsoft Office\Office15\POWERPNT.EXE"
$x64_PP_2010 ="C:\Program Files (x86)\Microsoft Office\Office14\POWERPNT.EXE"
$x86_PP_2010 ="C:\Program Files\Microsoft Office\Office14\POWERPNT.EXE"

# Set Reg branch paths / names
$provider_name = "Contoso"
$reg_root_path_2010 = "HKCU:\Software\Microsoft\Office\14.0\Common\Spotlight\Content\$provider_name\PP1033\FeaturedTemplates\1\1"
$reg_root_path_2013 = "HKCU:\Software\Microsoft\Office\15.0\Common\Spotlight\Content\$provider_name\PP1033\FeaturedTemplates\1\0000001"

# The value of the 'source' registry key, which is the location of the template
$new_template_name = "Contoso_PowerPoint_Template_2015_v2.potx"
$x64_Template = "C:\Program Files (x86)\Microsoft Office\Templates\$provider_name\$new_template_name"
$x86_Template = "C:\Program Files\Microsoft Office\Templates\$provider_name\$new_template_name"

# Function to add the registry key
function AddRegKey {

param ($reg_root_path,$reg_key_Name)

    New-ItemProperty -Path $reg_root_path -Name source -Value $reg_key_name -Force
}

# X64 OS and Office 2013
if (test-path $x64_PP_2013)
    {
        AddRegKey -reg_root_path $reg_root_path_2013 -reg_key_Name $x64_Template
     }

# X64 OS and Office 2010
if (test-path $x64_PP_2010)
    {
        AddRegKey -reg_root_path $reg_root_path_2010 -reg_key_Name $x64_Template
     }

# X86 OS and Office 2013
if (test-path $x86_PP_2013)
    {
        AddRegKey -reg_root_path $reg_root_path_2013 -reg_key_Name $x86_Template
     }

# X86 OS and Office 2010
if (test-path $x86_PP_2010)
    {
        AddRegKey -reg_root_path $reg_root_path_2010 -reg_key_Name $x86_Template
     }

Variables to Change

The $Provider_Name variable should be the same as the $ProviderName variable you used in the scripts for the machine settings.

Variable Name Value
$Provider_Name The name of the folder you created that contains your custom template/s.
$new_template_name The filename of the new template document

What Does the Script Do?

The remediation script will update the ‘source’ registry key in the branch indicated with the correct filename and location for the new template file.