Local security policy using Pester

In my previous post, I’ve shown how to use DSC to configure the local security policy. If you only want to get the compliance status of the system, Pester might be more suitable.

I first create a hastable of settings like this:

# secedit.exe /export /Cfg C:\secpol.txt /areas SECURITYPOLICY
(Get-Content -Path 'C:\secpol.txt' -ReadCount 1) `
-match '^([A-Z\s0-9_\\]+)=(.*)$' -replace '=',',' |
ConvertFrom-Csv -Header Key,Value1,Value2 | ForEach-Object {
    '    @{'
    "        Key = '{0}'" -f $_.Key
    "        Value1 = '{0}'" -f $_.Value1
    if ($_.Value2) {
    "        Value2 = '{0}'" -f $_.Value2
    '    },'

and I populate the file secpol.ps1 using the output of the above command.
The content looks like this. The hashtable is stored inside an array named $SecurityPolicy.

Now in the test file secpol.tests.ps1, I’ve:

Let’s see how to use it

Invoke-Pester ~/documents/pester/secpol


Fine, my system is compliant.
But, let’s I change the Maximum password age using gpedit.msc

Now, Pester reports my system as not being compliant:
But, the only drawback with my quick’n dirty code is that it doesn’t tell you what value was found instead of the expected 42 in this case.

Audit policy using Pester

In my previous post, I’ve shown how to leverage DSC to apply a configuration that defines the local Audit policy.
If you’re just interested in the compliance of a system, Pester might be more suitable to assess it against a template. It will somehow validate the operational status of the system.

First, I create a hashtable of settings like this:

auditpol.exe /get /category:* /r |
ConvertFrom-Csv |
Select Subcategory*,*lusion* | 
ForEach-Object {
    '    @{'
    "        Name = '{0}'" -f $_.Subcategory
    "        GUID = '{0}'" -f $_.'Subcategory GUID'
    "        Inclusion = '{0}'" -f $_.'Inclusion Setting'
    '    },'

and I populate the file auditpol.ps1 using the output of the above command. The content looks like this. The hashtable is stored inside an array named $AuditPolicy.

Now in the test file auditpol.tests.ps1, I’ve:

Let’s see how to use it

Invoke-Pester ~/documents/pester/auditpol


If I change the Logoff policy for example like this:

auditpol /Set /subcategory:{0CCE9216-69AE-11D9-BED3-505054503030} /failure:enable

and run the pester test a second time, I’ll get:
…my system isn’t compliant anymore with the settings defined in my template.


PowerShell celebrated its 10th anniversary on Monday, November 14th.

To celebrate it, there was a live stream all day long that was announced on the PowerShell Team blog

If you missed it, no problem, you can go to channel9 an watch it using this link https://channel9.msdn.com/Events/PowerShell-Team/PowerShell-10-Year-Anniversary


The community demonstrated all day long how they use PowerShell. That was awesome!!! And Kenneth Hansen and Angel Calvo discussed Future Directions for PowerShell

There were also Code Golf holes to celebrate that day 😀

  • Code golf hole 1
  • I submitted the following answer and introduced a old trick (works in PS2.0) to subtract days to the current datetime object

    gcim(gwmi -li *ix*).Name|? I*n -gt((date)+-30d)

    and it passed the pester test
    Here’s what it does:

    • gwmi is the alias of the Get-WmiObject cmdlet.
    • -li is the shortest version of the -List parameter of Get-WmiObject.
    • -List allows wildcards when looking for WMI classes. So *ix* matches the Win32_QuickFixEngineering WMI class that the Get-Hotfix cmdlet queries.
    • gcim is the alias of the Get-CimInstance cmdlet.
    • (gwmi -li *ix*).Name returns Win32_QuickFixEngineering.
    • Now that we have the list of hotfixes as CIM instances we can filter on the right.
    • ? is the alias of Where-Object.
    • I*n is the short name of the InstalledOn property that is a datetime object.
    • So we can compare it to the current date minus 30 days.
    • We can omit Get- in Get-Date and just type (date).
    • To subtract 30 days we use the old trick (date)+-30d 😎

    A longer form would be

    Get-CimInstance (Get-WmiObject -List *ix*).Name |
    Where InstalledOn -gt (Get-date).AddDays(-30d)
    # or 
    Get-CimInstance Win32_QuickFixEngineering |
    Where InstalledOn -gt (Get-date).AddDays(-30d)
  • Code golf hole 2
  • I submitted the following answer that uses the -File switch parameter. Some answers submitted have a problem and may be broken when you change the path to another drive like HKLM: or Cert: for example. Mine is also somehow broken and works only if the console is started as administrator where the default path is set to C:\windows\system32.

    (ls c: -File|% E*n|group|sort c* -d)[0..9]

    …but it passed the pester test
    Anyway, here’s how to decode it:

    • ls is the alias of the Get-ChildItem cmdlet.
    • ls c: -File will return only files including those that don’t have an extension in system32.
    • % is the alias of the ForEach-Object cmdlet.
    • E*n is the short name of the Extension property of items returned by Get-ChildItem.
    • Group is the short version of the Group-Object cmdlet. We can usually omit the -Object (Noun) for cmdlets that deal with -Object except for the New-Object cmdlet.
    • sort is the alias of the Sort-Object cmdlet.
    • c* is the short name of the Count property returned by Group-Object.
    • -d is the short name of the -Descending switch parameter of the Sort-Object cmdlet.
    • To get only the first 10, we enclose everything in parentheses to treat it as an array and then we enumerate the elements in the array using the [0..9] notation.

    A longer form would be

    (Get-ChildItem -Path c: -File | ForEach-Object { 
    } |Group-Object | 
    Sort-Object -Property Count -Descending)[0..9]
  • Code golf hole 3
  • For the 3rd hole, I submitted the following solution 😎

    gal ?,?? -e h,g?,?s

    and it passed the pester test
    Here is how to read it:

    • gal is the alias of the Get-Alias cmdlet.
    • Get-Alias uses by the default the -Name parameter and it accepts an array of strings and wildcards.
    • * represents all/any characters and ? only one character (it’s the same in DOS) and ?? represents two characters.
    • -e is the short name of the -Exclude parameter of the Get-Alias cmdlet.
    • -Exclude also accepts an array of strings and wildcards.
    • To avoid aliases for Get- cmdlets, we explicitly exclude h, the alias of the Get-History cmdlet, all the aliases for Get- cmdlets that begin by g and followed by a second letter like gi (Get-Item), gc (Get-Content),…, and finally the last two Unix aliases of the Get-Process cmdlet, ps, and the Get-ChildItem cmdlet, ls.

    A longer form would be

    Get-Alias -Name ?,?? -Exclude h,gc,gi,gl,gm,gp,gu,gv,ps,ls

Security policy and DSC

When I showcased DSC to our security team, I also built another wrapper of secedit.exe.
I took the same quick’n dirty approach as the audit policy DSC script from my previous post. Again, only a File and a Script DSC resources are involved in the configuration.

Note that there’s also a limitation in my code.
Secedit.exe can handle more than just the local security policy.
There are other areas it can cover: restricted group settings, user logon rights,

To get the security baseline I first exported the local security policy to a file like this:

secedit.exe /export /Cfg C:\secpol.txt /areas SECURITYPOLICY

… and I copied/pasted the content of the resulting C:\secpol.txt into to Content property of my File resource.

Audit policy and DSC


It can be found on the powershell gallery and/or github

Now that Microsoft has published a full module for this purpose, I can actually show you the quick’n dirty way I coded it a few months ago when I needed to showcase DSC to our internal security team.

It only uses native File and Script DSC resources. In other words, there’s no dependency on any external DSC resource 🙂

First to get the content of the CSV file we’ll drop on the disk and that represents our desired settings, I do

auditpol.exe /get /category:* /r |
ConvertFrom-Csv |
Select Subcategory*,*lusion* | 
Export-Csv -Path ~/Documents/polaudit.csv

Then I paste the content of the polaudit.csv into the Content property of the File resource.

The Get and Test part of the script DSC resource use the same trick above to get the output of our brave old legacy (heritage) auditpol.exe as objects:

How to create UEFI bootable USB media to install Windows Server 2016

Since Windows Server 2016 has been released, I grabbed the RTM ISO file and wanted to install a new server using a USB stick.

I configured the BIOS settings of the server to only boot UEFI and disabled the legacy boot.

Some of the key points to bear in mind:

  • A USB stick with more than 5.3GB is required
  • UEFI requires a FAT32 partition
  • FAT32 has some serious limitations and the size of the install.wim file exceeds those limits. This file requires therefore to be split into multiple more suitable parts

More on this here: https://blogs.technet.microsoft.com/askcore/2013/03/20/creating-bootable-usb-drive-for-uefi-computers/

Here’s how I created my USB boot media compatible with UEFI using PowerShell:

Inside the Nuget bootstraping process

A few days ago the PowerShell Team announced on their blog that PowerShellGet has been open-sourced. Both PowerShellGet and PackageManagement modules are now available on the PowerShell Gallery to more easily consume/update these modules and on github to contribute to these projects:

The article says

PowerShellGet has a dependency on PackageManagement.

That’s true.
I’d add that you cannot use the Find-Module cmdlet from the PowerShellGet module before the PackagementManagement module installed the Nuget provider.

Do I have Nuget listed as a provider?

# You can either do
# and see if Nuget is in the list
# or
Get-PackageProvider | Where Name -eq 'NuGet'


But, as soon as you do

Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue


…or if you use the Find-Module cmdlet, you’ll be asked to complete the Nuget provider installation.

Find-Module -Name Pester -Repository PSGallery


Did you notice that the messages don’t say exactly the same thing and that one proposes to install a minimum version and the other
The Get-PackageProvider from the PackageManagement module seems more accurate than the Find-Module cmdlet from the PowerShellGet module.
Well, because the PowerShellGet module hardcodes the version

…whereas the PackageManagement module built the version on the fly:

After opening the PSModule.psm1 file located in C:\Program Files\WindowsPowerShell\Modules\PowerShellGet\, I can notice that many functions inside that module call either:

# the BootstrapNuGetExe switch 
# used by the Publish-Module function
Install-NuGetClientBinaries -CallerPSCmdlet $PSCmdlet `


Install-NuGetClientBinaries -CallerPSCmdlet $PSCmdlet

The reason is explained on this page: What’s the difference between nuget-anycpu.exe, Microsoft.PackageManagement.NuGetProvider.dll and nuget.exe?

nuget.exe is used by PowerShellGet to publish packages
Microsoft.PackageManagement.NuGetProvider.dll is used by OneGet and PowerShellGet to discover and install packages.
Only the Publish-Module cmdlet of PowerShellGet will require nuget.exe.

So far, we’ve seen that there are two components that can be downloaded and installed.
According to the content of the PSModule.psm1 file, Nuget.exe is downloaded by the PowerShellGet module and uses the following URL to get it:

# go fwlink for 'https://nuget.org/nuget.exe'
$script:NuGetClientSourceURL = 

Where does Microsoft.PackageManagement.NuGetProvider.dll come from?

Microsoft proposes in their recent blog post to do the following to install the Nuget provider:

Install-PackageProvider -Name Nuget –Force –Verbose

The same piece of information is also mentioned in the FAQ of the PackagementManagement module on github: How do I install a package provider such as NuGet provider if I do not have Internet connection on my box?

Again inside the the PSModule.psm1 file (the PowerShellGet module), its internal Install-NuGetClientBinaries function actually launches the following to install the Nuget provider:

PackageManagement\Install-PackageProvider -Name 'Nuget' `
-MinimumVersion ([Version]'')  `
-Scope $scope -Force


Instead of the above command, I propose to do directly the following and add the Debug switch to uncover what happens behind the scene:

Get-PackageProvider -Name NuGet `
-ErrorAction SilentlyContinue -Verbose -Debug

The following hardcoded URL is immediately used: https://go.microsoft.com/fwlink/?LinkID=627338&clcid=0x409


Let’s examine this URL and set the MaximumRedirection to 0 to avoid any redirection.

$HT = @{
 Uri = 'https://go.microsoft.com/fwlink/?LinkID=627338&clcid=0x409'
 MaximumRedirection = 0
 ErrorAction = 'SilentlyContinue'
$req = Invoke-WebRequest @HT
# the page has moved
# to this location

We can see the page is redirected to another URL:

The file downloaded from https://az818661.vo.msecnd.net/providers/providers.masterList.feed.swidtag is a swidtag file (Software Identiy Tag) written in XML:

Now, to get the latest version of the Microsoft.PackageManagement.NuGetProvider.dll file, I need to read the content of the providers.masterList.feed.swidtag XML file:

# set a variable
$MasterSwidTagURI = 'https://az818661.vo.msecnd.net/providers/providers.masterList.feed.swidtag'

# Get the link of the Nuget provider
(Invoke-WebRequest -Uri $MasterSwidTagURI -MaximumRedirection 0 -ErrorAction SilentlyContinue).Content
))).SoftwareIdentity.Link | Where {
    $_.rel -eq 'package' -and
    $_.latest -eq 'true' -and
    $_.name -eq 'nuget'
} | Select -expand href

# or more simply using the Invoke-RestMethod cmdlet
(Invoke-RestMethod $MasterSwidTagURI).SoftwareIdentity.Link | 
Where {
    $_.rel -eq 'package' -and
    $_.latest -eq 'true' -and
    $_.name -eq 'nuget'
} | Select -expand href


To get the latest version of the dll, there’s a specific swidtag file to read from the following URL: https://oneget.org/nuget-

Let’s examine the content of this new XML file:

$dllURI = 'https://oneget.org/nuget-'
# Get the version
(Invoke-RestMethod -Uri $dllURI).SoftwareIdentity.version
# Get the source (dll download location)
(Invoke-RestMethod -Uri $dllURI).SoftwareIdentity.Link.href
# New info about the file to download
(Invoke-RestMethod -Uri $dllURI).SoftwareIdentity.Payload.File


Now, we know how and from where the file is being downloaded.

As of version, there’s a hash being used by the Install-PackageProvider cmdlet to check the integrity of the downloaded Microsoft.PackageManagement.NuGetProvider.dll file.
If the Install-PackageProvider is invoked with the Debug switch, I can see at the end of the debug stream the following line: DEBUG: 00:02:07.7019092 BoostrapRequest::ValidateFileHash

The above hash that appears inside the swidtag file doesn’t look standard.

Here’s how it can be verified:

# Download the dll
Invoke-WebRequest -Uri $((Invoke-RestMethod -Uri $dllURI).SoftwareIdentity.Link.href) -OutFile ~/downloads/$((Invoke-RestMethod -Uri $dllURI).SoftwareIdentity.Payload.File.Name) -Verbose

# Get its SHA512 hash
(Get-FileHash ~/downloads/Microsoft.PackageManagement.NuGetProvider.dll -Algorithm SHA512 | Select -Expand Hash).ToLower()

# Check it matches the one specified in the swidtag file
   (Invoke-RestMethod -Uri 'https://oneget.org/nuget-').SoftwareIdentity.Payload.File.hash
 ) -replace '-',''