2. Save Your Standards from Becoming Rarely Used Checklists; Codify Them

Whether you are talking about manual processes or automation, standards are critical: They’re used to ensure everyone is on the same page and help guarantee that common tasks and implementations are consistent.

Imagine the chaos that would ensue if five different technicians deployed a domain controller each, into an existing Active Directory domain without any defined standards. Each server would have a unique flavor. Different firewall ports are open if the firewall is even enabled. Only three of them have your usual suite of support tools stored locally. There is a mix of both Server Core, and Desktop Experience installs. You eventually find out that one technician even deployed a Windows Server 2012 R2 image.

If only you had a standard that dictated precisely how domain controllers were to be deployed and configured in your environment.

The Sad Fate of Most Standards

As crucial as standards are, you inevitably find that they become glorified checklists. If you’re lucky, the wider team knows that these standards exist; if you’re even luckier, the team follows them.

Unfortunately, teams only forget the standards if those standards are not ingrained into your workplace culture. You may come across a new domain controller that doesn’t match the defined standard. When asked, the technician that deployed it reports that they didn’t even know that the standard existed.

Other than ongoing A Clockwork Orange-style reconditioning sessions, how can you ensure that your team follows the standards?

The answer—much like automation in general—is to remove the human element.

If you can codify your standards, then you can audit that the team is following them. Once you have made that leap, you can then take your codified standards and use them to help automate compliance.

Introducing the Requirements Module

Requirements is a PowerShell Module that is available on the PowerShell Gallery. Its purpose is to be a framework for defining and enforcing system configurations.

Hearing that description, you might rightly wonder if Microsoft has renamed Desired State Configuration (DSC). It hasn’t, but there is some overlap between the Requirements module and DSC. The main difference between the two is that DSC is built for massive operations, applying many different configurations across numerous systems at the same time. On the other hand, Requirements applies a single configuration against one system at a time.

If you have already invested in learning DSC, you don’t have to throw away what you know to use Requirements. In fact, you can use DSC resources with Requirements, although this means that your Requirements configuration will now depend on DSC’s configuration manager.

The Requirements module is open source, and you can contribute to the project by visiting the codebase on GitHub.

Requirements Primer

At a fundamental level, you use the Requirements module by defining a collection of, well, “requirements.”

You can define these requirements as a hashtable containing three elements:

  • Describe: The human-readable summary of a given requirement;
  • Test: Code that determines if the system meets the requirement;
  • Set: Code that puts a system into a state which satisfies the requirement.

It is possible to leave out the set component if you only want to audit if a system meets your standards. If you happen to know what idempotence is, you can also leave out the test component, but this chapter won’t be covering that concept.

The following example illustrates a sample standard represented with the Requirements module:

$Requirements = @(
  @{
    Describe = 'Notepad is running'
    Test = {
      $null -ne (Get-Process -Name 'notepad' -ErrorAction SilentlyContinue)
    }
    Set = {
      Start-Process -FilePath 'notepad.exe'
    }
  },
  @{
    Describe = 'The Requirements module is installed'
    Test = {
      $null -ne (Get-Module -Name 'Requirements' -ListAvailable)
    }
    Set = {
      Install-Module -Name 'Requirements' -Force
    }
  }
)

This example tests to see if Notepad is running and starts the process if not. It then tests the installation status of the Requirements module itself, which would pass, since it is running the test. For completeness, if the second test fails, the code within the Set element will install the missing module.

If you only want to audit this set of requirements, you would use the Test-Requirement function. To make the output from this execution easier to digest, you can pipe the output to Format-Table.

$Requirements | Test-Requirement | Format-Table State, Result, Requirement
State Result Requirement
----- ------ -----------
Start        Notepad is running
 Stop False  Notepad is running
Start        The Requirements module is installed
 Stop True   The Requirements module is installed

This example output shows that Notepad is not running, indicated by the False result. As expected, Test-Requirement detected the Requirements module, and there is a corresponding True result.

Because this is only testing the requirements, Test-Requirement makes no changes to bring the system into compliance.

To enforce these requirements, you will instead use the Invoke-Requirement function. You can also use an included formatting function, Format-Checklist, to get dynamic output as the requirements are tested and enacted.

$Requirements | Invoke-Requirement | Format-Checklist
√ 12:11:21 Notepad is running
√ 12:11:22 The Requirements module is installed

In this output, the √ character indicates that the test has passed. If you run this yourself, you see that the output is also colored. While a given test runs, its line is a neutral color with no icon and is then updated based on the success or failure of each test.

When invoking your set of requirements, Invoke-Requirement executes them in order. It tests the first requirement, and if the test fails, the Set code runs followed by the tests being run again to validate that the change was effective. If the test passes in the first instance or the change was successfully validated, then the next requirement in the set begins processing.

The Lab

To follow along with the demonstration in this chapter, you need a fresh install of Windows Server 2019 Standard. You can run this in a virtual machine (VM) on Hyper-V on your workstation, or in your cloud provider of choice.

If needed, you can download the ISO for this server from the Microsoft Evaluation Center.

Aside from setting your administrator password, you also also need to install the Requirements module on your new server.

Install-Module -Name Requirements

If running Windows Server in a VM, you may want to take a snapshot/checkpoint before carrying out the demonstration. Having a snapshot/checkpoint allows you to quickly roll back changes that are made without needing to undo changes manually or reinstalling the entire operating system.

Demo: From Standards to Requirements

This demonstration takes you from zero to hero—or at least competent—with regards to using the Requirements module. Take note that there is a lot more to this module than what one chapter can cover. The GitHub Repository is an excellent resource if you want to learn more.

The Standard

Your company is deploying several instances of Windows Server 2019. There are some essential configuration elements you need to keep consistent across all of these instances, so you define the following standard:

  • The default computer name must not be used;
  • The time zone must be set to UTC;
  • CD/DVD drive must be on drive letter B;
  • The C drive must be labeled as “OS”;
  • The NTFSSecurity PowerShell module should be installed.

Imagine that you have typed this up on the proper template, and management has signed off on it. You have circulated the standard through all proper channels to the technicians that should follow it.

To your shock, you come across several new installs that don’t follow—or only partially follow—the standard.

Testing the Standard

Armed with your defined standard, you can start working towards codifying and testing it as a set of requirements. The best way to get started is to focus on each element of your standard individually, addressing only one at a time before moving onto the next.

This demo walks you through building out your requirements, but remember that you group these in a collection, which is the first thing that is specified.

$Requirements = @(
  # Include your individual requirements here.
)
Test: The Default Computer Name Should Not Be Used

Generally, if you wanted to see the name of the computer you are using, you might use an environment variable, $env:COMPUTERNAME. While this returns the computer’s current computer name, you need to think forward to enforcing the standard. When you change a computer’s name, that environment variable doesn’t update until after a reboot, but you need to check that your change is effective before that.

Luckily, you can find a computer’s name—changed or not—in the registry.

@{
  Describe = "The default computer name should not be used"
  Test = {
    $Reg = "HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName"
    (Get-ItemProperty -Path $Reg -Name "ComputerName") -notmatch 'WIN-[\w]+'
  }
},

Notice that the Describe element is copied directly from your standard, as it’s already written for human consumption.

The Test element starts by defining where in the registry to find the computer name. Next, it receives the current computer name and checks that it does not match a pattern.

This pattern is a Regular Expression (RegEx), and it looks for a string that starts with “WIN-“ followed by one or more numbers or letters. This happens to match the automatically generated names used when installing Windows Server.

Test: The Time Zone Should Be Set to UTC

Time zones are a more straight forward setting than the computer name.

You can run Get-TimeZone before and after changing a system’s time zone setting and get the correct information right away.

@{
  Describe = "The time zone should be set to UTC"
  Test = { (Get-TimeZone).Id -eq 'UTC' }
},

This time you are checking to see if the current time zone’s ID is equal to ‘UTC’. This logic is similar to an if statement, and results in either a True or False being returned.

Test: CD/DVD Drive Should Be on Drive Letter B

Depending on how you deploy your virtual servers, you may find that they don’t have any optical drives. With that in mind, this test needs to succeed if there is no drive to be assigned the desired drive letter.

@{
  Describe = "CD/DVD drive should be on drive letter B"
  Test = {
    $Drive = Get-CimInstance -ClassName Win32_Volume -Filter 'DriveType = 5'
    $null -eq $Drive -or $Drive.DriveLetter -eq 'B:'
  }
},

Here, you retrieve the optical disk by selecting anything volumes with a drive type of 5 (Compact Disk). When testing this, you first check to see if the drive exists by comparing it against null. If there is no optical disk, the test passes.

Assuming there is a disk, you then check to see if its drive letter is equal to “B:” and the test fails if it isn’t.

Test: The C Drive Should Be Labeled as “OS”

You’ve already run tests on one type of disk, and the test for the C drive is very similar. This time you get all disks assigned the drive letter C and then check whether its label is equal to “OS.”

@{
  Describe = "The C drive should be labeled as 'OS'"
  Test = {
    $C = Get-CimInstance -ClassName Win32_Volume -Filter "DriveLetter = 'C:'"
    $C.Label -eq 'OS'
  }
},
Test: The NTFSSecurity PowerShell Module Should Be Installed

Your final test is regarding a PowerShell module that you want to ensure is available on all of your servers.

@{
  Describe = "The NTFSSecurity PowerShell module should be installed"
  Test = {
    $null -ne (Get-Module -Name NTFSSecurity -ListAvailable)
  }
}

To perform this test, you run Get-Module and specify the desired module by name. You must include the -ListAvailable switch so that you get the needed output even if PowerShell has not loaded the module into the current session.

You then check that output against $null using the “not equals” operator, meaning that you got something from Get-Module.

Running The Test

With all of your requirements written, add them all to your $Requirements collection and then pass them through Test-Requirement.

$Requirements = @(
  # .. snip ..
)

$Requirements | Test-Requirement | Format-Table State, Result, Requirement

On a fresh Windows Server 2019 install, the output won’t show much alignment with the standard.

State Result Requirement
----- ------ -----------
Start        The default computer name should not be used
 Stop False  The default computer name should not be used
Start        The time zone should be set to UTC
 Stop False  The time zone should be set to UTC
Start        CD/DVD drive should be on drive letter B
 Stop False  CD/DVD drive should be on drive letter B
Start        The C drive should be labeled as 'OS'
 Stop False  The C drive should be labeled as 'OS'
Start        The NTFSSecurity PowerShell module should be installed
 Stop False  The NTFSSecurity PowerShell module should be installed

Every test did not pass, meaning that each of your requirements is not yet satisfied.

Enforcing the Standard

Testing your standards is only half the battle. You could call your automation journey quits here and try to use the failing tests to get technicians to revisit their work. The better option is to build on your current requirements so that they can bring systems into compliance automatically.

This section follows the same format as the previous one on testing, including the same test elements that you saw previously.

Set: The Default Computer Name Should Not Be Used

The main problem when changing away from the default computer name assigned to your server is deciding what the new name should be.

If you have a particular naming scheme already, you should find some way of representing it in code. For this example, you’re changing the server name to LAB- followed by eight characters from a randomly generated globally unique identifier (GUID).

A potential new computer name is LAB-FD5C436F.

@{
  Describe = "The default computer name should not be used"
  Test = {
    $Reg = "HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName"
    (Get-ItemProperty -Path $Reg -Name "ComputerName") -notmatch 'WIN-[\w]+'
  }
  Set = {
    $NewName = "LAB-$(((New-Guid).Guid.split('-')[1,2] -join '').ToUpper())"
    Rename-Computer -NewName $NewName -Force
  }
},

This new name is applied using the Rename-Computer cmdlet, and you supply the -Force switch so that PowerShell does not prompt you to confirm this change.

Set: The Time Zone Should Be Set to UTC

Changing the time zone is instantaneous. If you watch the desktop clock on your server when Invoke-Requirement enforces these requirements, you may even catch the time change.

@{
  Describe = "The time zone should be set to UTC"
  Test = { (Get-TimeZone).Id -eq 'UTC' }
  Set = { Set-TimeZone -Id 'UTC' }
},

To enact the change, you call Set-TimeZone and specify the desired time zone.

Set: CD/DVD Drive Should Be on Drive Letter B

When testing for optical drives, remember that you stored any available drives in a variable. This variable is only available in the Test element and doesn’t flow through to the Set element. Not having this variable accessible means that you need to find your optical drive again rather than being able to reuse the existing variable.

@{
  Describe = "CD/DVD drive should be on drive letter B"
  Test = {
    $Drive = Get-CimInstance -ClassName Win32_Volume -Filter 'DriveType = 5'
    $null -eq $Drive -or $Drive.DriveLetter -eq 'B:'
  }
  Set = {
    $Drive = Get-CimInstance -ClassName Win32_Volume -Filter 'DriveType = 5'
    Set-CimInstance -InputObject $Drive -Property @{DriveLetter='B:'}
  }
},

The syntax for changing the drive letter involves passing your $Drive variable to Set-CimInstance, along with a hashtable containing any properties you want to change. For this requirement, we’re only changing the DriveLetter property, so that is the only property specified in the hashtable.

Set: The C Drive Should Be Labeled as “OS”

Like the previous Set element, you need to find your C drive again to make changes to it. Here you use Set-CimInstance again, but specify Label in the property hashtable, as that’s the only thing that needs to be changed.

@{
  Describe = "The C drive should be labeled as 'OS'"
  Test = {
    $C = Get-CimInstance -ClassName Win32_Volume -Filter "DriveLetter = 'C:'"
    $C.Label -eq 'OS'
  }
  Set = {
    $C = Get-CimInstance -ClassName Win32_Volume -Filter "DriveLetter = 'C:'"
    Set-CimInstance -InputObject $C -Property @{Label='OS'}
  }
},
Set: The NTFSSecurity PowerShell Module Should Be Installed

Finally, you need to make sure that technicians have installed the desired PowerShell module on the server.

To do this, you call Install-Module like you would when installing modules on your own computer. You use the -Force switch is to install the module without needing manual input.

@{
  Describe = "The NTFSSecurity PowerShell module should be installed"
  Test = {
    $null -ne (Get-Module -Name NTFSSecurity -ListAvailable)
  }
  Set = {
    Install-Module -Name 'NTFSSecurity' -Force
  }
}
Applying Requirements

With your definitions for the Requirements module now fully formed, it is time to use them to bring your server into compliance with your standard. Your final $Requirements collection looks like this:

$Requirements = @(
@{
  Describe = "The default computer name should not be used"
  Test = {
    $Reg = "HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName"
    (Get-ItemProperty -Path $Reg -Name "ComputerName") -notmatch 'WIN-[\w]+'
  }
  Set = {
    $NewName = "LAB-$(((New-Guid).Guid.split('-')[1,2] -join '').ToUpper())"
    Rename-Computer -NewName $NewName -Force
  }
},
@{
  Describe = "The time zone should be set to UTC"
  Test = { (Get-TimeZone).Id -eq 'UTC' }
  Set = { Set-TimeZone -Id 'UTC' }
},
@{
  Describe = "CD/DVD drive should be on drive letter B"
  Test = {
    $Drive = Get-CimInstance -ClassName Win32_Volume -Filter 'DriveType = 5'
    $null -eq $Drive -or $Drive.DriveLetter -eq 'B:'
  }
  Set = {
    $Drive = Get-CimInstance -ClassName Win32_Volume -Filter 'DriveType = 5'
    Set-CimInstance -InputObject $Drive -Property @{DriveLetter='B:'}
  }
},
@{
  Describe = "The C drive should be labeled as 'OS'"
  Test = {
    $C = Get-CimInstance -ClassName Win32_Volume -Filter "DriveLetter = 'C:'"
    $C.Label -eq 'OS'
  }
  Set = {
    $C = Get-CimInstance -ClassName Win32_Volume -Filter "DriveLetter = 'C:'"
    Set-CimInstance -InputObject $C -Property @{Label='OS'}
  }
},
@{
  Describe = "The NTFSSecurity PowerShell module should be installed"
  Test = {
    $null -ne (Get-Module -Name NTFSSecurity -ListAvailable)
  }
  Set = {
    Install-Module -Name 'NTFSSecurity' -Force
  }
}
)

Now you can enforce your standard by passing this collection to Invoke-Requirement and formatting the output using Format-Checklist.

Before executing this, you may want to change your $WarningPreference, as you get a warning about needing to restart the server for the new name to take effect. Typically this is fine, but a warning disrupts the checklist output.

$WarningPreference = 'SilentlyContinue'
$Requirements | Invoke-Requirement | Format-Checklist
√ 10:23:55 The default computer name should not be used
√ 10:23:57 The time zone should be set to UTC
√ 10:24:00 CD/DVD drive should be on drive letter B
√ 10:24:02 The C drive should be labeled as 'OS'
√ 10:24:21 The NTFSSecurity PowerShell module should be installed

Congratulations, your server is now entirely completely in compliance with your defined standard!

Wrap Up

The demonstration in this chapter involved running your requirements directly on the target server. If you want to scale this out a little, you could consider running it from your Remote Monitoring & Management tool, or your automation orchestrator of choice. You could even build this into your image so that it executes on the first login.

While Requirements is a potent tool, at larger scales, you should consider implementing PowerShell DSC instead.

If you’re hooked and want a challenge to test your knowledge of the Requirements module, try the following. Create a set of requirements that set up your workstation with the software you need and any personalizations you always make. The result gives you a repeatable tool you can use to speed up migrating to a new computer.

Now, go and save your essential standards from being forgotten forever!