Tracking Office 365 versions

I’ve just started to work on Office 365 and I wondered how I could track the different versions, their release notes, what changed, when…
I’ve deployed a Semi-Annal Targeted version and got the following version.
I wondered where this info under Product Info come from when you click on File/Office Account:

I started to dig into the registry and the following key contains many info about the Office 365 branch that was installed.

Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration" | 
Select AudienceId,UpdateChannel,*Version*,CDNBaseUrl | fl

Then I found these two links:

These are very nice and let you know what changed but I wanted to avoid parsing a web page to be able to track Office 365 versions, branches… and I’d like to get the same nice chart:

Then I found the github page about deploying Office 365 although I knew about the XML editor.
It appears that it contained all what I needed πŸ˜€

So, I wrote a function to help you and me achieve the following:

# See examples provided in the help of the function
Get-Help Get-O365UpdateChannelReleaseHistory -Examples

# Example 1: Displays all branches and releases
Get-O365UpdateChannelReleaseHistory  -Verbose |
ft -AutoSize

Note that there’s a 0 behind the version because it’s a real [version] object and not a [string]. Having real version objects will allow you to do a successful comparison between versions. Idem for release dates.
Notice also that there’s already a Long-Term Servicing Channel (2019) branch in the xml data. Who knew that?

# Example 2: Displays all branches and 
# only displays the latest version of every branch
Get-O365UpdateChannelReleaseHistory -Verbose| 
Where { $_.Latest} | ft -AutoSize

The next examples will display the same branch by calling the function by name, by id, by displayname and by global unique identifier (guid):
Note that every parameter (name, id, displayname and guid) have completion because valid sets have been declared in the parameters block. Just press TAB after a parameter:

# Example 3: Using the name
Get-O365UpdateChannelReleaseHistory -Name FirstReleaseDeferred -Verbose|
ft -AutoSize
# Example 4: using the Id
Get-O365UpdateChannelReleaseHistory -Id Targeted -Verbose|
ft -AutoSize
# Example 5: Using the displayname
Get-O365UpdateChannelReleaseHistory -DisplayName 'Semi-Annual Channel (Targeted)' -Verbose | ft -AutoSize
# Example 6: using a GUID
Get-O365UpdateChannelReleaseHistory -Guid b8f9b850-328d-4355-9145-c59439a0c4cf -Verbose | ft -AutoSize

If the function was able to download at least once a file but then fails, it will use the previous file that was previously downloaded. The verbose stream displays the published date found in the downloaded xml file:

Enjoy 😎

Function Get-O365UpdateChannelReleaseHistory {
Displays the Office 365 release history of a branch or all branches
Downloads the release history, parses the xml and displays the release history as object
Finds a branch from its name
Finds a branch from its Id
.PARAMETER DisplayName
Finds a branch from its display name
Finds a branch from its global unique identifier
Get-O365UpdateChannelReleaseHistory -Verbose | ft -AutoSize
Displays all branches and releases
Get-O365UpdateChannelReleaseHistory -Verbose | Where { $_.Latest} | ft -AutoSize
Displays all branches and only displays the latest version of every branch
Get-O365UpdateChannelReleaseHistory -Name FirstReleaseDeferred -Verbose | ft -AutoSize
Displays the branch named FirstReleaseDeferred and all its released versions
Get-O365UpdateChannelReleaseHistory -Id Targeted -Verbose | ft -AutoSize
Displays the branch Id named Targeted and all its released versions
Get-O365UpdateChannelReleaseHistory -DisplayName 'Semi-Annual Channel (Targeted)' -Verbose | ft -AutoSize
Displays the branch where its display name is 'Semi-Annual Channel (Targeted)' and all its released versions
Get-O365UpdateChannelReleaseHistory -Guid b8f9b850-328d-4355-9145-c59439a0c4cf -Verbose | ft -AutoSize
Displays the branch where its guid is b8f9b850-328d-4355-9145-c59439a0c4cf and all its released versions
All the data used to create this function where taken from:
'Monthly Channel (Targeted)',
'Monthly Channel',
'Semi-Annual Channel (Targeted)',
'Semi-Annual Channel',
'Long-Term Servicing Channel (2019)'
Begin {
$url = ''
If ($Guid) {
Switch ($Guid) {
'64256afe-f5d9-4f86-8936-8840a6a4f5be' {
$Id = 'Insiders' # FirstReleaseCurrent
'492350f6-3a01-4f97-b9c0-c7c6ddf67d60' {
$Id = 'Monthly' # Current
'b8f9b850-328d-4355-9145-c59439a0c4cf' {
$Id = 'Targeted' # FirstReleaseDeferred
'7ffbc6bf-bc32-4f92-8982-f9dd17fd3114' {
$Id = 'Broad' # Deferred
'f2e724c1-748f-4b47-8fb8-8e0d210e9208' {
$Id = 'LTSC2019'
default {}
Process {}
End {
try {
Invoke-WebRequest -Uri $url -UseBasicParsing -OutFile "$($env:temp)\" -ErrorAction Stop
} catch {
Write-Warning -Message "Failed to download file because $($_.Exception.Message)"
if (Test-Path -Path "$($env:temp)\" -PathType Leaf) {
if (-not(Test-Path -Path "$($env:temp)\O365-releasehistory" -PathType Container)) {
$null = mkdir "$($env:temp)\O365-releasehistory"
$null = & (Get-Command "$($env:systemroot)\system32\expand.exe") @(
if (Test-Path -Path "$($env:temp)\O365-releasehistory\ReleaseHistory.xml" -PathType Leaf) {
$xml = [xml](Get-Content -ReadCount 1 -Path "$($env:temp)\O365-releasehistory\ReleaseHistory.xml")
Write-Verbose -Message "Displaying release history published on $($xml.ReleaseHistory.PublishedDate)"
# Filtering by passed paramters if any
switch ($PsCmdlet.ParameterSetName) {
'byName' {
$channel = $xml.ReleaseHistory.UpdateChannel | Where-Object { $_.Name -eq $Name }
'byId' {
$channel = $xml.ReleaseHistory.UpdateChannel | Where-Object { $_.Id -eq $Id }
'byDisplayName' {
$channel = $xml.ReleaseHistory.UpdateChannel | Where-Object { $_.DisplayName -eq $DisplayName }
'byGUID' {
$channel = $xml.ReleaseHistory.UpdateChannel | Where-Object { $_.Id -eq $Id }
default {
$channel = $xml.ReleaseHistory.UpdateChannel
# Can be one or all, see above switch filtering
$channel |
ForEach-Object {
$cName = $_.Name
$cId = $_.ID
$cDisplayName = $_.DisplayName
$_.Update |
ForEach-Object {
# We've an Id, time to get back it's Guid
Switch ($cId) {
'Insiders' {$cGuid = '64256afe-f5d9-4f86-8936-8840a6a4f5be' ; break }
'Monthly' {$cGuid = '492350f6-3a01-4f97-b9c0-c7c6ddf67d60' ; break }
'Targeted' {$cGuid = 'b8f9b850-328d-4355-9145-c59439a0c4cf' ; break }
'Broad' {$cGuid = '7ffbc6bf-bc32-4f92-8982-f9dd17fd3114' ; break }
'LTSC2019' {$cGuid = 'f2e724c1-748f-4b47-8fb8-8e0d210e9208' ; break }
default {}
# Send object through the pipeline
Name = $cName
Id = $cId
DisplayName = $cDisplayName
Guid = $cGuid
Latest = $(
switch($_.Latest) {
'True' { $true ; break}
'False'{$false ; break}
default {}
Version = [Version]"$($_.Version).0"
LegacyVersion = [Version]$_.LegacyVersion
Build = $_.Build
ReleaseDate = $(Get-Date -Date "$($_.PubTime)")
} else {
Write-Warning -Message "Cannot find $($env:temp)\O365-releasehistory\ReleaseHistory.xml"
} else {
Write-Warning -Message "Cannot find $($env:temp)\"

About PowerShell Core 6.0 logging

I’ve seen in this interview that group policy templates (ADMX files) should be shipped soon and allow corporate admins to control PowerShell Core ScriptBlockLogging, Transcripts,.. all the security stuff the blue team ❀ .

Procmon reveals what registry path pwsh.exe is looking for:

It appears that the policy path for PowerShell Core 6.0 is different from Windows PowerShell 5.x:
Microsoft\Windows\PowerShell is replaced by Microsoft\PowerShellCore.

Using some google fu, I came across this pull request where there’s an interesting discussion, a sample powershell.config.json file. We also learn that:

On Windows, we first query GPO from registry, if the required policy is not defined, then we query policies from the configuration file.

Another very useful link is the help file where you can read that:

Windows requires the event provider to be registered before logged events can appear in the event log. For PowerShell, this is accomplished by running the RegisterManifest.ps1 from an elevated PowerShell prompt.

When you’ve ScriptBlockLogging enabled by GPO and registered the ETW provider, you get the related events in this location

Get-WinEvent -ListProvider "PowerShellCore"
Get-WinEvent -LogName 'PowerShellCore/Operational' -MaxEvents 1

Last thing, let’s look how different is the transcript header:

PowerShell Core has its own learning curve πŸ™‚

AutoRuns module compatible with PowerShell Core 6.0

I’ve glad to announce that my AutoRuns module is compatible with PowerShell Core 6.0 πŸ™‚

  • What changed? What was fixed?
    • The Get-FileHash function taken on PowerShell 4.0 wasn’t compatible with PowerShell Core. I’ve included a test on $PSVersionTable to fix this shortcoming.
    • The Get-WmiObject cmdlet was removed and replaced with Get-CimInstance by this commit (I’ve added a function to retrieve WMI namespaces recursively. I’ve also added a function to look for WMI providers)
    • There were some issues with the ImagePath property calculated from the raw value. Issues 5, 6, 7, 8, 9 were fixed.
    • I’ve added a template to report issue that I copied from this location. You can now submit an issue more easily.
  • What’s next?
  • What else?
  • Enjoy! PowerShell rocks! 😎

PowerShell Core 6.0: Generally Available (GA) and Supported

It’s like a Christmas present! Wow! PowerShell Core 6.0: Generally Available (GA) and Supported!

Congratulations to the PowerShell team and the PowerShell community who work hard to hit this milestone!

Yes, PowerShell Core 6.0 is *cross-platform* and it also means that
Microsoft ❀ Linux, we know that.
I hope that we see in the near future that Linux ❀ Microsoft and its community embraces this new #pwsh shell after realizing it’s not just another shell.

The above blog post announcing the release is a very good starting point and answers many questions.

I started to get binaries from There are SHA256 hashes at the end of the page as well as release notes.
This link shows how to install it on Windows. This one is for Linux.

The main documentation is available at

If you just do the following…

… you may notice that’s a big difference with “Windows PowerShell”.

Reading these pages is highly recommended:

I’ve followed the guidance about contributing and I’ve already opened an issue about digital signature of the content of zip and msi packages.

Find file names and URL of Windows 10 feature updates on WSUS

This question about how to find file names for Windows 10 feature updates on WSUS was raised this morning or last night on the list because Windows 10 1709 (CB = Current branch) was declared “CBB”, current branch for business (sorry, this is the old wording). In other words, the 1709 branch went from Semi-Annual Channel (Targeted) deployment state to a broader one called just “Semi-Annual Channel” (SAC)….

If I’m not clear, here’s the official release cadence, names and explanation:

… this question was raised because a 4th ISO is also expected…

and another list member asked how to get this info.

Abbodii replied the following:

If you are interested in these XML files as well, here are the links:

In a business environment, you’re more likely to find a WSUS server and it appears that you can also get the file names (ESD images) and their source location using PowerShell like this:

'Feature update to Windows 10, version 1709, en-us'
) | 
ForEach-Object { 
 Write-Verbose "Looking at update id $($_.Id.UpdateId) revision: $($_.Id.RevisionNumber)" -Verbose
 $_.GetInstallableItems() | 
 ForEach-Object {
  $_.Files | 
  ForEach-Object {
    URI = $_.OriginUri.OriginalString
    FileName = $_.OriginUri.Segments[-1]
} | Select FileName

This also means that you can approve only one feature updates if you only have for example x64 based Windows 10 clients to target.
With the above code, you know the update id and you can now do:

$WXtargetgroup = (Get-WsusServer).GetComputerTargetGroups() |
Where Name -eq 'Windows 10 x64'

'Feature update to Windows 10, version 1709, en-us'
) | Where-Object { 
 $_.Id.UpdateId -eq '7c803efa-be06-4481-9f8c-889afb34faf8' 
} | 
ForEach-Object -Process {
 Write-Verbose -Message "Approving $($_.Title)" -Verbose        

About CPU bug aka #Meltdown / #Spectre

  • What are the vulnerabilities?
    • CVE-2017-5715 – (Spectre), branch target injection
    • CVE-2017-5753 – (Spectre), bounds check bypass
    • CVE-2017-5754 – (Meltdown), rogue data cache load, memory access permission check performed after kernel memory read
  • Where to start? What about reading the following posts?

This isn’t an exhaustive list of posts, it’s just a starting point. These vulnerabilities have the widest scope I’ve ever seen and show how fragile IT devices and software are is. I should probably start distributing stickers sayin ‘Human error inside’ πŸ˜‰

  • What should I do? What’s the plan?

While it isn’t just a Microsoft issue, a list member (Mike) provided the following plan for Windows based computers:

Please note that you’ll need a microcode update or firmware update from your device manufacturer to be able to fully mitigate these vulnerabilities whatever OS and software you run.

If you run an Antivirus (AV) software (you should), please make sure it’s compatible with the security fixes released by software or OS vendors.

  • Where do I find the SpeculationControl PowerShell module provided by the MSRC?

The PowerShell gallery hosts the module:

The MSRC also released a zip version of it that you’ll find on

  • How do I use this module?

It may not be that easy and straightforward as you may think, when you’re supposed to start by installing the module with the following command

# Open a PowerShell prompt and type:
Install-Module SpeculationControl

Why? Because it depends on the version of PowerShell you run, if you run the console with elevated admin privileges, whether the Nuget provider has already been bootstraped or not… (see more on my Inside the Nuget bootstraping process post)

Here’s what I did on my Windows 10 (1709) where the Nuget provider wasn’t present:

# 1. Download the nuget provider dll
Invoke-WebRequest -Uri ` `
-OutFile `

# 2. Check the integrity of the downloaded file 
(Get-FileHash ~/downloads/Microsoft.PackageManagement.NuGetProvider.dll -Algorithm SHA512 | Select -Expand Hash).ToLower() -eq 

# 3. Create a destination folder
mkdir "C:\Program Files\PackageManagement\ProviderAssemblies\nuget\"

# 4. Copy the dll file to this folder
copy ~/downloads/Microsoft.PackageManagement.NuGetProvider.dll  `
-Destination "C:\Program Files\PackageManagement\ProviderAssemblies\nuget\"

# 5. Load the dll
Import-PackageProvider -Name Nuget -Verbose

# 6. Save the module from the
Save-Module -Name SpeculationControl -Repository  PsGallery -Verbose -Path ~/Downloads

# 7. Change the execution policy for the current console
Set-ExecutionPolicy  -Scope Process -ExecutionPolicy Bypass

# 8. Import the module (version 1.0.1 in my case)
Import-Module ~\Downloads\SpeculationControl\1.0.1\SpeculationControl.psd1 -Verbose

# 9. Use it 

  • How do I use the module against remote computers?

A fellow MVP Mike F. Robbins shows a nice way to achieve this on his blog:
Using PowerShell to Check Remote Windows Systems for CVE-2017-5754 (Meltdown) and CVE-2017-5715 (Spectre)

  • How do I follow the changes in the SpeculationControl module?

Well you can’t. The MSRC hasn’t indicated a ProjectURI in the metadata of the module 😦

I’ve saved all the versions from the PowerShell Gallery and pushed them into a github repo.

As of version 1.0.2, the module hosted on the PSGallery is digitally signed.

You can now check what changed using diff on the different commits: πŸ™‚

Get MSRC security updates data

Last year, the MSRC (Microsoft Security Response Center) stared to add release notes each month Security Update Guide > Dashboard. It aims to document already known issues.

The problem is that you get usually one release note on the portal (except this month) and cannot see previously released notes.
I’ve started to address this shortcoming and thought I’d share it.
You can find on all the links to the release notes ever published on the Security Update Guide > Dashboard πŸ™‚

I’ve also created folders where I store the CVRF (Common Vulnerability Reporting Framework) XML document and html bulletin that you’d get with the cmdlets in the MsrcSecurityUpdates module.

Back on the release notes. There’s actually another bigger issue:
Adding the release notes was Microsoft’s quick fix to provide ASAP the relevant info to IT professionals. Fine, no problem with that but it also appears that this “known issue” info isn’t documented directly into the XML cvrf document 😦

I’ve already tried to contact the Industry Consortium for Advancement of Security on the Internet (ICASI) and asked if the CVRF schema could be revised and updated.
I wrote the following message in March 2017 (no luck, no answer so far):

Get the content of the WindowsUpdate log as an object

I wanted to read the WindowsUpdate.log created by the Get-WindowsUpdateLog cmdlet. I knew there is an article that shows How to read the Windowsupdate.log file that I used some years ago to color the windows update log.

I also noticed that there’s a major difference in the content of the log.
The column separator is not longer a tab, it’s a serie of spaces.

I’ve also discovered another very insightful article about Understanding the Windowsupdate.log file for advanced users

I created another function that uses regular expression grouping to split each line and extracts the info based on the given format:

Function Import-WindowsUpdateLog {
Read the content of the Windows Update log and import it as an object
Read the content of the Windows Update log and import it as an object.
It will read each line and create an object with the following properties:
The path of the windows update log file.
Import-WindowsUpdateLog -FilePath ~\Desktop\WindowsUpdate.log
"~\Desktop\WindowsUpdate.log" | Import-WindowsUpdateLog | Out-GridView
Get-Item ~\Desktop\WindowsUpdate.log | Import-WindowsUpdateLog | Out-GridView
Test-Path -Path $_ -PathType Leaf
Begin {}
Process {
try {
Get-Content -Path $FilePath -ReadCount 1 -ErrorAction Stop |
ForEach-Object {
$Date,$Hour,$WUPID,$WUTID,$Component,$Message = (
).Match($_).Groups | Select-Object -Last 6 -ExpandProperty Value
Date = $Date
Hour = $Hour
Component = $Component
Message = $Message
} catch {
Throw "Failed because $($_.Exception.Message)"
End {}

With this function, you can simply do:

"~\Desktop\WindowsUpdate.log" | Import-WindowsUpdateLog | 

and you can use the Out-GridView cmdlet to filter and search what you’re looking for:

The above function is quite handy and would for sure replace notepad to read the human readable WindowsUpdate log file created by the Get-WindowsUpdateLog cmdlet.