November 2024 PowerShell Potluck
I'm back with another PowerShell Potluck! This month, I have a variety of topics to share with you. I have a new module for working with Bluesky, a new function for encrypting environment variables, a custom history formatting function, and more. I also have a scripting challenge for you to try out. Let's dig in!
PSBluesky
For all practical purposes, I have abandoned X, as many others have. I have moved my primary social media presence to Bluesky. One of the things I like about this platform is the open and public API. Bluesky is built on top of the AT Protocol and is well documented. I have created a new PowerShell module called PSBluesky
that you can use to interact with Bluesky directly from a PowerShell prompt. The module requires PowerShell 7 and can be installed from the PowerShell Gallery.
Install-Module PSBluesky
Before you can use the module, you need to create a Bluesky session with a credential. I'm recommended people use an app password. You can look at the README file in the module's GitHub repository for more information, or if you have installed the module run Open-BskyHelp
to open a PDF version of the file.
Run Get-BskyModuleInfo
to see a list of available module commands and aliases.
PS C:\> Get-BskyModuleInfo
Module: PSBluesky [v2.1.0]
Name Alias Synopsis
---- ----- --------
Add-BskyImage Upload an image to Bluesky
Find-BskyUser bsu Search for Bluesky user accounts
Get-BskyAccountDID Resolve a Bluesky account name to its DID
Get-BskyFeed bsfeed Get your Bluesky feed
Get-BskyFollowers bsfollower Get your Bluesky followers
Get-BskyFollowing bsfollow Get a list of Bluesky accounts that you follow
Get-BskyModuleInfo Get a summary of the PSBlueSky module.
Get-BskyNotification bsn Get Bluesky notifications.
Get-BskyProfile bsp Get a Bluesky profile
Get-BskySession bss Show your current Bluesky session.
Get-BskyTimeline bst Get your Bluesky timeline
New-BskyPost skeet Create a Bluesky post
Open-BskyHelp bshelp Open the PSBluesky help document
Start-BSkySession Start a new Bluesky session
Update-BskySession Refresh-BskySession Refresh the Bluesky session token
Once you've run Start-BskySession
, you can use other module commands.
The module uses custom formatting and offers features like clickable links.
You can also post a message right from a PowerShell prompt, complete with tags, mentions, and links.
Skeet "I'm writing about the PSBluesky module in the monthly newsletter wrapup. #PowerShell" -Verbose
The module includes custom verbose messaging.
I'm having fun with this module and have even more features planned. Even if you are not a Bluesky user, you might want to look at the code to see how I am building tools around a REST API.
New-EncryptedEnvironmentVariable
Here's an item that came across my Bluesky feed from Stephen Valdinger, more commonly known as @steviecoaster in PowerShell circles. He has posted a function that lets you create an encrypted environment variable.
The function uses the native .NET data protection APIs to create a variable with a secure string value. When you create the variable, you also need to specify the environment scope:
New-EncryptedEnvironmentVariable -Name zSecret -Value BananaMonkey246 -Scope machine
This will create a persistent environment variable in the machine scope. I created this in a PowerShell 7 session.
PS C:\> $env:zSecret
System.Security.SecureString
Because this is an environment variable, I can access it in a different PowerShell session, such as this Windows PowerShell session.
PS C:\> $env:zSecret
01000000d08c9ddf0115d1118c7a00c04fc297eb010000000728496af3d3f946bfe05138d1391c1e00000000020000000000106600000001000020000000be5e6034f844605dfed25dbec739971f6f0ca5433e4d6e79edf56d4b1f728724000000000e8000000002000020000000eb41e9c1bb3f27e28f265935db9b1d23d98778ed75cbd049a79fcab63c9a642a2000000068689c12c24ccf403b1cd1804b6c7d20595238ad447842dc16daaf13d74532ba400000008afe8788627110465842706eb7ed4ba4fb21fc9c93efb35ca3c5fd8f35467c5dc107d389f82e6058e568fe3bc2acb66528429fabead747fce1e73739764dbc6c
PS C:\>
Here I get the secure string value. To use it, I can convert it back to a secure string:
$ss = $env:zSecret | ConvertTo-SecureString
I'll can verify the value by creating a credential object with the secure string and then resolving it with the GetNetworkCredential()
method.
PS C:\> $cred = [PSCredential]::new("foo",$ss)
PS C:\> $cred.getNetworkCredential().Password
BananaMonkey246
This kind of persistence only works if you specify User
or Machine
as the scope. Any Process
scoped variables will be lost when the session ends. Remember, the secure string can only be decrypted on the same machine and user account that created it.
If you find a use for this function, I'd love to hear about it.
History Custom Formatting
I use the Get-History
cmdlet all the time. One thing I like in PowerShell 7 is that it shows you how long the command took to complete.
PS C:\> Get-History -count 3
Id Duration CommandLine
-- -------- -----------
155 1:02.276 $l = get-Winevent -ListLog * -ComputerName thinkx1-jh
156 0.003 get-history 1
157 0.030 get-history
But I wanted to see a little more information, so I wrote a custom formatting file using New-PSFormatXML
. I have shared the file as a gist on GitHub. The file uses $PSStyle
so it requires PowerShell 7. After you've downloaded the file, import it into your session.
Update-FormatData -AppendPath c:\scripts\history.format.ps1xml
This gives you a new table view.
Get-History -count 10 | Format-Table -View time
If I always want this view available, I can add the Update-FormatData
command to my PowerShell profile script.
More Validation Error Messages
I've spent time recently in the newsletter demonstrating parameter validation techniques. One of the reasons I like [ValidateScript()]
in PowerShell 7 is the ability to specify a custom error message. It turns out, this feature is available for a few other validation attributes as well.
It is supported in [ValidateSet()]
:
Function Get-PSFileInfo {
[CmdletBinding()]
Param(
[Parameter(Position = 0, Mandatory)]
[string]$Path,
[ValidateSet('Size', 'Age', 'Security', 'Owner',
ErrorMessage = 'The value [{0}] is an invalid option. Valid values are Size, Age, Security, and Owner. Please try again.')]
[string]$Option = 'Size'
)
Write-Host "Processing $Path using option $Option"
}
Here it is in action:
PS C:\> Get-PSFileInfo C:\scripts\Get-AccessMaskDecode.ps1 -Option Age
Processing C:\scripts\Get-AccessMaskDecode.ps1 using option Age
PS C:\> Get-PSFileInfo C:\scripts\Get-AccessMaskDecode.ps1 -Option foo
Get-PSFileInfo: Cannot validate argument on parameter 'Option'. The value [foo] is an invalid option. Valid values are Size, Age, Security, and Owner. Please try again.
You can also use it with [ValidatePattern()]
:
Function Get-PSFileInfo {
[CmdletBinding()]
Param(
[Parameter(Position = 0, Mandatory)]
[ValidateScript({Test-Path $_},
ErrorMessage = 'The specified path, {0} could not be found'
)]
[ValidatePattern('\.ps(m)?(1(xml)?)?(d1)?$', ErrorMessage = 'The path does not look like a PowerShell-related file.'
)]
[string]$Path,
[ValidateSet('Size', 'Age', 'Security', 'Owner', ErrorMessage = 'The value [{0}] is an invalid option. Valid values are Size, Age, Security, and Owner. Please try again.')]
[string]$Option = 'Size'
)
Write-Host "Processing $Path using option $Option"
}
Notice that I am combining validation tests for $Path
.
PS C:\> Get-PSFileInfo c:\scripts\oops.ps1
Get-PSFileInfo: Cannot validate argument on parameter 'Path'. The specified path, c:\scripts\oops.ps1 could not be found
PS C:\> Get-PSFileInfo C:\work\0wq51y1v.dat
Get-PSFileInfo: Cannot validate argument on parameter 'Path'. The path does not look like a PowerShell-related file.
PS C:\> Get-PSFileInfo C:\work\a.ps1
Processing C:\work\a.ps1 using option Size
Using a custom error message lets your command easier to use and understand when something goes wrong.
A Scripting Challenge
Finally, I have a new scripting challenge for you. I hope you are taking advantage of these as I think they are great learning tools. For this month, write a PowerShell function to query the Win32_UserProfile
class on a local or remote computer. The output should look like this:
Name : systemprofile
LastUseTime : 11/19/2024 1:55:44 PM
Path : C:\WINDOWS\system32\config\systemprofile
PathCreated : 4/1/2024 3:26:07 AM
PathModified : 6/25/2024 3:23:58 AM
Size : 165896534
Special : True
Loaded : True
PSComputerName : JEFFDESK
As usual, I have bonus elements if you find this too easy:
- If the profile has no files show a size value of 0
- Allow the user to select a profile by name
- Use a type name for the output object so that you can write a custom format file
- Optimize the function for performance
You will need to think about the code you want to run and where to run it. I'll share my solution later next month.
Summary
We're rapidly reaching the end of another year and I want to sincerely thank my premium subscribers show support makes this newsletter possible. Free subscribers can upgrade at any time, and likewise cancel at any time. But while a premium member, you will have access to all of the previous issues in the archive. See you next month.