A few weeks ago, an Iron Scripter PowerShell challenge was issued. This was a beginner to intermediate level challenge to get and set the registered user and/or organization values. These challenges, and solutions such as mine, aren't intended to production-ready tools. Instead, you should use them as learning vehicles to advance your PowerShell scripting skills. The concepts and techniques are more important than the actual result. Feel free to stop reading and try your hand at the challenge.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Read the Registry
The information for registered user and organization is stored in the registry. You can use Get-ItemProperty.
The cmdlet returns extra information, so you might want to be more selective.
Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows NT\CurrentVersion" -Name registered* |
Select-Object -property Registered*
Another option is to use Get-ItemPropertyValue to retrieve the value alone.
Get-ItemPropertyValue -Path "HKLM:\Software\Microsoft\Windows NT\CurrentVersion" -Name registeredowner,registeredorganization
With these ideas in mind, here is a simple function to get the necessary information from the local computer.
Function Get-RegisteredUserSimple {
Param()
$path = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion"
Get-ItemProperty -Path $path |
Select-Object -Property RegisteredOwner,RegisteredOrganization,
@{Name="Computername";Expression={$env:computername}}
}
Sometimes, the use of Select-Object can get in the way of clarity. That's why I tend to define custom objects like this:
Function Get-RegisteredUserBasic {
[cmdletbinding()]
Param()
$path = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion"
$reg = Get-ItemProperty -Path $path
[pscustomobject]@{
RegisteredUser = $reg.RegisteredOwner
RegisteredOrganization = $reg.RegisteredOrganization
Computername = $env:COMPUTERNAME
}
}
The output will be the same.
Set the Registry
What about setting new values? For that task we can use Set-ItemProperty. I'm trusting you'll read full help and examples for all of the commands I'm using. Here's my relatively simple function to set the owner and/or organization values.
Function Set-RegisteredUserBasic {
[cmdletbinding(SupportsShouldProcess)]
Param(
[Parameter()]
[alias("user")]
[ValidateNotNullOrEmpty()]
[string]$RegisteredUser,
[Parameter()]
[alias("org")]
[ValidateNotNullOrEmpty()]
[string]$RegisteredOrganization,
[switch]$Passthru
)
#registry path
$path = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion"
#a flag variable to indicate if a change was made
$set = $False
#Only set property if something was entered
if ($RegisteredUser) {
Write-Verbose "Setting Registered Owner to $RegisteredUser"
Set-ItemProperty -Path $path -Name "RegisteredOwner" -Value $RegisteredUser
$set = $True
}
#Only set property if something was entered
if ($RegisteredOrganization) {
Write-Verbose "Setting Registered Organization to $RegisteredOrganization"
Set-ItemProperty -Path $path -Name "RegisteredOrganization" -Value $RegisteredOrganization
$set = $True
}
#passthru if something was set
if ($set -AND $passthru) {
$reg = Get-ItemProperty $path
[pscustomobject]@{
RegisteredUser = $reg.RegisteredOwner
RegisteredOrganization = $reg.RegisteredOrganization
Computername = $env:COMPUTERNAME
}
} #if passthru
}
I've included a very intermediate-level features such as support for -WhatIf, some parameter validation and parameter aliases. Because I configured cmdletbinding to use "SupportsShouldProcess", I'll automatically get the WhatIf and Confirm parameters. Even better, any command that I call will automatically consume the parameter values. In my function, Set-ItemProperty supports -WhatIf. But I don't have to do anything special.
My code is using a -Passthru switch parameter to get value, if specified.
I don't have any real error-handling in this function. In order to modify the this part of the registry you need to be running PowerShell as administrator. If I wasn't, Set-ItemProperty would throw an exception, which I would be fine with. One thing I could do is require administrator access.
The functions have to live in a .ps1 file and then be dot-sourced. Let's leave modules out of the discussion. At the top of the .ps1 file I can add these require statements:
#requires -version 5.1
#requires -RunAsAdministrator
When the user dot sources the file, if they are not running as Administrator, PowerShell will throw an exception.
Taking the Next Step
My basic functions work fine when working with the localhost. And I think they would have met the basic requirements of the challenge. But the challenge had extra features. Let's look at those.
When using the registry provider as I am, this only works on the local computer. To access the registry remotely, you can use PowerShell remoting.
Invoke-Command -scriptblock {
Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows NT\CurrentVersion" -Name registered*
} -computername win10 -credential $artd | Select-Object -property Registered*,PSComputername
I can take this basic idea and turn it into an advanced PowerShell function. By the way, I always start with a command-line solution like this first. Once I have it working, then I can begin building a function around it. If you are new to PowerShell I recommend this approach.
Building a Remoting Function
Here's my get function.
Function Get-RegisteredUser {
[cmdletbinding()]
Param(
[Parameter(Position = 0, ValueFromPipeline)]
[alias("cn")]
[string[]]$Computername = $env:COMPUTERNAME,
[pscredential]$Credential,
[int32]$ThrottleLimit,
[switch]$UseSSL
)
Begin {
Write-Verbose "Starting $($MyInvocation.MyCommand)"
#a scriptblock to run remotely
$sb = {
$path = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion"
$reg = Get-ItemProperty -Path $path
[pscustomobject]@{
PSTypeName = "PSRegisteredUser"
RegisteredUser = $reg.RegisteredOwner
RegisteredOrganization = $reg.RegisteredOrganization
Computername = $env:COMPUTERNAME
}
}
$PSBoundParameters.Add("Scriptblock", $sb)
$PSBoundParameters.Add("HideComputername", $True)
}
Process {
#add the default computername if nothing was specified
if (-NOT $PSBoundParameters.ContainsKey("computername")) {
Write-Verbose "Querying localhost"
$PSBoundParameters.Computername = $Computername
}
Invoke-Command @PSBoundParameters | ForEach-Object {
#create a custom object
[pscustomobject]@{
PSTypeName = "psRegisteredUser"
Computername = $_.Computername
User = $_.RegisteredUser
Organization = $_.RegisteredOrganization
Date = (Get-Date)
}
}
} #process
End {
Write-Verbose "Ending $($MyInvocation.MyCommand)"
}
}
I've moved all of the registry reading code inside a scriptblock which I'll run with Invoke-Command. The function parameters and the same as Invoke-Command so I can splat the built-in $PSBoundParameters hashtable to the command. Although, you'll see that I'm tweaking it a bit to add parameters.
$PSBoundParameters.Add("Scriptblock", $sb)
$PSBoundParameters.Add("HideComputername", $True)
When the command runs remotely, it sends a serialized object back to me. I'm creating a custom object for each result.
Invoke-Command @PSBoundParameters | ForEach-Object {
#create a custom object
[pscustomobject]@{
PSTypeName = "psRegisteredUser"
Computername = $_.Computername
User = $_.RegisteredUser
Organization = $_.RegisteredOrganization
Date = (Get-Date)
}
}
Adding Formatting
You'll notice that I defined a typename for the custom object. This is so that I can create a custom format file.
Get-RegisteredUser |
New-PSFormatXML -path c:\scripts\registered.format.ps1xml -GroupBy Computername -Properties User,Organization,Date
New-PSFormatXML is from the PSScriptTools module. I can edit the xml file to meet my needs. In my case, I set specific column widths and formatted the Date value.
<?xml version="1.0" encoding="UTF-8"?>
<!--
Format type data generated 11/16/2020 11:32:51 by PROSPERO\Jeff
This file was created using the New-PSFormatXML command that is part
of the PSScriptTools module.
https://github.com/jdhitsolutions/PSScriptTools
-->
<Configuration>
<ViewDefinitions>
<View>
<!--Created 11/16/2020 11:32:51 by PROSPERO\Jeff-->
<Name>default</Name>
<ViewSelectedBy>
<TypeName>psRegisteredUser</TypeName>
</ViewSelectedBy>
<GroupBy>
<!--
You can also use a scriptblock to define a custom property name.
You must have a Label tag.
<ScriptBlock>$_.machinename.toUpper()</ScriptBlock>
<Label>Computername</Label>
Use <Label> to set the displayed value.
-->
<PropertyName>Computername</PropertyName>
<Label>Computername</Label>
</GroupBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths.
<AutoSize />-->
<TableHeaders>
<TableColumnHeader>
<Label>User</Label>
<Width>18</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Organization</Label>
<Width>20</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Date</Label>
<Width>10</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<!--
By default the entries use property names, but you can replace them with scriptblocks.
<ScriptBlock>$_.foo /1mb -as [int]</ScriptBlock>
-->
<TableColumnItem>
<PropertyName>User</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Organization</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>$_.Date.ToShortDateString()</ScriptBlock>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
I then import the file into my session.
Update-FormatData C:\scripts\registered.format.ps1xml
Now, when I run the function I get better formatted results.
Setting Remote Registry Values
I'll take a similar approach to setting the registry values remotely. I have to have admin access to connect remotely so I don't have to worry about not having access. My set function uses the same concepts and techniques as the get function.
Function Set-RegisteredUser {
[cmdletbinding(SupportsShouldProcess)]
Param(
[Parameter()]
[alias("user")]
[ValidateNotNullOrEmpty()]
[string]$RegisteredUser,
[Parameter()]
[alias("org")]
[ValidateNotNullOrEmpty()]
[string]$RegisteredOrganization,
[Parameter(ValueFromPipeline)]
[alias("cn")]
[string[]]$Computername = $env:COMPUTERNAME,
[pscredential]$Credential,
[switch]$Passthru
)
Begin {
Write-Verbose "Starting $($myinvocation.MyCommand)"
$sb = {
[cmdletbinding()]
<#
PowerShell doesn't serialize a switch type very well so I'll
make passthru a boolean. Although in this situation, instead of
passing parameters I could have referenced the variables from
the local host as I'm doing with -Verbose and -WhatIf
#>
Param(
[string]$RegisteredUser,
[string]$RegisteredOrganization,
[bool]$Passthru
)
$VerbosePreference = $using:verbosepreference
$WhatIfPreference = $using:WhatifPreference
#registry path
$path = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion"
Write-Verbose "[$($env:COMPUTERNAME)] Using registry path $path"
#a flag variable to indicate if a change was made
$set = $False
#Only set property if something was entered
if ($RegisteredUser) {
Write-Verbose "[$($env:COMPUTERNAME)] Setting Registered Owner to $RegisteredUser"
#define my own -WhatIf
if ($pscmdlet.ShouldProcess($env:COMPUTERNAME, "Set registered user to $RegisteredUser")) {
Set-ItemProperty -Path $path -Name "RegisteredOwner" -Value $RegisteredUser
$set = $True
}
}
#Only set property if something was entered
if ($RegisteredOrganization) {
Write-Verbose "[$($env:COMPUTERNAME)] Setting Registered Organization to $RegisteredOrganization"
if ($pscmdlet.ShouldProcess($env:COMPUTERNAME, "Set registered organization to $RegisteredOrganization")) {
Set-ItemProperty -Path $path -Name "RegisteredOrganization" -Value $RegisteredOrganization
$set = $True
}
}
#passthru if something was set
if ($set -AND $passthru) {
$reg = Get-ItemProperty $path
[pscustomobject]@{
PSTypeName = "PSRegisteredUser"
Computername = $env:COMPUTERNAME
User = $reg.RegisteredOwner
Organization = $reg.RegisteredOrganization
Date = Get-Date
}
} #if passthru
} #close scriptblock
$icmParams = @{
Scriptblock = $sb
ArgumentList = @($RegisteredUser, $RegisteredOrganization, ($Passthru -as [bool]))
HideComputername = $True
}
if ($Credential) {
Write-Verbose "Using credential for $($credential.username)"
$icmParams.Add("Credential", $credential)
}
} #begin
Process {
$icmParams.Computername = $Computername
Invoke-Command @icmParams | Select-Object -Property * -ExcludeProperty RunspaceID
#You could also create the custom object on this end as I did with Get-RegisteredUser
} #process
End {
Write-Verbose "Ending $($myinvocation.MyCommand)"
} #end
}
There are a few things I want to point out. Remember, the scriptblock is running remotely but I need to pass values from my local session. My code is demonstrating a few techniques.
First, the scriptblock is defined with parameters.
$sb = {
[cmdletbinding()]
<#
PowerShell doesn't serialize a switch type very well so I'll
make passthru a boolean. Although in this situation, instead of
passing parameters I could have referenced the variables from
the local host as I'm doing with -Verbose and -WhatIf
#>
Param(
[string]$RegisteredUser,
[string]$RegisteredOrganization,
[bool]$Passthru
)
Later in my code I'll pass the local values to the scriptblock.
$icmParams = @{
Scriptblock = $sb
ArgumentList = @($RegisteredUser, $RegisteredOrganization, ($Passthru -as [bool]))
HideComputername = $True
}
The other approach is to employ the $using: prefix. This is how I am setting -Verbose and -Whatif in the remote scriptblock.
$VerbosePreference = $using:verbosepreference
$WhatIfPreference = $using:WhatifPreference
In essence, I'm setting the remote preference variables to use the local values.
In my function, instead of passing WhatIf to Set-ItemProperty, I'm defining my own WhatIf code to provide better information.
if ($pscmdlet.ShouldProcess($env:COMPUTERNAME, "Set registered user to $RegisteredUser")) {
Set-ItemProperty -Path $path -Name "RegisteredOwner" -Value $RegisteredUser
$set = $True
}
I could follow the same process as with my get function and define a true custom object that could use the same formatting file. I'll leave that exercise for you.
Summary
I hope you'll try these functions out for yourself. Read the cmdlet help so that you understand why the code works. If you have any questions, please leave a comment. And I hope you'll keep an eye out for other Iron Scripter Challenges and try your hand.
By the way, if you are looking for other ways to test your PowerShell skills, especially if you are a beginner, you might want to grab a copy of The PowerShell Practice Primer.
1 thought on “Answering the PowerShell Registered User Challenge”
Comments are closed.