• Skip to primary navigation
  • Skip to main content
  • Skip to primary sidebar

Clatent

Technology | Fitness | Food

  • About
  • Resources
  • Contact

Reporting

Teams Chat and PowerShell – How to add value!

March 23, 2026 by ClaytonT Leave a Comment

It’s been a bit for a technical blog post, but I’ve missed it and finally found some time to do one. What we are learning today is how to send messages, photos, urls, and information from APIs to Teams’ chat using PowerShell. This only covers for Teams chat’s as using Teams’ channels use slightly different authentication and cmdlets, but the concepts are the same. I know it’s not as exciting as Agents, Identity, or zero trust, but this will hopefully make your work day easier and bring more value to your and your company.

To start this off, we will first need a chat ID. I recommend at first using your chat with yourself, or use a chat that only you and people that won’t mind being bombed with test messages will mind!

Best way to get your chat ID

  1. Go to the web version of Teams and go to the chat you want to Read and/or write to
    1. We use to be able to copy the url and pull the ChatID out of it, but I’m not seeing that option anymore.
  2. Then click on the 3 dots in the top right and click “Copy Link”
  3. Here we will need to use everything after “chat/” or “channel” and before “/conversations” or “/General”
    1. It usually starts with “19:” or “19%” right after the “chat/”
  4. Once we have that I’d save it to a variable so you don’t have to keep remembering it and or copying and pasting that large link. Here I saved it to the variable $MyTestChatID so it was easy to remember. If this was prod, I’d use something on the lines of $TeamsChatId
    $MyTestChatID = "19:6h632y7d-nmx8-1yno-325h-77en2mx84x83_s78hy6e7-n6g3-7 9j0-36hf-86njyio53053@unq.gbl.spaces"
  5. Now we need to connect to Microsoft Graph. If your just adding information to a Teams Chat, all you will need is ChatMessage.Send and Chat.ReadWrite, but if you are creating a new chat, then you will also need Chat.Create.Connect-MgGraph -Scopes "Chat.ReadWrite", "ChatMessage.Send"

Before we start digging in, one thing to know is there are 2 different types of “Content Type.” The first is “text” which only allows text formatting, and there is “html” which you guessed it, allows html formatting. I haven’t gone crazy with the html formatting but you can make some nice charts and such with it and use images.

Simple Text:

Here is a simple text example, that just puts “This is my test” in the chat that you earlier added.

$Content = "This is my test"
	 
$ChatMessageBody = @{
   contentType = 'text'
   content     = $Content
}
$ChatMessage = New-MgChatMessage -ChatId $MyTestChatID -Body $ChatMessageBody

Using Emojis and Text:

You can add some flare to the text contentType by using Emojis.This will add the caution icon to your message

$Content = "⚠️ ALERT: Production Server DB-01 is offline!"

	 
$ChatMessageBody = @{
   contentType = 'text'
   content     = $Content
}
$ChatMessage = New-MgChatMessage -ChatId $MyTestChatID -Body $ChatMessageBody
	

Using bold and italicize:

If we want to add anymore style we need to change the contentType to html. Now we can bold and italicize the alert.

$Content = "<b><i>⚠️ ALERT:</b></i> Production Server DB-01 is offline!<p>"

	 
$ChatMessageBody = @{
   contentType = 'html'
   content     = $Content
} 
$ChatMessage = New-MgChatMessage -ChatId $MyTestChatID -Body $ChatMessageBody

Using html:

Now lets add on to it and make the alert a bit more useful, like giving a link to the change log request form to see if there was any scheduled maintenance for this time.

$Content = "<b><i>⚠️ ALERT:</b></i> Production Server DB-01 is offline!<p>"
$Content = $Content + "Please confim if this is an issue"
$Content = $Content + "<p>For more information, see <a href='<https://corporate.com/changelog>'>Corporate Change Log Requests</a>"
	 
$ChatMessageBody = @{
  contentType = 'html'
  content     = $Content
} 
$ChatMessage = New-MgChatMessage -ChatId $MyTestChatID -Body $ChatMessageBody

Piping functions with html:

Lets go a little more advanced and pipe in a function and put the results in teams. In this example we get the results from Get-Process, and only show the Name, Id, and CPU usage. How this works is by running Get-Process, selecting those objects, then converting the output to fragmented HTML. This is where instead of showing all of the html code, it only shows it for the table. Then pipe it to Out-String to turn the string array into a single string so that Teams can read it. If you don’t it will output but it will output System.Object[].

$FunctionTest = Get-Process | Select-Object Name, Id, CPU
$Content = ($FunctionTest | ConvertTo-Html -Fragment) | Out-String

$ChatMessageBody = @{
    contentType = 'html'
    content     = $Content
}
	 
$ChatMessage = New-MgChatMessage -ChatId $MyTestChatID -Body $ChatMessageBody

Yes, here is a one-liner for it, but wanted to show you could have functions before the actual “$Content” if you wanted.

$Content = Get-Process |
Select Name, Id, CPU |
ConvertTo-Html -Fragment |
Out-String

$ChatMessageBody = @{
    contentType = 'html'
    content     = $Content
}
	 
$ChatMessage = New-MgChatMessage -ChatId $MyTestChatID -Body $ChatMessageBody

APIs and html:

Now seeing text, html, and being able to show results from functions is great, but what about APIs? We can integrate them too, I mean who doesn’t want a Dad Joke every day right in there teams chat?? I tried to get Steven’s Judd API, but he wouldn’t give it to me. This is just scratching the surface, but wanted to show you it is possible. Full example right after the explaination.

A quick breakdown of this is it will first try what’s in the “try” block and if there is a terminating error at any part of it, it goes directly to the “catch” block and will run that code. Since we want to get information from icanhazdadjoke api, we have to first put the headers in, then call or “Invoke-RestMethod” the api with its Uri, and this grabs the information and saves it to the $response variable. Now we can use $($response.joke) to grab the actual joke for the example which we will see in the next step.

# Setting up headers and calling API for joke information
try {
    # Get a random dad joke from the API
    $headers = @{
        'Accept' = 'application/json'
        'User-Agent' = 'PowerShell Script (<https://github.com/DevClate/>)'
    }
    
    $response = Invoke-RestMethod -Uri "<https://icanhazdadjoke.com/>" -Headers $headers -Method Get
 

Here we create the Body content with “$jokeContent” as a Here-String(everything between @” and “@) which is a multi-line string literal that preserves all whitespace and line breaks. This makes the design of html easy and ensures it looks like we want in the teams chat. You can see we used a subexpresssion $($response.joke) to pull the actual joke, because if we just used $response.joke it won’t interpolate correctly inside the string.

 $jokeContent = @"
🤣 <b>Daily Dad Joke</b> 🤣<br><br>
<img src="<https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif>" alt="Dad Joke GIF" style="max-width: 200px; height: auto;"><br><br>
$($response.joke)<br><br>
<small><i>Joke ID: $($response.id)</i></small><br>
<small>Source: icanhazdadjoke.com | $(Get-Date -Format 'MMM dd, yyyy h:mm tt')</small>
"@

If your not familiar with html formatting, you can see the <b> that starts the bolding of “Daily Dad Joke” and stops bolding with </b>. Since this is a string we are using <br><br> to create line breaks.

🤣 <b>Daily Dad Joke</b> 🤣<br><br>

Here we will put in the code to show the image we want to show with the joke with the width and height.

<img src="<https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif>" alt="Dad Joke GIF" style="max-width: 200px; height: auto;"><br><br>

Also you can see with $(Get-Date -Format ‘MMM dd, yyyy h:mm tt’) to pull the date and time the joke was from.

<small>Source: icanhazdadjoke.com | $(Get-Date -Format 'MMM dd, yyyy h:mm tt')</small>

Then as long as there are no terminating errors you will get to the send your new daily joke to teams part, where you will add your $ChatMessageBody and for the content you would use $jokeContent unless you changed the variable for the joke content. Basically this makes it readable to Graph and Teams.

# Send to Teams body
    $ChatMessageBody = @{
    contentType = 'html'
    content     = $jokeContent
    }

After you’ve built the body, now we send it to Teams and output status to our terminal. Make sure you are using the correct ChatId, as you don’t want to send your message to the wrong person, so in this case, lets use our variable we created earlier $MyTestChatID. Then we will add our Body variable to make sure we get the right joke sent with $ChatMesssageBody. As long as all went smoothly we will see the joke in teams, and in our terminal we will see that it was successfully sent and the text version of the joke.

Now if it has a terminating error at any point, we move straight to the “Catch” so this is what the user will see if there is an terminating error. This is basically the same as above, except we add the “Write-Error” with the exception message, and put it a static joke in that gets sent to Teams. As you can see we changed the joke variable to $fallbackJoke and changed the content to $fallbackContent and everything else remains the same.

$ChatMessage = New-MgChatMessage -ChatId $MyTestChatID -Body $ChatMessageBody
    
    Write-Host "✅ Dad joke sent to Teams chat successfully!"
    Write-Host "Joke: $($response.joke)"
catch {
    Write-Error "❌ Failed to get dad joke: $($_.Exception.Message)"
    
    # Fallback joke if API fails
    $fallbackJoke = "Why don't scientists trust atoms? Because they make up everything!"
    
    $fallbackContent = @"
🤣 <b>Dad Joke (Backup)</b> 🤣<br><br>
<img src="<https://media.giphy.com/media/xT9IgG50Fb7Mi0prBC/giphy.gif>" alt="Classic Dad Joke" style="max-width: 200px; height: auto;"><br><br>
$fallbackJoke<br><br>
<small><i>API was unavailable, so here's a classic!</i></small><br>
<small>$(Get-Date -Format 'MMM dd, yyyy h:mm tt')</small>
"@
    
    $ChatMessageBody = @{
    contentType = 'html'
    content     = $fallbackContent
    }

    $ChatMessage = New-MgChatMessage -ChatId $MyTestChatID -Body $ChatMessageBody
    
    Write-Host "✅ Fallback dad joke sent to Teams chat!"
}

And here is the full script!

try {
    # Get a random dad joke from the API
    $headers = @{
        'Accept' = 'application/json'
        'User-Agent' = 'PowerShell Script (<https://github.com/DevClate/>)'
    }
    
    $response = Invoke-RestMethod -Uri "<https://icanhazdadjoke.com/>" -Headers $headers -Method Get
    
    # Using HTML img tag with external URL
    $jokeContent = @"
🤣 <b>Daily Dad Joke</b> 🤣<br><br>
<img src="<https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif>" alt="Dad Joke GIF" style="max-width: 200px; height: auto;"><br><br>
$($response.joke)<br><br>
<small><i>Joke ID: $($response.id)</i></small><br>
<small>Source: icanhazdadjoke.com | $(Get-Date -Format 'MMM dd, yyyy h:mm tt')</small>
"@

    # Send to Teams
    $ChatMessageBody = @{
    contentType = 'html'
    content     = $jokeContent
    }
    
    $ChatMessage = New-MgChatMessage -ChatId $MyTestChatID -Body $ChatMessageBody
    
    Write-Host "✅ Dad joke sent to Teams chat successfully!"
    Write-Host "Joke: $($response.joke)"
}
catch {
    Write-Error "❌ Failed to get dad joke: $($_.Exception.Message)"
    
    # Fallback joke if API fails
    $fallbackJoke = "Why don't scientists trust atoms? Because they make up everything!"
    
    $fallbackContent = @"
🤣 <b>Dad Joke (Backup)</b> 🤣<br><br>
<img src="<https://media.giphy.com/media/xT9IgG50Fb7Mi0prBC/giphy.gif>" alt="Classic Dad Joke" style="max-width: 200px; height: auto;"><br><br>
$fallbackJoke<br><br>
<small><i>API was unavailable, so here's a classic!</i></small><br>
<small>$(Get-Date -Format 'MMM dd, yyyy h:mm tt')</small>
"@
    
    $ChatMessageBody = @{
    contentType = 'html'
    content     = $fallbackContent
    }

    $ChatMessage = New-MgChatMessage -ChatId $MyTestChatID -Body $ChatMessageBody
    
    Write-Host "✅ Fallback dad joke sent to Teams chat!"
}

Advanced Example:

This script below is to show what is possible, and I won’t be going into detail on it, but wanted to give you a good starting point for a mini project. If you have any questions on it or want to work through it, I’d be glad to help out.

It will allow you to show completed and non completed migration status that you can sort by migration status and/or location status. If you wanted to, you could add a filter for department too. The only thing out of the box you need to do is put your ChatID in there and make sure you have the csv file saved.

I may have put in there a way to only have to put the users UPN to send them a message…

<#
.SYNOPSIS
    Get migration status from CSV and optionally send to Teams.

.DESCRIPTION
    Reads a CSV file containing user migration data and filters by location
    and migration status. Optionally sends the results to a Teams chat.

.EXAMPLE
    # All migrated users, all locations
    .\Get-MigrationStatus.ps1 -MigrationStatus migrated

.EXAMPLE
    # Not migrated users, specific location
    .\Get-MigrationStatus.ps1 -MigrationStatus notmigrated -Location Location1

.EXAMPLE
    # Both status, specific location, send to Teams
    .\Get-MigrationStatus.ps1 -MigrationStatus both -Location Location2 -RecipientUpn "manager@contoso.com"

.EXAMPLE
    # All users, all locations, to default chat
    .\Get-MigrationStatus.ps1 -MigrationStatus both

.NOTES
    Business Use Case: Migration status reporting, IT compliance tracking
    Required Scopes (if sending to Teams): Chat.Create, ChatMessage.Send
#>

param(
    [Parameter(Mandatory = $true)]
    [ValidateSet("Migrated", "NotMigrated", "Both")]
    [string]$MigrationStatus,

    [Parameter(Mandatory = $false)]
    [ValidateSet("Location1", "Location2", "Location3", "All")]
    [string]$Location = "All",

    [Parameter(Mandatory = $false)]
    [string]$RecipientUpn,

    [Parameter(Mandatory = $false)]
    [string]$ChatId = "default-chat-id",

    [Parameter(Mandatory = $false)]
    [string]$CsvPath = "$PSScriptRoot\\users.csv"
)

function Get-MigrationData {
    param(
        [string]$Path,
        [string]$Location,
        [string]$MigrationStatus
    )

    if (-not (Test-Path $Path)) {
        throw "CSV file not found: $Path"
    }

    $users = Import-Csv $Path

    # Filter by location
    if ($Location -ne "All") {
        $users = $users | Where-Object { $_.Location -eq $Location }
    }

    # Filter by migration status
    switch ($MigrationStatus) {
        "migrated" {
            $filtered = $users | Where-Object { $_.Migrated -ne "" -and $null -ne $_.Migrated }
        }
        "notmigrated" {
            $filtered = $users | Where-Object { $_.Migrated -eq "" -or $null -eq $_.Migrated }
        }
        "both" {
            $filtered = $users
        }
    }

    # Calculate counts
    $totalCount = ($users | Measure-Object).Count
    $migratedCount = ($users | Where-Object { $_.Migrated -ne "" -and $null -ne $_.Migrated } | Measure-Object).Count
    $pendingCount = $totalCount - $migratedCount

    return @{
        Users = $filtered
        TotalCount = $totalCount
        MigratedCount = $migratedCount
        PendingCount = $pendingCount
    }
}

function Send-MigrationStatusToTeams {
    param(
        [object]$Data,
        [string]$Location,
        [string]$MigrationStatus,
        [string]$RecipientUpn,
        [string]$ChatId
    )

    # Build HTML table
    $tableRows = ""
    foreach ($user in $Data.Users) {
        $migratedDisplay = if ($user.Migrated) { $user.Migrated } else { "<em>Pending</em>" }
        $tableRows += @"
<tr>
    <td>$($user.Name)</td>
    <td>$($user.Email)</td>
    <td>$($user.Location)</td>
    <td>$($user.Department)</td>
    <td>$migratedDisplay</td>
</tr>
"@
    }

    # Build title based on status and location
    $statusTitle = switch ($MigrationStatus) {
        "Migrated" { "Migrated" }
        "NotMigrated" { "Not Migrated" }
        "Both" { "All" }
    }

    $locationTitle = if ($Location -eq "All") { "All Locations" } else { $Location }

    $htmlContent = @"
<h2>📊 Migration Status - $statusTitle ($locationTitle)</h2>
<table border="1" cellpadding="5">
<tr>
    <th>Name</th>
    <th>Email</th>
    <th>Location</th>
    <th>Department</th>
    <th>Migrated</th>
</tr>
$tableRows
</table>
<h3>📈 Summary</h3>
<ul>
<li><strong>Total:</strong> $($Data.TotalCount)</li>
<li><strong>Migrated:</strong> $($Data.MigratedCount)</li>
<li><strong>Pending:</strong> $($Data.PendingCount)</li>
</ul>
<p><em>Report generated: $(Get-Date -Format "MMMM dd, yyyy HH:mm")</em></p>
"@

    # Check if already connected to Graph
    $graphContext = Get-MgContext -ErrorAction SilentlyContinue
    if ($graphContext) {
        Write-Host "Already connected to Graph as $($graphContext.Account)" -ForegroundColor Gray
        $connected = $true
    }
    else {
        # Connect to Graph
        Connect-MgGraph -Scopes "Chat.Create", "ChatMessage.Send" -ErrorAction Stop
        $connected = $true
    }

    # Send to appropriate chat
    if ($RecipientUpn) {
        # Create new 1:1 chat
        Write-Host "Creating chat with $RecipientUpn..."
        $chat = New-MgChat -ChatType OneOnOne -Members @(
            @{
                "@odata.type" = "#microsoft.graph.aadUserConversationMember"
                "roles" = @("owner")
                "user@odata.bind" = "<https://graph.microsoft.com/v1.0/users('$((Get-MgContext).Account>)')"
            },
            @{
                "@odata.type" = "#microsoft.graph.aadUserConversationMember"
                "roles" = @("owner")
                "user@odata.bind" = "<https://graph.microsoft.com/v1.0/users('$RecipientUpn')>"
            }
        )
        $targetChatId = $chat.Id
    }
    else {
        # Use default chat
        Write-Host "Using default chat: $ChatId"
        $targetChatId = $ChatId
    }

    # Send message
    $message = New-MgChatMessage -ChatId $targetChatId -Body @{
        contentType = "html"
        content = $htmlContent
    }

    # Only disconnect if we initiated the connection
    if (-not $graphContext) {
        Disconnect-MgGraph -ErrorAction SilentlyContinue
    }

    return $message.Id
}

function Show-MigrationStatus {
    param(
        [object]$Data,
        [string]$Location,
        [string]$MigrationStatus
    )

    $statusTitle = switch ($MigrationStatus) {
        "Migrated" { "Migrated" }
        "NotMigrated" { "Not Migrated" }
        "Both" { "All" }
    }

    $locationTitle = if ($Location -eq "All") { "All Locations" } else { $Location }

    Write-Host "`n=== Migration Status - $statusTitle ($locationTitle) ===" -ForegroundColor Cyan

    # Display table
    $Data.Users | Format-Table -AutoSize

    # Summary
    Write-Host "`n📊 Summary:" -ForegroundColor Green
    Write-Host "  Total:    $($Data.TotalCount)"
    Write-Host "  Migrated: $($Data.MigratedCount)"
    Write-Host "  Pending:  $($Data.PendingCount)"
}

# Main execution
Write-Host "Reading migration data from: $CsvPath" -ForegroundColor Gray

# Get data
$migrationData = Get-MigrationData -Path $CsvPath -Location $Location -MigrationStatus $MigrationStatus

# Display locally
Show-MigrationStatus -Data $migrationData -Location $Location -MigrationStatus $MigrationStatus

# Send to Teams if requested
if ($RecipientUpn -or $ChatId -ne "default-chat-id") {
    Write-Host "`nSending to Teams..." -ForegroundColor Yellow

    try {
        $messageId = Send-MigrationStatusToTeams -Data $migrationData -Location $Location -MigrationStatus $MigrationStatus -RecipientUpn $RecipientUpn -ChatId $ChatId
        Write-Host "✓ Message sent to Teams! (ID: $messageId)" -ForegroundColor Green
    }
    catch {
        Write-Warning "Failed to send to Teams: $_"
    }
}
else {
    Write-Host "`n(No Teams notification - add -RecipientUpn or -ChatId to send)" -ForegroundColor Gray
}

And here is an example for showing Migrated Devices only at Location2.

The full script is also located at Migration Example

There we have it, hope you enjoyed this blog post and learned something from it that will make your life easier. Think of any repetitive tasks, if you want to know when a long script is finished have it notify you chat, or have an agent pick what you and/or your team is having for lunch and put it in the team chat! Would love to hear how you used this blog post or how you incorporate PowerShell and Teams chat/channels.

I created a Github repository for these scripts and for using PowerShell and Teams with other examples and learning as well – Teams GitHub Repo

Have a great day and keep learning and sharing!

Tagged With: 365, Automation, PowerShell, Reporting, Teams

EntraFIDOFinder: New Web UI and Over 70 New Authenticators

January 26, 2026 by ClaytonT Leave a Comment

You read that right, over 70 new authenticators are now approved for Entra Attestation and have been add to the web ui and the PowerShell module! I knew they had to be holding back after these last few updates. Also I’ve updated the web UI and curious of your thoughts. I wanted to make it more modern and easier to view, especially the details window.

Here are a few of the new authenticators, but check the change log for the full list.

Android Authenticator

AAGUID: b93fd961-f2e6-462f-b122-82002247de78

Supported Interfaces:

InterfaceSupported
Biometric✅
USB❌
NFC❌
BLE❌

ATLKey Authenticator

AAGUID: 019614a3-2703-7e35-a453-285fd06c5d24

Supported Interfaces:

InterfaceSupported
Biometric❌
USB✅
NFC❌
BLE❌

Dapple Authenticator from Dapple Security Inc.

AAGUID: 6dae43be-af9c-417b-8b9f-1b611168ec60

Supported Interfaces:

InterfaceSupported
Biometric❌
USB❌
NFC❌
BLE❌

Deepnet SafeKey/Classic (FP)

AAGUID: e41b42a3-60ac-4afb-8757-a98f2d7f6c9f

Supported Interfaces:

InterfaceSupported
Biometric✅
USB❌
NFC❌
BLE❌

Deepnet SafeKey/Classic (USB)

AAGUID: b9f6b7b6-f929-4189-bca9-dd951240c132

Supported Interfaces:

InterfaceSupported
Biometric❌
USB❌
NFC❌
BLE❌

ellipticSecure MIRkey USB Authenticator

AAGUID: eb3b131e-59dc-536a-d176-cb7306da10f5

Supported Interfaces:

InterfaceSupported
Biometric❌
USB✅
NFC❌
BLE❌

Ensurity AUTH BioPro Desktop

AAGUID: 9eb85bb6-9625-4a72-815d-0487830ccab2

Supported Interfaces:

InterfaceSupported
Biometric✅
USB✅
NFC❌
BLE❌

Ensurity AUTH TouchPro

AAGUID: 50cbf15a-238c-4457-8f16-812c43bf3c49

Supported Interfaces:

InterfaceSupported
Biometric❌
USB✅
NFC❌
BLE❌

I’ve been working on better ways to see what keys have been added, removed, or modified, as well as approving valid vendors. It’s not perfected yet, but when I get closer, I’ll do a demo of it.

Let me know what you think of the new design and what functionality you wish it had. Also are there any keys you wish were attestation approved for Entra?

Where to get:
PowerShell Gallery: https://www.powershellgallery.com/packages/EntraFIDOFinder/0.0.22
Github: https://github.com/DevClate/EntraFIDOFinder/tree/main
Web UI: https://devclate.github.io/EntraFIDOFinder/Explorer/

Appreciate you taking the time and stay safe out there!

Tagged With: 365, Automation, Entra, EntraFIDOFinder, PowerShell, Reporting, Security

EntraFIDOFinder Update

September 26, 2025 by ClaytonT Leave a Comment

There haven’t been much changes the past couple months, but finally a biggish update happened where Microsoft has added 10 more keys that are Attestation capable.

Added Attestation capable keys:

  • Chipwon Clife Key | 930b0c03-ef46-4ac4-935c-538dccd1fcdb
  • HID Crescendo 4000 FIDO | aa79f476-ea00-417e-9628-1e8365123922
  • ID-One Key | 82b0a720-127a-4788-b56d-d1d4b2d82eac
  • ID-One Key | f2145e86-211e-4931-b874-e22bba7d01cc
  • VeridiumID Passkey Android SDK | 8d4378b0-725d-4432-b3c2-01fcdaf46286
  • VeridiumID Passkey iOS SDK | 1e906e14-77af-46bc-ae9f-fe6ef18257e4
  • VinCSS FIDO2 Fingerprint | 9012593f-43e4-4461-a97a-d92777b55d74
  • YubiKey 5 Series with NFC – Enhanced PIN | 662ef48a-95e2-4aaa-a6c1-5b9c40375824
  • YubiKey 5 Series with NFC – Enhanced PIN (Enterprise Profile) | b2c1a50b-dad8-4dc7-ba4d-0ce9597904bc
  • YubiKey 5 Series with NFC KVZR57 | 9eb7eabc-9db5-49a1-b6c3-555a802093f4

Are you requiring attestation? How has your implementation of FIDO2 keys been?

Don’t forget about the web version at: https://devclate.github.io/EntraFIDOFinder/Explorer/

Need the module?
– PowerShell: Install-PSResource EntraFIDOFinder
– PowerShell Gallery: https://www.powershellgallery.com/packages/EntraFIDOFinder/0.0.19
– GitHub: https://github.com/DevClate/EntraFIDOFinder

Tagged With: 365, Automation, EntraFIDOFinder, FIDO2, PowerShell, Reporting, Security

How to Delete Recurring Planner Tasks with PowerShell

July 30, 2025 by ClaytonT Leave a Comment

Are you using PowerShell and Microsoft Planner? I feel it doesn’t get the love it deserves, and to be honest, I hadn’t used PowerShell with Planner in a while, but wanted to get back into it. I first starting using the Microsoft.Graph.Planner and found some limitations that were possible if you used the Graph API directly. One of the things that stood out was you couldn’t call a Plan or Bucket by its real name, only by its ID. Yes, I could have added some logic to make it so, but realized that it also couldn’t remove recurring tasks. I thought it was going to be a quick fix, but found out hours later that wasn’t the case.. hence this post!

Let’s get into the process now.

First, we are going to go to planner.microsoft.cloud and click on the task you want or click on the plan, then on the task you want.

Once you are there you will find the ID in the URL

Below you will see in bold the “TaskId.” The “PlanId” is right after ‘plan/’. They will always be in these locations.

<https://planner.cloud.microsoft/webui/plan/1L__9CleiAwPwqMXDEPEALPQKPa9/view/board/task/1Peq3A7__1EXqot27RoV53QYBZuS?tid=26my427d-m317-83y1-63r0-4suv1pe421y8>

Next we will create a $TaskId variable, where you will put the TaskId inside of it

$TaskId = "1Peq3A7__1EXqot27RoV53QYBZuS"

Before we go farther, lets connect to Microsoft Graph (beta) with these scopes

Connect-MgGraph -Scopes 'Tasks.ReadWrite','Group.ReadWrite.All'

Then you’ll want to get the Task information and save it to the $Task variable to use later. This is important as this will store the ETAG value that you will need to delete the task, as this value changes anytime something changes with that task.

$task = Invoke-MgGraphRequest -Uri "<https://graph.microsoft.com/beta/planner/tasks/$taskId>"

Now here is the fun part, there is no way as of right now to delete a recurring meeting in one call. The best way I found to do it is to first cancel the recurrence then delete it. After doing more research I found later on that it does say you have to ‘$null’ out “Schedule” from Microsoft Learn. I figured it out the hard way when I was using “Developer Tools” to see the API requests it was doing on each click.

Let’s cancel the recurrence, first we have to build out the body to null out schedule and we do that like below.

$body = @{
    recurrence = @{
        schedule = $null
    }
} | ConvertTo-Json -Depth 3

After the body, we create the the “Header” for the request. We do that by below. This is very important because if you don’t Graph won’t know the exact task you are trying to change.

$headers = @{ 
    "If-Match" = $task.'@odata.etag'
    "Content-Type" = "application/json"
}

Now that we have TaskID, Body, and Header we can update(PATCH) the recurring task to a non recurring task.

Invoke-MgGraphRequest -Method PATCH -Uri "<https://graph.microsoft.com/beta/planner/tasks/$taskId>" -Body $body -Headers $headers

Perfect, you have canceled the recurring task and can now delete it. This may seem repetitive, but as of right now it’s the only way to do it. You have to get the task information again because it will now have a new ETAG, and will fail if you try to use the previous one.

$task = Invoke-MgGraphRequest -Uri "<https://graph.microsoft.com/beta/planner/tasks/$taskId>"

And we will have to put the updated ETAG in the header

$headers = @{ 
    "If-Match" = $task.'@odata.etag'
    "Content-Type" = "application/json"
}

We could have done this part in the beginning, but didn’t want to throw too much at you in the beginning, but here we will create the URI as a variable to make the API request shorter and easier to read.

$Uri = "<https://graph.microsoft.com/beta/planner/tasks/$taskId”>

The moment is finally here, where we actually get to delete the task…

Invoke-MgGraphRequest -uri $Uri -Method Delete -Headers $Headers

That’s it! Now go back to planner and confirm that is has been deleted.

Congrats on deleting your first recurring task! Below, I’ve put the whole script so you can see it all together and you can update the TaskId then run it to to delete recurring tasks.

If you’re interested in learning more about Planner and PowerShell, stay tuned as I may have some ideas to make using them together even easier.

$taskId = "1Peq3A7__1EXqot27RoV53QYBZuS"

$task = Invoke-MgGraphRequest -Uri "<https://graph.microsoft.com/beta/planner/tasks/$taskId>"

# Cancel the recurrence by setting schedule to null
$body = @{
    recurrence = @{
        schedule = $null
    }
} | ConvertTo-Json -Depth 3

$headers = @{ 
    "If-Match" = $task.'@odata.etag'
    "Content-Type" = "application/json"
}

Invoke-MgGraphRequest -Method PATCH -Uri "<https://graph.microsoft.com/beta/planner/tasks/$taskId>" -Body $body -Headers $headers

$task = Invoke-MgGraphRequest -Uri "<https://graph.microsoft.com/beta/planner/tasks/$taskId>"

$headers = @{ 
    "If-Match" = $task.'@odata.etag'
    "Content-Type" = "application/json"
}

$Uri = "<https://graph.microsoft.com/beta/planner/tasks/$taskId”>

Invoke-MgGraphRequest -uri $uri -Method Delete -Headers $headers

Let me know if you have any questions or feedback, have a great day!

Tagged With: 365, Automation, Planner, PowerShell, ProjectManagement, Reporting, Tasks

Why does my 365 Admin Audit Log sometime say it’s disabled, but other times enabled? Am I being compromised?

July 16, 2025 by ClaytonT Leave a Comment

Let me first start this off with I’m 99% sure you aren’t being compromised, but read on to see what I mean.

I first ran into this when I was running Maester and I saw that it said my test failed for having Unified Audit Log enabled. I then went to my Purview Portal and saw that it was enabled. Next I ran the command:

Get-AdminAuditLogConfig | Format-List UnifiedAuditLogIngestionEnabled

And received this output:

UnifiedAuditLogIngestionEnabled : False

It got me worried, as why is the PowerShell version saying it failed, but the GUI isn’t. Honestly, I usually trust the PowerShell output before the GUI. Then I run the PowerShell command to set it to “True.”

Set-AdminAuditLogConfig -UnifiedAuditLogIngestionEnabled $true

And received this output:

WARNING: The command completed successfully but no settings of 'Admin Audit Log Settings' have been modified.

Are you scratching your head like I was? I thought, maybe it’s because on the portal it shows it’s enabled, it is seeing it there and not changing it. Why not put that in the warning message though?

I did a little research and found Audit Log Enable Disable | MSFT which is where this little gem is located

Important

Be sure to run the previous command in Exchange Online PowerShell. Although the Get-AdminAuditLogConfig cmdlet is also available in Security & Compliance PowerShell, the UnifiedAuditLogIngestionEnabled property is always False, even when auditing is turned on.

And that is when it clicks, I connect to ExchangeOnlineManagement first then IPPSSession which must be causing the issue! I then disconnect with “Disconnect-ExhangeOnline”, and reconnect using “Connect-ExchangeOnline.” Now for the moment of truth:

Get-AdminAuditLogConfig | Format-List UnifiedAuditLogIngestionEnabled

UnifiedAuditLogIngestionEnabled : True

Success! But now the “why does this happen and why haven’t more people reported this?” I asked my buddy Sam Erde, had he seen this before? And he was perplexed as I was. Then he started digging a bit, and saw that you couldn’t use -NoClobber as it is from the same module.

The crazy part is, if you export both versions, they are the exact same code! What could it be? Is it how the IPPSSession connects to the API? If so, why not put a message saying you are connecting with IPPSSession, please disconnect and use connect-exchangeonline?

The mystery still continues, but I know Sam is working on a fix to handle this more consistently and hopefully have a fix shortly!

Have you been burned by this before?

Cliff notes version:

  • You weren’t compromised (unless you see it being changed in the logs and/or you ensure you are checking it correctly)
  • Make sure when checking for AuditLog is enabled through PS that your not using IPPSSession for the command
  • Sam Erde is working on a fix for Maester

Hope this saves you some headaches and have a great day!

Tagged With: 365, Maester, PowerShell, Purview, Reporting, Security

EntraFIDOFinder now with over 50 new keys!

March 17, 2025 by ClaytonT Leave a Comment

I guess I should be careful what I ask for now.. Not sure if you saw, but when Microsoft first made this update it blew up my repo with over 100 issues due to all the changes and I assumed Microsoft had changed how they formatted their website, but they hadn’t. It was just from the new keys, vendors, and changes to current keys.

All of their basic info has been updated on the web and PowerShell module, but I haven’t put all the meta data in from the FIDO Alliance, as I’m looking for a way to fully automate it when new keys are added.

Now to the part you really care about

New Vendors:

  • Android
  • Dapple Security
  • Eviden
  • Foongton
  • GSTAG
  • ID-One
  • IIST
  • Infineon Technologies AG
  • KeyVault
  • Ledger
  • Nitrokey
  • OneKey
  • Samsung
  • Securité Carte à Puce
  • TruU
  • Veridium
  • VeroCard
  • Vivokey
  • WinMagic
  • ZTPass

New Keys:

AAGUIDVendorDescription
eb3b131e-59dc-536a-d176-cb7306da10f5ellipticSecureellipticSecure MIRkey USB Authenticator
8da0e4dc-164b-454e-972e-88f362b23d59EvidenCardOS FIDO2 Token
46544d5d-8f5d-4db4-89ac-ea8977073fffFoongtonFoongtone FIDO Authenticator
773c30d9-5919-4e96-a4f5-db65e95cf890GSTAGGSTAG OAK FIDO2 Authenticator
7991798a-a7f3-487f-98c0-3faf7a458a04HID GlobalHID Crescendo Key V3
2a55aee6-27cb-42c0-bc6e-04efe999e88aHID GlobalHID Crescendo 4000
82b0a720-127a-4788-b56d-d1d4b2d82eacID-OneID-One Key
f2145e86-211e-4931-b874-e22bba7d01ccID-OneID-One Key
4b89f401-464e-4745-a520-486ddfc5d80eIISTIIST FIDO2 Authenticator
cfcb13a2-244f-4b36-9077-82b79d6a7de7Infineon Technologies AGUSB/NFC Passcode Authenticator
58b44d0b-0a7c-f33a-fd48-f7153c871352LedgerLedger Nano S Plus FIDO2 Authenticator
fcb1bcb4-f370-078c-6993-bc24d0ae3fbeLedgerLedger Nano X FIDO2 Authenticator
341e4da9-3c2e-8103-5a9f-aad887135200LedgerLedger Nano S FIDO2 Authenticator
2cd2f727-f6ca-44da-8f48-5c2e5da000a2NitrokeyNitrokey 3 AM
70e7c36f-f2f6-9e0d-07a6-bcc243262e6bOneKeyOneKey FIDO2 Bluetooth Authenticator
53414d53-554e-4700-0000-000000000000SamsungSamsung Pass
5343502d-5343-5343-6172-644649444f32Securité Carte à PuceESS Smart Card Inc. Authenticator
050dd0bc-ff20-4265-8d5d-305c4b215192ThaleseToken Fusion FIPS
10c70715-2a9a-4de1-b0aa-3cff6d496d39ThaleseToken Fusion NFC FIPS
c3f47802-de73-4dfc-ba22-671fe3304f90ThaleseToken Fusion NFC PIV Enterprise
146e77ef-11eb-4423-b847-ce77864e9411ThaleseToken Fusion NFC PIV
ba86dc56-635f-4141-aef6-00227b1b9af6TruUTruU Windows Authenticator
95e4d58c-056e-4a65-866d-f5a69659e880TruUTruU Windows Authenticator
5ea308b2-7ac7-48b9-ac09-7e2da9015f8cVeridiumVeridium Android SDK
6e8d1eae-8d40-4c25-bcf8-4633959afc71VeridiumVeridium iOS SDK
99ed6c29-4573-4847-816d-78ad8f1c75efVeroCardVeroCard FIDO2 Authenticator
d7a423ad-3e19-4492-9200-78137dccc136VivoKeyVivoKey Apex FIDO2
31c3f7ff-bf15-4327-83ec-9336abcbcd34WinmagicWinMagic FIDO Eazy – Software
970c8d9c-19d2-46af-aa32-3f448db49e35WinMagicWinMagic FIDO Eazy – TPM
f56f58b3-d711-4afc-ba7d-6ac05f88cb19WinMagicWinMagic FIDO Eazy – Phone
b7d3f68e-88a6-471e-9ecf-2df26d041edeYubicoSecurity Key NFC by Yubico
9ff4cc65-6154-4fff-ba09-9e2af7882ad2YubicoSecurity Key NFC by Yubico – Enterprise Edition (Enterprise Profile)
34f5766d-1536-4a24-9033-0e294e510fb0YubicoYubiKey 5 Series with NFC Preview
6ec5cff2-a0f9-4169-945b-f33b563f7b99YubicoYubiKey Bio Series – Multi-protocol Edition (Enterprise Profile)
8c39ee86-7f9a-4a95-9ba3-f6b097e5c2eeYubicoYubiKey Bio Series – FIDO Edition (Enterprise Profile)
24673149-6c86-42e7-98d9-433fb5b73296YubicoYubiKey 5 Series with Lightning
3a662962-c6d4-4023-bebb-98ae92e78e20YubicoYubiKey 5 FIPS Series with Lightning (Enterprise Profile)
20ac7a17-c814-4833-93fe-539f0d5e3389YubicoYubiKey 5 Series (Enterprise Profile)
b90e7dc1-316e-4fee-a25a-56a666a670feYubicoYubiKey 5 Series with Lightning (Enterprise Profile)
760eda36-00aa-4d29-855b-4012a182cdebYubicoSecurity Key NFC by Yubico Preview
fcc0118f-cd45-435b-8da1-9782b2da0715YubicoYubiKey 5 FIPS Series with NFC
ff4dac45-ede8-4ec2-aced-cf66103f4335YubicoYubiKey 5 Series
7b96457d-e3cd-432b-9ceb-c9fdd7ef7432YubicoYubiKey 5 FIPS Series with Lightning
97e6a830-c952-4740-95fc-7c78dc97ce47YubicoYubiKey Bio Series – Multi-protocol Edition (Enterprise Profile)
6ab56fad-881f-4a43-acb2-0be065924522YubicoYubiKey 5 Series with NFC (Enterprise Profile)
d2fbd093-ee62-488d-9dad-1e36389f8826YubicoYubiKey 5 FIPS Series (RC Preview)
4599062e-6926-4fe7-9566-9e8fb1aedaa0YubicoYubiKey 5 Series (Enterprise Profile)
d7781e5d-e353-46aa-afe2-3ca49f13332aYubicoYubiKey 5 Series with NFC
62e54e98-c209-4df3-b692-de71bb6a8528YubicoYubiKey 5 FIPS Series with NFC Preview
34744913-4f57-4e6e-a527-e9ec3c4b94e6YubicoYubiKey Bio Series – Multi-protocol Edition
ed042a3a-4b22-4455-bb69-a267b652ae7eYubicoSecurity Key NFC by Yubico – Enterprise Edition
3b24bf49-1d45-4484-a917-13175df0867bYubicoYubiKey 5 Series with Lightning (Enterprise Profile)
3124e301-f14e-4e38-876d-fbeeb090e7bfYubicoYubiKey 5 Series with Lightning Preview
9e66c661-e428-452a-a8fb-51f7ed088acfYubicoYubiKey 5 FIPS Series with Lightning (RC Preview)
ce6bf97f-9f69-4ba7-9032-97adc6ca5cf1YubicoYubiKey 5 FIPS Series with NFC (RC Preview)
2772ce93-eb4b-4090-8b73-330f48477d73YubicoSecurity Key NFC by Yubico – Enterprise Edition Preview
ad08c78a-4e41-49b9-86a2-ac15b06899e2YubicoYubiKey Bio Series – FIDO Edition
905b4cb4-ed6f-4da9-92fc-45e0d4e9b5c7YubicoYubiKey 5 FIPS Series (Enterprise Profile)
b415094c-49d3-4c8b-b3fe-7d0ad28a6bc4ZTPassZTPass SmartAuth
  • Updated Keys
    • Updated ‘NFC’ for AAGUID ’30b5035e-d297-4ff1-b00b-addc96ba6a98′ from ‘Yes’ to ‘No’.
    • Updated ‘Description’ for AAGUID ’83c47309-aabb-4108-8470-8be838b573cb’ from ‘YubiKey Bio Series (Enterprise Profile)’ to ‘YubiKey Bio Series – FIDO Edition (Enterprise Profile)’.
    • Updated ‘Description’ for AAGUID ‘5ca1ab1e-1337-fa57-f1d0-a117e71ca702’ from ‘Allthenticator App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers’ to ‘Allthenticator iOS App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers’.
    • Updated ‘Description’ for AAGUID ‘d8522d9f-575b-4866-88a9-ba99fa02f35b’ from ‘YubiKey Bio Series’ to ‘YubiKey Bio Series – FIDO Edition’.
    • Updated ‘Description’ for AAGUID ‘dd86a2da-86a0-4cbe-b462-4bd31f57bc6f’ from ‘YubiKey Bio FIDO Edition’ to ‘YubiKey Bio Series – FIDO Edition’.

I know, it was a lot for me too! Which FIDO2 keys do you like the best? Feel free to message me if you rather not put it in the comments, but would love to hear your experiences.

PowerShell Gallery: https://www.powershellgallery.com/packages/EntraFIDOFinder/0.0.16
GitHub: https://github.com/DevClate/EntraFIDOFinder
Web Version: https://devclate.github.io/EntraFIDOFinder/Explorer/

Hope you enjoyed and have a great day!

Tagged With: 365, Automation, Entra, EntraFIDOFinder, FIDO2, PowerShell, Reporting, Security

  • Page 1
  • Page 2
  • Page 3
  • Interim pages omitted …
  • Page 6
  • Go to Next Page »

Primary Sidebar

Clayton Tyger

Tech enthusiast dad who has lost 100lbs and now sometimes has crazy running/biking ideas. Read More…

Find Me On

  • Email
  • GitHub
  • Instagram
  • LinkedIn
  • Twitter

Recent Posts

  • Learning ValidateSet in PowerShell: Valid Values Only
  • Teams Chat and PowerShell – How to add value!
  • EntraFIDOFinder: New Web UI and Over 70 New Authenticators
  • January 19, 2026 Updates to EntraFIDOFinder
  • v0.0.20 EntraFIDOFinder is out

Categories

  • 365
  • Active Directory
  • AI
  • AzureAD
  • BlueSky
  • Cim
  • Dashboards
  • Documentation
  • Entra
  • Get-WMI
  • Learning
  • Module Monday
  • Nutanix
  • One Liner Wednesday
  • Passwords
  • PDF
  • Planner
  • PowerShell
  • Read-Only Friday
  • Reporting
  • Security
  • Uncategorized
  • Windows
  • WSUS

© 2026 Clatent