Find All agent-based Hybrid Runbook Workers

In case you are not away the Microsoft Monitoring Agent (MMA) will be retired on 31 August 2024. Prior to this date you will need to migrate all your agent-based hybrid workers to the extension-based worker. Microsoft has provided plenty of guidance on migrating the existing agent-based hybrid workers to extension-based hybrid workers. However, it may be difficult to keep track of all the hybrid workers in your environment and which ones have been migrated.

I put together the following script that will gather all the hybrid workers in your environment and let you know which ones are still agent-based. Just connect to Azure using the Connect-AzAccount and run this script below.

# Run Connect-AzAccount before running this script
Function Get-SubscriptionHybridRunbookWorkers{
    [CmdletBinding()]
    param(
        $SubscriptionId
    )
    # Set context to the subscription
    if($(Get-AzContext).Subscription.SubscriptionId -ne $SubscriptionId){
        Set-AzContext -SubscriptionId $SubscriptionId -ErrorAction Stop | Out-Null
    }
    Write-Verbose "Connected to $($(Get-AzContext).Subscription.Name)"

    # Get all of the automation accounts
    $AllAutomationAccounts = Get-AzAutomationAccount

    foreach($acct in $AllAutomationAccounts){
        Write-Verbose " - Account: $($acct.AutomationAccountName)"
        $autoAcct = @{
            AutomationAccountName = $acct.AutomationAccountName 
            ResourceGroupName = $acct.ResourceGroupName
        }
        # Get all the Hybrid Runbook Worker Groups
        $HybridRunbookWorkerGroups = Get-AzAutomationHybridRunbookWorkerGroup @autoAcct
        foreach($hrwg in $HybridRunbookWorkerGroups){
            # Output all the individual Hybrid Runbook Workers
            Get-AzAutomationHybridRunbookWorker @autoAcct -HybridRunbookWorkerGroupName $hrwg.Name | Select-Object WorkerType, WorkerName, 
                @{l='HybridRunbookWorkerGroupName';e={$hrwg.Name}}, @{l='GroupType';e={$hrwg.GroupType}}, @{l='SubscriptionId';e={$SubscriptionId}},
                @{l='AutomationAccountName';e={$acct.AutomationAccountName}}, @{l='ResourceGroupName';e={$acct.ResourceGroupName}}
        }
    }
}

# Get all enabled Azure subscriptions
$AllSubscriptions = Get-AzSubscription | Where-Object{ $_.State -eq 'Enabled' } | Select-Object Name, Id -Unique

$AllHybridRunbookWorkers = foreach($sub in $AllSubscriptions){
    Get-SubscriptionHybridRunbookWorkers -SubscriptionId $sub.Id
}

# Output all v1 hybrid workers aka the agent-based ones that need to be migrated
$AllHybridRunbookWorkers | Where-Object{ $_.WorkerType -eq 'HybridV1' }

Once the script is complete, it will output any hybrid workers that still have the worker type of HybridV1. This indicates that it is an agent-based worker and will need to be migrated.

WorkerType                   : HybridV1
WorkerName                   : MyOnPremSrv.contoso.com
HybridRunbookWorkerGroupName : HybridGroup01
GroupType                    : User
SubscriptionId               : d02f6dda-71fd-45b6-a0c0-4a9510dcb33e
AutomationAccountName        : MyAutomationAccount
ResourceGroupName            : TheResourceGroup

Any hybrid workers with the Group Type of User are members of a hybrid worker group. You can follow Microsoft’s guidance for migrating the existing agent-based hybrid workers to extension-based hybrid workers.

The Group Type of System indicates the machine is registered with the Automation Update Management. For these follow Microsoft’s guidance to move virtual machines from Automation Update Management to Azure Update Manager.

2023: A PowerShell Year in Review

2023 was quite the year for me and PowerShell, so I thought I would put together a brief summary of the year as I saw it. Plus provide you with some fun and interesting statics from the PowerShell Weekly newsletter.

This year was also a huge year for me. My book Practical Automation with PowerShell was released in April. This book aims to help you take your PowerShell skills to the next level and create full enterprise-ready automations. It was a real labor of lover to share my experiences in automation with the world. And I hope everyone who has read it has found it useful.

I’m looking forward to 2024 where I plan on making some major updates to some of my community module, publish more blog content, and in general continue my journey with PowerShell and technology in general. But I know most of you aren’t here to read about me, so here is my recap of some of the highlights from this year.

PowerShell Platform

PowerShell 7.4 was made generally available in November and is now built on .NET 8.
• The new package manager, PSResourceGet was released and is now included in PowerShell 7.4.
Microsoft Graph modules have now officially replaced the Azure AD modules.
• There were also new releases for PSReadLine and Crescendo.

Community

The PowerShell Podcast is still going strong with weekly releases and are almost up to 100 episodes.
The Pacific PowerShell User Group was started, and many local user groups are still going strong. Most notably the New York PowerShell Meetup and Research Triangle PowerShell User Group. (If you know of other PowerShell user groups please let me know and I’ll add them here.)
• This year was also my time presenting at the PowerShell + DevOps Global Summit. It was great getting to meet so many member of the community in person. Be sure to check out the PowerShell + DevOps Global Summit 2023 playlist on YouTube for my presentation and all of the others. And don’t forget to purchase your tickets to the 2024 summit.

PowerShell Weekly by the Numbers

For those who aren’t aware PowerShell Weekly is a weekly collection of PowerShell news, blogs, scripts, and other related media from world the web, that I found useful and wanted to share. With this being the end of the year, I thought I would share some interesting insights and numbers from 2023. Start with the number:

689
The total number of links this year

301
The number of unique contributors

140
The number of unique sites

Top Links
Completion Predictor v0.1.1 Release by The PowerShell Team (231 – clicks)
How to Create a Powershell Form Generator by Fabio De Oliveira (193 – clicks)
PowerShell Extension for Visual Studio Code Spring 2023 Update by The PowerShell Team (153 – clicks)
Building your own Terminal Status Bar in PowerShell by mdgrs (145 – clicks)
Automatically convert a PowerShell command to use splatting by Mike F. Robbins (116 – clicks)
PowerShell KeePass and saving time. by Emil Larsson (115 – clicks)
Increase maturity of PowerShell script with Mermaid diagram by Wiktor Mrowczynski (115 – clicks)
Best Practices Make Perfect by Jeff Hicks (110 – clicks)
POSH by James Brundage (110 – clicks)
How to optimize and speed up your PowerShell scripts by Bas Wijdenes (103 – clicks)

I hope everyone has a wonderful holiday season and I will see you all again in 2024.

Display Profile Functions

If you are like me and have multiple machines you work on with different profiles, it can be difficult to remember which profile contains which functions. So, I wrote a quick function that will display all the functions for me on start up.

Just add the code below to the bottom of your $profile file.

Function Get-ProfileFunctions {
    $__ProfileFunctions = Get-Content $profile | Where-Object { $_.Trim() -match '^function' } | Foreach-Object {
        $_.Replace('{', ' ').Replace('(', ' ').Split()[1]
    }

    if ($__ProfileFunctions) {
        Write-Host "Profile functions loaded`n$('-' * 24)" -ForegroundColor Yellow
        $__ProfileFunctions | ForEach-Object { Write-Host " - $($_)" -ForegroundColor Yellow }
        Write-Host "`n"
    }
}
Get-ProfileFunctions

Now when you open PowerShell you will see a prompt like the one below showing you the functions you have in the profile file.

Profile functions loaded
------------------------
- MyCustom-Function1
- Get-ProfileFunctions

You can also run Get-ProfileFunctions at any time to show the functions again.

Converting Visio to PNG and SVG

When working in Visio, it is not uncommon that you need to export your diagram to a picture for sharing or placing in documentation. For example, when writing, I will often have multiple Visio diagrams that I continually tweak throughout the process. So, I wrote a function to take all the Visio diagrams in a folder and export them to SVG and PNG.

All you need to do is pass this function the path of the folder, and it will do the rest.


Function Export-VisioToImages {
    <#
.SYNOPSIS
Use to export Visio diagrams to PNG and SVG format

.PARAMETER Path
Specifies the path to the folder with the Visio diagrams

.PARAMETER Filter
Specifies a filter to qualify the Path parameter. Default value is "*.vsdx"

.PARAMETER Force
Forces the command to overwrite existing export files.

.EXAMPLE
Export-VisioToImages -Path $Path -Force

.NOTES
Requires that Visio is installed on the local machine
#>
    param(
        [string]$Path,
        [string]$Filter = "*.vsdx",
        [switch]$Force
    )

    # Create the Visio object
    $Visio = New-Object -comobject Visio.Application
    $Visio.Visible = $false

    # Get all the Visio files in the folder
    $FilesToExport = Get-ChildItem $Path -Filter $Filter

    foreach ($item in $FilesToExport) {
        # Open the Visio document
        $doc = $Visio.Documents.Open($item.FullName)

        # Set the paths for the svg and png files
        $ExportPaths = @(
            Join-Path $item.DirectoryName "$($item.BaseName).svg"
            Join-Path $item.DirectoryName "$($item.BaseName).png"
        )

        foreach ($export in $ExportPaths) {
            if (Test-Path $export) {
                # If file exists and force is true, the delete the existing file
                if ($Force) {
                    Remove-Item $export -Force
                }
                # else if the file exists and force is not true, go to the next file
                else {
                    Write-Warning "Skipping '$export' because it already exists. Use -force to overwrite it."
                    continue
                }
            }
            # Export the Visio document
            $doc.Pages | ForEach-Object {
                $_.Export($export)
            }
            Write-Output $export
        }
        # Close the document
        $doc.Close()
    }

    # Close Visio
    $Visio.Quit()
}

The this post of part of the series Automation Authoring. Refer the main article for more details on use cases and additional content in the series.

Extracting images from Word

The process of extracting images with a Word document is relatively straightforward. All you have to do is rename the document from a .docx to a .zip and extract it. Once you do that, all the images will be in a subfolder named media.

However, with the help of PowerShell, we can not only automate the extraction but also copy them to a new location and list the caption information for each image.

The first thing you need to do is rename the Word document with the .zip extension. To ensure the original Word document remains untouched, we’ll copy it to a temporary folder and rename it.

Once you have the zip file, you can run a simple Expand-Archive command to extract the contents of the Word document. You will find the images in the subfolder word\media.

Then you can have PowerShell copy the files to another directory. And if that is all you wanted to do, you are done.

However, we can take things a step further and parse the Word document to display the captions for each image.

To do this, you will need to load the document.xml file into a PowerShell object. This XML contains all the configuration and references for the Word document. You can then parse through each paragraph to find the ones that are images and the ones that are captions. Images will have a drawing section under the paragraph, and captions with have a fldSimple property.

A child node named keepNext lets you determine if a caption is above or below the picture. When the caption is below, the image will have the keepNext node, but when the caption is above, the caption paragraph will have the keepNext node. If there is no caption, neither will have the node.

You can see this in the output below. Figures 1 and 3 have the captions below. Figure 2 has the caption above, and figure 4 does not have a caption.

Now all you need to do is parse through each image, match it with its appropriate caption, and output the results.

You can find the full code below. Also, since it parses the XML and not Word itself, this function does not require Word to be installed.

Function Export-ImagesFromWord {
    <#
.SYNOPSIS
Extracts images from a Word document and copies them to a new location

.DESCRIPTION
Extracts images from a Word document and copies them to a new location. 
After the extraction the caption informatino will be outputed to the screen

.PARAMETER DocumentPath
The path of the Word Document

.PARAMETER Destination
The folder to copy the file into

.EXAMPLE
Export-ImagesFromWord -DocumentPath "D:\scripts\ImageExamples.docx" -Destination "D:\scripts\images"

.NOTES
Does not require Word to be installed
#>
    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$DocumentPath,
        [Parameter(Mandatory = $true)]
        [string]$Destination
    )

    # Create a temporary folder to hold the extracted files
    $BaseName = [System.IO.Path]::GetFileNameWithoutExtension($documentPath) 
    $extractPath = Join-Path $env:Temp "mediaExtract\$($BaseName)"
    If (Test-Path $extractPath) {
        Remove-Item -Path $extractPath -Force -Recurse | Out-Null
    }
    New-Item -type directory -Path $extractPath | Out-Null

    # Copy the Word document as a zip and expand it
    $zipPath = Join-Path $extractPath "$($BaseName).zip"
    $zip = Copy-Item $documentPath $zipPath -Force -PassThru
    Expand-Archive -Path $zip.FullName -DestinationPath $extractPath -Force

    # Get the media files extracted and copy them to the output folder
    $mediaPath = Join-Path $extractPath 'word\media'
    If (-not(Test-Path $Destination)) {
        New-Item -type directory -Path $Destination | Out-Null
    }
    $extractedfigures = Get-ChildItem $mediaPath -File | Copy-Item -Destination $Destination -PassThru | Select-Object Name, @{l = 'Figure'; e = { $null } }, 
        @{l = 'Caption'; e = { '' } }, @{l = 'Id'; e = { [int]$($_.BaseName.Replace('image', '')) } }, FullName

    # Get the document configuration
    $documentXmlPath = Join-Path $extractPath 'word\document.xml'
    [xml]$docXml = Get-Content $documentXmlPath -Raw

    # Get all the paragraphs to find the images and captions
    $paragraphs = $docXml.document.body.p | Select-Object @{l = 'keepNext'; e = { @($_.pPr.ChildNodes.LocalName).Contains('keepNext') } }, 
        @{l = 'Id'; e = { $_.r.drawing.inline.docPr.id } }, @{l = 'CaptionId'; e = { $_.fldSimple.r.t } }, @{l = 'Prefix'; e = { $_.r[0].t.'#text' } }, 
        @{l = 'Text'; e = { $_.r[-1].t.'#text' } }, @{l = 'instr'; e = { $_.fldSimple.instr } }

    # Parse through each paragraph to match the caption to the image
    for ($i = 0; $i -lt $paragraphs.Count; $i++) {
        $capId = -1
        if ($paragraphs[$i].Id -gt 0 -and $paragraphs[$i].keepNext -eq $true) {
            $capId = $i + 1
        }
        elseif ($paragraphs[$i].Id -gt 0 -and $paragraphs[$i - 1].keepNext -eq $true) {
            $capId = $i - 1
        }

        if ($capId -gt -1) {
            $extractedfigures | Where-Object { $_.Id -eq $paragraphs[$i].Id } | ForEach-Object {
                $_.Figure = $paragraphs[$capId].CaptionId
                $_.Caption = "$($paragraphs[$capId].Prefix)$($paragraphs[$capId].CaptionId)$($paragraphs[$capId].Text)"
            }
        }
    }

    $extractedfigures | Select-Object Name, Figure, Caption, FullName
}

The this post of part of the series Automation Authoring. Refer the main article for more details on use cases and additional content in the series.

  |  
Start Azure VM and Open Bastion

The following snippet will check if a VM is turned on, and if not start it, then launch the Bastion connection window in Edge.

$VM = Get-AzVM -Name 'LN-TCTester-01' -Status
if($VM.PowerState -eq 'VM deallocated'){
    $VM | Start-AzVM
}
Start-Process -Path msedge -ArgumentList "https://portal.azure.com/#/resource$($VM.Id)/bastionHost"
  |  
Automation Authoring

When I started writing my book, Practical Automation with PowerShell, I discovered how much time and energy is required to keep everything up to date. For example, if I changed a piece of code in the text, I had to make sure the code sent to the publisher and uploaded to GitHub matched. Not to mention the style guidelines I needed to follow.

Or, if I included a screenshot or diagram, I had to upload the original to another folder named to match the chapter and figure number in the book. As you can imagine adding, changing, or removing anything during the editing process would create a laundry list of other things I would need to check. And this is where PowerShell came to the recuse.

I used PowerShell to help me keep track of code, images, my table of contents, and numerous other portions of the book writing process. So, I thought I would share with you some of the code and techniques I used during the authoring process.

While you might not be writing a book, you may certainly find some of these useful in your day-to-day scripting needs.

Each post in this series will be linked to and detailed below. Be sure to check back often for updates.

Working with Images

  • Comparing Images – The first post in this series is a function that allows you to compare two images. During the writing process, I had to ensure that the pictures in my Word documents matched the files I had uploaded to the publisher. This function allowed me to compare the two and ensure everything matched.
  • Extracting images from Word – This post will show you have to use PowerShell extraction images from a Word document, copy them to a new location, and list the caption information for each image.
  • *Converting Visio to PNG and SVG – During the writing process I had to create many Visio diagrams. Each diagram I created had to be embedded in the Word document as a PNG and I had to supply the graphics team with an SVG version. So, I wrote this function to export all Visio diagrams in a folder to PNG and SVG.

*newest post in the series

Up Next – Comparing Word documents – Ever end up with two copies of the same Word document? Well this post will show you how to use PowerShell to quickly find the differences between them.

Compare Images

The following function can be used to compare two pictures based on size and a pixel-by-pixel comparison.

Function Compare-Images {
    param(
        $ReferenceFile,
        $DifferenceFile
    )
    $ReferenceImage = [System.Drawing.Bitmap]::FromFile($ReferenceFile)
    $DifferenceImage = [System.Drawing.Bitmap]::FromFile($DifferenceFile)

    if ($ReferenceImage.Size -ne $DifferenceImage.Size) {
        Write-Host "Images are of different sizes"
        $false
    }
    else {
        # Set the difference to 0
        [float]$Difference = 0;
        # Parse through each pixel
        for ($y = 0; $y -lt $ReferenceImage.Height; $y++) {
            for ($x = 0; $x -lt $ReferenceImage.Width; $x++) {
                $ReferencePixel = $ReferenceImage.GetPixel($x, $y);
                $DifferencePixel = $DifferenceImage.GetPixel($x, $y);
                # Caculate the difference in the Red, Green, and Blue colors
                $Difference += [System.Math]::Abs($ReferencePixel.R - $DifferencePixel.R);
                $Difference += [System.Math]::Abs($ReferencePixel.G - $DifferencePixel.G);
                $Difference += [System.Math]::Abs($ReferencePixel.B - $DifferencePixel.B);
            }
        }
        # Caculate the precentage of difference between the photos
        $Difference = $(100 * ($Difference / 255) / ($ReferenceImage.Width * $ReferenceImage.Height * 3))
        if ($Difference -gt 0) {
            Write-Host "Difference: $Difference %" 
            $false
        }
        else {
            $true
        }
    }
}
PS C:\>Compare-Images "C:\Pictures\Pic01a.png" "C:\Pictures\Pic01b.png"
Difference: 0.01859266 %
False

The this post of part of the series Automation Authoring. Refer the main article for more details on use cases and additional content in the series.

PowerShell Weekly Redesign!

I am pleased to announce that PowerShell Weekly has been redesigned and moved to its own sub-site psweekly.dowst.dev. All past posts and links are available there as well.

What’s New

  • New look and feel
  • Thumbnails for every link
  • Improved searching of the 2,000+ links
    • Search by keyword, date, author, category
  • Custom RSS Feed specifically the links
2021: A PowerShell Year in Review

2021 was quite the year for PowerShell. We saw a lot of first and improvements in the platform. Not just from Microsoft but the community as a whole. I also personally hit a few milestones.

First and foremost, happy 15th birthday to PowerShell.

This year was also a huge year for me. My book Practical Automation with PowerShell was released for early access. You can purchase the eBook now and read chapters 1-8 right away, with chapters 9-11 coming very soon. The full book will be available this spring in eBook or hardcopy. This book aims to help you take your PowerShell skills to the next level and create full enterprise-ready automations.

Unfortunately, my blog posting has slowed a bit due to writing an entire book, but I did hit the 100th edition milestone for PowerShell Weekly. And I’m planning some redesigns for it later this year.

This year also marked my Podcast debut on IT Reality Podcast. I was also honored to present Automate Your Entire Server Patching Process for PowerShell Southampton and Automate the Admin for the New York PowerShell Meetup.

I’m looking forward to continuing my journey with PowerShell and technology in general. But I know most of you aren’t here to read about me, so here is my recap of some of the highlights from this year.

PowerShell Team

The PowerShell team at Microsoft has a busy year. me other firsts for PowerShell this year.

  • PowerShell 7 saw 12 different production releases, including 7.2.0 in November, followed quickly by 7.2.1 in December. There were also 11 preview and RC releases.
  • SecretManagement and SecretStore modules are made generally available, giving PowerShell a secure, repeatable, customizable way to store and retrieve passwords and secrets inside your scripts/automations.
  • PowerShell 7.2 updates are made available via Microsoft Update
  • Azure AD modules were migrated to Graph
  • PowerShell Crescendo 0.7.0-Preview.4 is released. Getting us one step closer to having a production-ready framework to develop PowerShell cmdlets for native OS commands. On any platform.
  • PowerShellGet 3.0 Preview 12 Release includes more parameters, additional pipeline support, more wildcard support and a number of other features detailed in the link provided.

The Community

Some remarkable and noteworthy community contributions from this year.

  • @kieranwalsh made a little PowerShell script to scan all locally installed modules and update them.
  • @Christopher83 with a great tip about using CTRL+SPACE to get a list of cmdlets.
  • @Jaap_Brasser developed a fantastic module to help configure Windows 11.
  • @SimonWahlin released the MyTesla module to automate your Tesla. Because why not!
  • @Ba4bes wrote a post showing you to create an API to find an Azure resource abbreviation. And how to turn it into an Azure Function App. All with PowerShell.
  • @TylerLeonhardt released Inline Values support for PowerShell. Allowing you to level up your PowerShell debugging by seeing values of variables right inline in the VS Code editor.
  • u/Kathy_Cooper1012 shared a script to audit Office 365 user’s activity with PowerShell.
  • u/rumorsofdemise shows how to test connectivity to ports from a server.
  • u/MadBoyEvo with the only command you will ever need to understand and fix your Group Policies (GPO).

PowerShell and Security

I don’t know about the rest of you, but it felt like every time we finished resolving some major vulnerability, another would pop up. Thank full the community and PowerShell were there to help us through some of the tough times.

  • @0gtweet with a script to remotely stop all spoolers where only default printers exist. #PrintNightmare
  • @sstranger created Get-Log4shellVuln.ps1 to scan all local drives for the presence of log4j jar files and analyzed the contents of the jar file to determine if it is vulnerable to log4shell (CVE-2021-44228) vulnerability
  • @darkQuassar  released a new package called AzureHunter, a Cloud Forensics Powershell module to run threat hunting playbooks on Azure UnifiedAuditLog data.

Top 10 PowerShell Weekly Links

These links were the most visited from PowerShell Weekly Newsletter

  1. Jeff Hicks: Better Event Logs with PowerShell
  2. u/DustinDortch: Why is  += so frowned upon?
  3. Kelvin Tegelaar: Tech in 5 minutes: Azure Functions
  4. Grzegorz Tworek: StopAndDisableDefaultSpoolers.ps1
  5. Adam Driscoll: Search Everything with PowerShell
  6. Idera: Finding System Paths
  7. Exchange Team: EXO V2 PowerShell module is now Generally Available on Linux & macOS
  8. Ioan Popovici: Clean-ADInactiveDevice
  9. Adam Driscoll: This is a PowerShell syntax\technique I was unaware of until today.
  10. Damien Van Robaeys: OneDrive and PowerShell: Get size and size on disk.

Modules

These modules were either released this year or received notable updates.

  • Pester v5.3.1 Pester provides a framework for running BDD style Tests to execute and validate PowerShell commands inside of PowerShell. Version 5.3.x included a bunch of new functionality, including a new Code Coverage mode, configure output for failed tests, automatic CI detection, and much more.
  • PSSlack v1.0.6 PowerShell module for the Slack API. Version 1.0.6 was released with multiple improvements and updates.
  • Foil v0.1.0 A PowerShell Crescendo wrapper for Chocolatey – https://github.com/ethanbergstrom/Foil
  • PnP.PowerShell v1.9.0 Microsoft 365 Patterns and Practices PowerShell Cmdlets
  • Posh-SSH v3.0.0 Provides SSH and SCP functionality for executing commands against remote hosts.

I know this post doesn’t even come close to capturing even a small amount of community contributions. So please feel free to comment or contact me with the ones you think should be included.