I’ve been pretty quiet as of late, however, not standing still. After a ton of heavy studying, achieved the Azure Security Engineer certification from Microsoft.
Learned a ton, especially on governance and Azure security design!

I’ve been pretty quiet as of late, however, not standing still. After a ton of heavy studying, achieved the Azure Security Engineer certification from Microsoft.
Learned a ton, especially on governance and Azure security design!
Create Office 365 mailbox folders and advanced mailbox rules with Powershell and MS Graph
Recently, a request came through to create some email messaging processing for various Office 365 users, which involved creating some folder structure in mailboxes of a list of users, and creating a message rule that had some different criteria based on email message attributes.
The needs were:
The New-MailboxFolder Powershell command works perfectly if the folder needed is for your own mailbox, if you want to run it against others, there is no current Powershell commandlet, so custom code must be created. While there are some basic examples out there, there was no comprehensive script anyone has published as of yet, so here is one I came up with.
For brevity purposes, I won’t go into detail the process that’s required to authenticate in order to run scripts against your environment, as there are quite a few resources available easily found by your favorite search engine, so I will skip over that process, and explain the “how to figure out what you need to accomplish with Powershell using MS Graph.”
$Mailboxes = @("userEmail.domain.com") $Folders = @("RootFolder","OtherRootFolder") $SubFolders = @("SubFolder") $MailbRule = "RuleForSubFolder" $AppId = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx" $AppSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" $Scope = "https://graph.microsoft.com/.default" $TenantName = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx" $Url = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token" Add-Type -AssemblyName System.Web $Body = @{ client_id = $AppId client_secret = $AppSecret scope = $Scope grant_type = 'client_credentials' } $PostSplat = @{ ContentType = 'application/x-www-form-urlencoded' Method = 'POST' # Create string by joining bodylist with '&' Body = $Body Uri = $Url } $Request = Invoke-RestMethod @PostSplat $Header = @{ Authorization = "$($Request.token_type) $($Request.access_token)" } foreach($Mailbox in $Mailboxes) { $Uri = "https://graph.microsoft.com/v1.0/users/$Mailbox/mailFolders" $Mailboxfolders = Invoke-RestMethod -Uri $Uri -Headers $Header -Method Get -ContentType "application/json" $MailboxfoldersList = $Mailboxfolders.value.displayName $NextPage = $Mailboxfolders.'@Odata.NextLink' While($null -ne $NextPage) { $Mailboxfolders = Invoke-RestMethod -Uri $NextPage -Headers $Header -Method Get -ContentType "application/json" $MailboxfoldersList += $Mailboxfolders.value.displayName $NextPage = $Mailboxfolders.'@Odata.NextLink' } foreach($Folder in $Folders) { $Body = @" { "displayName": "$Folder" } "@ Write-Host "Mailbox: $Mailbox`nMailboxfolders: $($MailboxfoldersList)`nFolder wanted: $Folder" if($($MailboxfoldersList) -contains $Folder) { Write-Host "$Folder folder already found at mailbox $Mailbox, creating subfolder.`n" $UriParent = "https://graph.microsoft.com/v1.0/users/$Mailbox/mailFolders/?`$filter=displayname eq '$Folder'" $ParentFolder = Invoke-RestMethod -Uri $UriParent -Headers $Header -Method Get -ContentType "application/json" $UriSub = "https://graph.microsoft.com/v1.0/users/$Mailbox/mailFolders/$($ParentFolder.value.id)/childFolders" } else { $ParentFolder = Invoke-RestMethod -Uri $Uri -Headers $Header -Method Post -Body $Body -ContentType "application/json" Write-Host "Created new folder: $($ParentFolder.displayName) to mailbox $Mailbox!`n" $UriSub = "https://graph.microsoft.com/v1.0/users/$Mailbox/mailFolders/$($ParentFolder.value.id)/childFolders" } } $MailboxSubfolders = Invoke-RestMethod -Uri $UriSub -Headers $Header -Method Get -ContentType "application/json" $MailboxSubfoldersList = $MailboxSubfolders.value.displayName foreach($SubFolder in $SubFolders) { $Body2 = @" { "displayName": "$SubFolder" } "@ if($($MailboxSubfoldersList) -contains $Subfolder) { Write-Host "$Subfolder folder already found at mailbox $Mailbox.`n" $UriGetSub = "https://graph.microsoft.com/v1.0/users/$Mailbox/mailFolders/$($ParentFolder.value.id)/childFolders/?`$filter=displayname eq '$Subfolder'" $SubId = Invoke-RestMethod -Uri $UriGetSub -Headers $Header -Method Get -ContentType "application/json" $UriGetRules = "https://graph.microsoft.com/v1.0/users/$Mailbox/mailFolders/inbox/messageRules" $MailboxRules = Invoke-RestMethod -Uri $UriGetRules -Headers $Header -Method Get -ContentType "application/json" Write-Host "The rules are: $($MailboxRules.value.displayName)" $MailboxRulesList = $MailboxRules.value.displayName if($($MailboxRulesList) -contains "$MailbRule") { Write-Host "The mailbox rule $MailbRule already found at mailbox $Mailbox.`n" break } else { ## For syntax: https://developer.microsoft.com/en-us/graph/graph-explorer $RuleBody = @" { "displayName": "$MailbRule", "sequence": 2, "isEnabled": true, "conditions": { "headerContains": [ "X-SomeCompany-tag: customTag" ] }, "actions": { "moveToFolder": "$($SubId.id)", "stopProcessingRules": true }, "exceptions": { "headerContains": [ "X-SomeCompany-Spam-Reason: eusafe", "Reply-To: noreply@somedomain.com" ] } } "@ $RuleUri = "https://graph.microsoft.com/v1.0/users/$Mailbox/mailFolders/inbox/messageRules" $NewRule = Invoke-RestMethod -Uri $RuleUri -Headers $Header -Method Post -Body $RuleBody -ContentType "application/json" Write-Host "Created new Rule: $MailbRule in mailbox $Mailbox!`n" } } else { $NewSubfolder = Invoke-RestMethod -Uri $UriSub -Headers $Header -Method Post -Body $Body2 -ContentType "application/json" Write-Host "Created new subfolder: $($NewSubfolder.displayName) in $Folder to mailbox $Mailbox!`n" $RuleBody = @" { "displayName": "$MailbRule", "sequence": 2, "isEnabled": true, "conditions": { "headerContains": [ "X-SomeCompany-tag: customTag" ] }, "actions": { "moveToFolder": "$($NewSubfolder.id)", "stopProcessingRules": true }, "exceptions": { "headerContains": [ "X-SomeCompany-Spam-Reason: eusafe", "Reply-To: noreply@somedomain.com" ] } } "@ $RuleUri = "https://graph.microsoft.com/v1.0/users/$Mailbox/mailFolders/inbox/messageRules" $NewRule = Invoke-RestMethod -Uri $RuleUri -Headers $Header -Method Post -Body $RuleBody -ContentType "application/json" Write-Host "Created new Rule: $MailbRule in mailbox $Mailbox!`n" } } }
The key part of this article is not to show how fancy of a script I can write (disclaimer: the fancy spacing is from Visual Studio Code, use it!), but rather, how to get at the MS Graph API and syntax required to do the tremendous amount of capabilities that it’s got access to. I figured by throwing up a script that does quite a few different things that were previously only available if you ran several different scripts one after another (and hoped nothing broke), here’s an example of doing several different things easily using Powershell against the MS Graph API.
To see what JSON I needed, I used https://developer.microsoft.com/en-us/graph/graph-explorer extensively to see the fields to use, and looked up the REST API documentation to see what properties are required in the request body. (For example, for the Message Rule, I went to: https://docs.microsoft.com/en-us/graph/api/mailfolder-post-messagerules)
Fix a madding “Invalid password” error when trying to use an Azure service that uses Azure AD DS as it’s authentication
I hope this helps someone fix a madding “Invalid password” error when trying to use an Azure service that uses Azure AD DS as it’s authentication with a synced account from on-premise AD.
With a recent implementation of Windows Virtual Desktop, and interesting failure occurred with a set of users that were synchronized to Azure AD from an on-premise AD environment.
The environment was one where there was no longer an Azure AD Connect configuration in place, in fact, the on-premise AD environment was no longer available. All users were using Office 365 services without any issues, and Azure AD Domain Services was implemented for new Azure services, one of them being WVD.
All WVD services were tested with the admin account that created the resources, and some test users created in Azure AD, however, when the group that needed to use the service tried to use it with their accounts, around half the necessary users could not log into the VM image.
After verifying all permissions were correctly assigned, and checking to see if there were any relevant differences between the accounts that were able to log on vs. the ones that were not, I noticed that all accounts were able to log on to the web URL, however after the initial logon to the service, the originally synced accounts were failing with an “invalid password” error, whereas the ones that were directly created in Azure succeeded. – Aha! This pointed me to the fact that the accounts seemed to be having some sort of Azure ADDS failure, as 365 services were not dependent on that.
Quite a few articles were read all over the place, with none being any help, so I went back to the basics, and went over the Azure AD to Azure AD DS synchronization guide much more methodical than I previously had.
I’ll cut to the chase, in the middle of that guide, the following statement is made: When a user is created in Azure AD, they’re not synchronized to Azure AD DS until they change their password in Azure AD.
This article is just a reminder to read event log errors carefully, as they tremendously help troubleshoot undocumented errors (which this blog is all about)
A Windows Virtual Desktop implementation kept having it’s Session host VMs randomly go to an Unavailable status, and after going through the full plethora of troubleshooting articles, I decided to take a closer look at the event error 3389:
Unable to retrieve DefaultAgent from registry: System.NullReferenceException: Object reference not set to an instance of an object.
at RDAgentBootLoader.BootLoaderSettings.get_DefaultAgentPath() in S:\src\RDAgent\src\RDAgentBootloader\BootLoaderSettings.cs:line 82
The above error doesn’t say much, and is pretty cryptic, but when we look at the detail pane, we get some better information:
Log Name: Application
Source: RDAgentBootLoader
Date: 12/11/2020 10:21:05 AM
Event ID: 3389
Task Category: None
Level: Error
Keywords: Classic
User: N/A
Computer: lis-wvd-0.xxxxxxx.com
Description:
Unable to retrieve DefaultAgent from registry: System.NullReferenceException: Object reference not set to an instance of an object.
at RDAgentBootLoader.BootLoaderSettings.get_DefaultAgentPath() in S:\src\RDAgent\src\RDAgentBootloader\BootLoaderSettings.cs:line 82
Event Xml:
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
<System>
<Provider Name="RDAgentBootLoader" />
<EventID Qualifiers="0">3389</EventID>
<Version>0</Version>
<Level>2</Level>
<Task>0</Task>
<Opcode>0</Opcode>
<Keywords>0x80000000000000</Keywords>
<TimeCreated SystemTime="2020-12-11T15:21:05.2207218Z" />
<EventRecordID>10136</EventRecordID>
<Correlation />
<Execution ProcessID="0" ThreadID="0" />
<Channel>Application</Channel>
<Computer>lis-wvd-0.xxxxxxxx.com</Computer>
<Security />
</System>
<EventData>
<Data>Unable to retrieve DefaultAgent from registry: System.NullReferenceException: Object reference not set to an instance of an object.
at RDAgentBootLoader.BootLoaderSettings.get_DefaultAgentPath() in S:\src\RDAgent\src\RDAgentBootloader\BootLoaderSettings.cs:line 82</Data>
</EventData>
</Event>
The first thing that stood out to me was “Unable to retrieve DefaultAgent from registry“, and after a quick search to see where that registry key was, I found it at: \HKLM\SOFTWARE\Microsoft\RDAgentBootLoader
Looking at my settings, I saw the “DefaultAgent” is pointing to an entry version that is not in my registry:
Hmm…. if it’s pointing to a version that there’s no key, let me check if the binaries are there on the machine. Yes, they were! So let me try creating the missing key by checking to see what was in the existing key:
Ok, let’s create the missing key in the same syntax as the existing one:
After a restart, the session hosts are now Available and stable!