Weekend Scripter: Use PowerShell for ADM Cleanup—The Exciting Conclusion

Summary: Two Microsoft PFEs conclude their three-part series on cleaning up old ADM files from AD DS.

Microsoft Scripting Guy, Ed Wilson, is here. Today, we have the conclusion of a three-part series written by PFEs Mark Morowczynski and Tom Moser.

Be sure to read the blog for Friday and Saturday first if you have not already done so.

Mark Morowczynski (@markmorow) and Tom Moser (@milt0r) are Premier Field Engineers (PFE) out of Chicago and Metro Detroit, respectively. They, with a group of contributing platform PFEs, write on the AskPFEPlat blog, covering a wide range of operating system, Active Directory, and other platform-related topics. Tom is a Red Wings fan. Mark is a Hawks fan. To date, no physical altercations have occurred due to this work conflict.

Take it away, Mark and Tom …

Well, it is now time to take a look at the CheckADMDate function.

Function: CheckADMDate

On the function home stretch here, we’ve got CheckADMDate. This function takes in a few parameters: $ADMDateStamps, $ADMName, and $Date, the latter two being typed as strings.

You may recall that near the top of the script, we defined $ADMDateStamps by using Import-CSV on a comma-separated list of names and dates. This function simply iterates through that array of objects we created and uses a Where-Object cmdlet to check to see if the ADM file name and date match the name and date of the specified ADM.

$match = $ADMDateStamps | Where { $_.ADM -eq $ADMName -and $_.DateStamp -eq $date }


if($match -eq $null)


    return $false


return $true 

If no match is found, then $match will not be created, meaning it will be equal to null. The function returns false. This indicates that the ADM template name and date don’t match any of the out-of-box ADMs and dates. Otherwise, it returns true.

The purpose of this check is to prevent people from deleting out-of-box ADMs that have been modified at some point along the way. It also helps to verify that somebody didn’t go and create an ADM called “system.adm” that doesn’t actually match the new out-of-box ADMX templates.

You can download the above function from the Script Repository.

Function: RestoreADMFiles

Finally, RestoreADMFiles is the function that seemed most important to people as we talked to our various Premier customers during ADRAPs, GPO Health Checks, and other PFE offerings. This function takes in a single parameter, $logPath. This variable contains the path to the CSV log file that was created when removing the ADM templates. Everything else needed to restore is in the log.

We use Import-CSV to read the log and covert it to an object array. The log contains five columns: TemplateName, FullPath, ADMXInCentralStore, BackupLocation, and NoMatch. When we get to the body of the script, we’ll go over those columns. The restore function utilizes BackupLocation and FullPath.

After using Import-CSV to read in the CSV log and create an object array based on the contents, we iterate through the collection, item by item, and simply copy the file from the backup path to the original ADM location, assuming the backup location isn’t empty, for some reason. 

foreach ($entry in $logData)


if($entry.BackupLocation -ne “”)


           copy-item $entry.BackupLocation -destination $entry.FullPath   



             “Restored $($entry.FullPath)”




 Like before, we use $? to verify that the restore was a success, then notify the user.

The script body

We’ve made it.

The first thing that happens in the script body is to check to see if the user has specified the –Restore switch. If they have, we call RestoreADMFiles, passing in the log path. Once the function is complete, the script exits.

Afterward, assuming the script has progressed that far, we call IsValidPolicyDefinitionPath, passing in the $PolicyDefinitionPath as the parameter. If the function return false, the user receives output saying that the path is invalid (whether it be the GP central store or local path) and the script exits. If the –NoCentralStore switch is not used when running the script, this function effectively verifies that the central store has actually been created.

Next, we use the Split-Path cmdlet on the $logPath variable that the user specified and store the result in a variable called $logDirectory. Split-Path can be given a full file name and will split the directory and file, returning just the directory. This is much quicker than attempting to use the –split operator or .NET Framework String.Split() method to separate a directory and a file.

$logDirectory = Split-Path $logpath

Once we’ve got the directory, we verify that it exists and create it if it doesn’t.

At this point, we’ve successfully bound to Active Directory, created all of the necessary paths and files, checked all of the inputs, and it’s time to get to business. So, we write the log file header:

set-content $logpath “TemplateName,FullPath,ADMXInCentralStore,BackupLocation,NoMatch”

Then, build the hash table containing information about all of the ADMX templates stored in the PolicyDefinitions folder:

$ADMXTemplates = GetADMXTemplateHashTable($PolicyDefinitionPath)

Run GetADMTemplates to do the recursive Get-Childitem on sysvol:

$ADMTemplates = GetADMTemplates($GPTPath)

Check to see if we actually found any and return a message if we didn’t:

if($ADMTemplates -eq $null)


    write-host -fore ‘yellow’ “No ADM Templates found.”



Then, we begin to iterate through all of the discovered ADM templates in a big foreach loop:

foreach ($ADMTemplate in $ADMTemplates)

The first thing we do in the loop is split the ADM name from the .ADM file extension, and then store the result in $TemplateName.

$TemplateName = $ADMTemplate.name.split(‘.’)[0]

if($TemplateName -eq “WUAU”) { $TemplateName = “WindowsUpdate” }

if($TemplateName -eq “wmplayer”) { $TemplateName = “WindowsMediaPlayer” }

if($TemplateName -eq “system”) { $TemplateName = “windowsfirewall” }

In the last three lines in the snippet above, we check $TemplateName and reset the value if it’s equal to WUAU, wmplayer, or system. This is because the new ADMX templates have different names or they’re a bit less monolithic.

After, we enter a boolean switch statement:


For the condition, we simply append .admx to the $TemplateName variable and go back to the hash table we created earlier. The beautiful thing about the hash table is its quick, dictionary-like lookups. If I want to know that the WindowsUpdate.admx file exists in the central store, I only need to run:


This will quickly do the lookup and return a true/false value. No need to repeatedly loop through the result of a Get-Childitem that has been stored in a variable or some other kind of object.

Assuming this returns true, the script falls to the $true case. 

$true that.

Inside the $true case, the script evaluates the $NoDateCheck variable. If it’s not equal to true, meaning the user did not set the switch when running the script, the script runs the CheckADMDate function for each ADM that was found.

if((CheckADMDate -ADMName $ADMTemplate.Name -Date $ADMTemplate.LastWriteTime.toshortdatestring() -ADMDateStamps $ADMDateStamps) -eq $false)


“$(($ADMTemplate).FullName) does not match any timestamps from Windows Server 2003 or Windows XP or any service packs. Verify that it has not been modified.” #wrapped from above



CheckADMDate is called, and the current ADM name, ADM last modified date, and $ADMDateStamps array (remember Import-CSV from the beginning?) are passed in to the function. If we find the name and date in the list, the function returns true and the script continues on. If it isn’t found, we notify the user that it was not found, and then use the continue keyword to break out of the current iteration in the for loop.

This means that processing for the current ADM template stops at line 318 in the script, and we loop back around to the next one. This check prevents the ADM from being removed if there’s a date mismatch.

$whatIf I don’t say –WhatIf? 

if($whatIf -eq $false)


BackupAndRemoveADM -ADMTemplatePath $ADMTemplate.FullName -BackupPath $BackupPath -BackupTarget $backupTarget                       


These four lines are the payload. If the user didn’t specify –WhatIf on the command line (because when I test, I test in prod), this expression will evaluate to $false. The function BackupAndRemoveADM is called, passing in the ADM’s full path, the backup location, and the explicit backup target (that path that contains the GUID). The script makes a copy, deletes the original, and continues on to:

add-content $logpath “$(($ADMTemplate).Name),$(($ADMTemplate).FullName),True,$($backupTarget)$(($ADMTemplate).Name)”                     

Where it logs ADM name, full path, true (meaning the ADMX was in the central store), and backup location.


The last few lines of our script contain the $false case from our switch statement. If the switch statement evaluated to false, that means we didn’t find a matching ADMX template for our ADM template. The ADM file is not removed, and we write a message to console indicating that. Finally, we write to the log file:

add-content $logpath “$(($ADMTemplate).Name),$(($ADMTemplate).FullName),False,,True” 

This appends another line to the log file, containing just the ADM name, the full path, then the values false, null, and true. The False value indicates that the script was unable to locate the matching ADMX in the central store. The backup location is left null because the script didn’t actually remove anything, and the last value, NoMatch, is set to True since no match was found in the central store.

Wrap up

At this point, we hope you’ve got a good understanding of how this script works and the techniques we used to allow admins to safely remove and restore their ADM templates. We hope that you, or your Active Directory administrators, will feel comfortable removing the space consuming ADM templates with the assistance of our script. Keep an eye on the Script Center for future updates. Writing this post has given us a few more ideas for the future.

~Tom and Mark

Thank you, Tom and Mark.

That’s it for this week. Join us tomorrow for some more Windows PowerShell fun.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

Comments (2)

  1. Anonymous says:

    thank you

Skip to main content