Getting old guest accounts in Azure AD

This is a rewrite from here office365itpros.com I have added some more properties to the report. And use msgraph instead of mggraph. # Needs permission User.Read.All $ClientID = '' $ClientSecret = '' $tenant_Id = '' # Connect to Graph # $Body = @{ Grant_Type = "client_credentials" resource = "https://graph.microsoft.com" client_id = $clientId client_secret = $clientSecret } $ConnectGraph = Invoke-RestMethod -Uri "https://login.microsoft.com/$tenant_Id/oauth2/token?api-version=1.0" -Method POST -Body $Body # Variable Collections # $Headers = @{ 'Content-Type' = "application/json" 'Authorization' = "Bearer $($ConnectGraph.access_token)" } $token = $ConnectGraph.access_token # Force TLS 1.2. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 function Get-GraphData { param ( [parameter(Mandatory)] [string]$AccessToken, [parameter(Mandatory)] [string]$Uri ) $Headers = @{ 'Authorization' = "Bearer $AccessToken" } do { $Results = Invoke-RestMethod -Uri $Uri -Headers $Headers -ErrorAction Stop $QueryResults += $Results.value $Uri = $Results.'@odata.nextLink' } while ($Uri) return $QueryResults } #This request get users list with signInActivity. $uri = "https://graph.microsoft.com/beta/users" $Result = @() [array]$Response = Get-GraphData -AccessToken $Token -Uri $uri if ($Response) { ForEach ($Respons in $Response) { $Result += New-Object PSObject -property $([ordered]@{ DisplayName = $Respons.displayName UserPrincipalName = $Respons.userPrincipalName UsageLocation = $Respons.usageLocation Contry = $Respons.country LastSignInDateTime = if($Respons.signInActivity.lastSignInDateTime) { [DateTime]$Respons.signInActivity.lastSignInDateTime } Else {$null} IsLicensed = if ($Respons.assignedLicenses.Count -ne 0) { $true } else { $false } IsGuestUser = if ($Respons.userType -eq 'Guest') { $true } else { $false } RefreshTokensValidFromDateTime = $Respons.RefreshTokensValidFromDateTime onPremisesDistinguishedName = $Respons.onPremisesDistinguishedName }) } } else { Write-Host "No User data found" } $GuestUsers = $Result | Where-Object{$_.IsGuestUser -eq "TRUE"} $GuestAccountAge = 365 # Value used for guest age comparison. If you want this to be a different value (like 30 days), change this here. #$GuestUsers = $users.value -All $true -Filter "UserType eq 'Guest'" | Sort DisplayName $Today = (Get-Date); $StaleGuests = 0 $Report = [System.Collections.Generic.List[Object]]::new() # Check each account and find those over 365 days old ForEach ($Guest in $GuestUsers) { $AADAccountAge = ($Guest.RefreshTokensValidFromDateTime | New-TimeSpan).Days If ($AADAccountAge -gt $GuestAccountAge) { $StaleGuests++ #Write-Host "Processing" $Guest.DisplayName $i = 0; $GroupNames = $Null # Find what Microsoft 365 Groups the guest belongs to... if any $ReportLine = [PSCustomObject]@{ UPN = $Guest.UserPrincipalName Name = $Guest.DisplayName Age = $AADAccountAge Created = $Guest.RefreshTokensValidFromDateTime } $Report.Add($ReportLine) } } # Output the report $Report | Sort Age -Descending | Format-Table -AutoSize Write-Host "Found" $StaleGuests "stale guest accounts." #reference: https://office365itpros.com/2019/10/15/report-old-guest-accounts/

April 26, 2023 · 2 min · 368 words · Benedikt Gabriel Egilsson

Getting started with Terraform and Azure

I like to watch videos on how to do things, but i don’t like to start from scratch. I like to have a template from which i can start and test things out. So start to look out for exporting Azure config to templates. Terraformer is a tool that can export existing cloud infrastructure to Terraform code. But I like aztfexport better because it is more Azure-specific. Install aztfexport is easy from package manager c:\winget install aztfexport And we need also Azure Cli c:\winget install azure-cli Login to Azure This was the part I struggled with: where is the config file? It was too simple: login to Azure with Azure Cli and then select the subscription I wanted to export from. Terraform, Terraformer, and aztfexport will use the same subscription as Azure Cli. ...

April 22, 2023 · 2 min · 253 words · Benedikt Gabriel Egilsson

Apps Expiration Azure AD

This script will check all registered apps in Azure AD and will return the following information: expiring apps, expired apps, and apps with no expiration date. # needs permission Application.Read.All,Directory.Read.All $ClientID = '' $ClientSecret = '' $tenant_Id = '' $TenantName = $Body = @{ Grant_Type = "client_credentials" resource = "https://graph.microsoft.com" client_id = $clientId client_secret = $clientSecret } $ConnectGraph = Invoke-RestMethod -Uri "https://login.microsoft.com/$tenant_Id/oauth2/token?api-version=1.0" -Method POST -Body $Body # Variable Collections # $path = "C:\temp\" $today = Get-Date $Headers = @{ 'Content-Type' = "application/json" 'Authorization' = "Bearer $($ConnectGraph.access_token)" } $token = $ConnectGraph.access_token # Force TLS 1.2. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 function Get-GraphData { param ( [parameter(Mandatory)] [string]$AccessToken, [parameter(Mandatory)] [string]$Uri ) $Headers = @{ 'Authorization' = "Bearer $AccessToken" } do { $Results = Invoke-RestMethod -Uri $Uri -Headers $Headers -ErrorAction Stop $QueryResults += $Results.value $Uri = $Results.'@odata.nextLink' } while ($Uri) return $QueryResults } #This request get application info $uri = "https://graph.microsoft.com/beta/applications/" [array]$apps = Get-GraphData -AccessToken $Token -Uri $uri $credentials = @() if ($apps) { foreach ($app in $apps) { $ApiUrl = "https://graph.microsoft.com/beta/applications/"+$app.id+"/owners" $owner = Invoke-WebRequest -Method GET -Uri $ApiUrl -ContentType "application/json" -Headers $headers | ConvertFrom-Json $app.KeyCredentials | foreach-object { #write-host $_.KeyId $_.DisplayName $credentials += [PSCustomObject] @{ CredentialType = "KeyCredentials"; DisplayName = $app.DisplayName; AppId = $app.AppId; ExpiryDate = $_.EndDateTime; StartDate = $_.StartDateTime; #KeyID = $_.KeyId; Type = $_.Type; Usage = $_.Usage; Owners = $owner.value.userPrincipalName; Expired = (([DateTime]$_.EndDateTime) -lt $today) ? "Yes" : "No"; } } $app.PasswordCredentials | foreach-object { #write-host $_.KeyId $_.DisplayName $credentials += [PSCustomObject] @{ CredentialType = "PasswordCredentials"; DisplayName = $app.DisplayName; AppId = $app.AppId; ExpiryDate = $_.EndDateTime; StartDate = $_.StartDateTime; #KeyID = $_.KeyId; Type = 'NA'; Usage = 'NA'; Owners = $owner.value.userPrincipalName; Expired = (([DateTime]$_.EndDateTime) -lt $today) ? "Yes" : "No"; } } } } $credentials | Export-Csv -Path $path\credentials.csv -NoTypeInformation #$credentials | Format-Table | Out-String|ForEach-Object {Write-Host $_} reference https://pnp.github.io/script-samples/aad-apps-expired-keys/README.html?tabs=graphps {: .prompt-info } ...

April 7, 2023 · 2 min · 298 words · Benedikt Gabriel Egilsson

Powershell Speedtest

Was having problems with my internet connection and wanted to monitor it. So I rewrote this script to use powershell and store the results in a table storage. The script is using the speedtest cli from https://www.speedtest.net/apps/cli {: .prompt-info } #$StorageAccountName = "StorageAcount" #$Key = "Storagekey" #$StorageContext = New-AzStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $Key #$Table = (Get-AzStorageTable -Context $StorageContext | Where-Object {$_.name -eq "Speedtest"}).CloudTable $applocation = "C:\apps\speedtest" $path = "C:\temp\" $SpeedTestResults=@() $SpeedtestObj=@() $i = 0 while ($i -eq 0) { $PartitionKey = "1" $SpeedTestResults = & "$($applocation)\speedtest.exe" --progress=no --format=json $SpeedtestResults = $SpeedTestResults | ConvertFrom-Json $SpeedtestObj += [PSCustomObject] @{ Time = Get-Date -Format "dd/MM/yyyy HH:mm K" downloadspeed = [math]::Round($SpeedtestResults.download.bandwidth / 1000000 * 8, 2) uploadspeed = [math]::Round($SpeedtestResults.upload.bandwidth / 1000000 * 8, 2) packetloss = ($($SpeedtestResults.packetLoss).ToString("P")) isp = $SpeedtestResults.isp ExternalIP = $SpeedtestResults.interface.externalIp InternalIP = $SpeedtestResults.interface.internalIp UsedServer = $SpeedtestResults.server.host location = $SpeedTestResults.server.location Jitter = [math]::Round($SpeedtestResults.ping.jitter) Latency = [math]::Round($SpeedtestResults.ping.latency) } # ---- Move to table storage ---- # Add-AzTableRow -table $Table -PartitionKey $PartitionKey -RowKey (Get-Date).Ticks -property $SpeedtestObj Start-Sleep -Seconds 15 } #$SpeedtestObj | Format-Table | Out-String|ForEach-Object {Write-Host $_} $SpeedtestObj | Export-Csv -Path $path\speedtest.csv -NoTypeInformation

April 6, 2023 · 1 min · 179 words · Benedikt Gabriel Egilsson

Esp32 Hot tub controller

This is my hot tub controler with outdoor shower controler. It is based on a ESP32 and a 4 channel relay module. It has a DS18B20 temperature sensor for the hot tub and a DS18B20 for the water in the hot tub. It also has a DS18B20 for the outdoor shower and a DS18B20 for the water in system. esphome: name: pottastyring platform: ESP32 board: nodemcu-32s wifi: ssid: "" password: "" domain: .lan manual_ip: static_ip: 192.168.x.x gateway: 192.168.x.x subnet: 255.255.255.0 dns1: 192.168.x.x # Enable fallback hotspot (captive portal) in case wifi connection fails ap: ssid: "hottub" password: "" captive_portal: web_server: port: 80 # Enable logging logger: # Enable Home Assistant API api: ota: time: - platform: sntp id: timer servers: - 0.pool.ntp.org - 1.pool.ntp.org - 2.pool.ntp.org dallas: - pin: GPIO13 sensor: - platform: dallas address: 0x67011464EA7EFF28 name: "Pottur hiti" id: pottur_hiti - platform: dallas address: 0x8B0314649975FF28 name: "Vatn i pott" id: vatn_i_hiti - platform: dallas address: 0x110000045976f328 name: "Hiti Skapur" id: Hiti_skapur - platform: dallas address: 0xe10314645814ff28 name: "Hiti Uti" id: hiti_uti binary_sensor: - platform: gpio filters: - delayed_on: 100ms id: fylla pin: number: GPIO17 mode: INPUT_PULLUP inverted: True name: "Pottur fylla takki" on_press: if: condition: lambda: 'return id(pottur_hiti).state < 20;' then: - logger.log: "The sensor value is below 20!" - switch.turn_on: relay_1 - delay: 1min - switch.turn_on: relay_4 - delay: 30min - switch.turn_off: relay_1 - delay: 3min - climate.control: id: pottur_climate mode: "HEAT" else: - logger.log: "The sensor value is above 20!" - platform: gpio filters: - delayed_on: 500ms id: sturta pin: number: GPIO21 mode: INPUT_PULLUP inverted: True name: "Sturta takki" on_press: if: condition: lambda: 'return id(pottur_hiti).state > 38;' then: - logger.log: "The sensor value is above 38!" - climate.control: id: pottur_climate mode: "OFF" - delay: 10sec - switch.turn_on: relay_3 - delay: 4min - switch.turn_off: relay_3 else: - logger.log: "The sensor value is below 20!" - switch.turn_on: relay_3 - delay: 4min - switch.turn_off: relay_3 - platform: gpio filters: - delayed_on: 500ms id: nidurfall pin: number: GPIO33 mode: INPUT_PULLUP inverted: True name: "taema takki" on_press: then: - switch.turn_off: relay_4 - climate.control: id: pottur_climate mode: "off" switch: - platform: gpio restore_mode: ALWAYS_OFF name: "Pottur fylla relay" pin: GPIO25 id: relay_1 - platform: gpio restore_mode: ALWAYS_OFF name: "Pottur hita relay" pin: GPIO26 id: relay_2 - platform: gpio restore_mode: ALWAYS_OFF name: "Sturta relay" pin: GPIO27 id: relay_3 - platform: gpio restore_mode: ALWAYS_ON name: "Loka nidurfalli" pin: GPIO18 id: relay_4 climate: - platform: bang_bang id: pottur_climate visual: min_temperature: 38 max_temperature: 42 temperature_step: 1.0 name: "Pottur vatn" sensor: pottur_hiti default_target_temperature_low: 38 °C default_target_temperature_high: 40 °C heat_action: if: condition: lambda: 'return id(pottur_hiti).state > 32;' then: - logger.log: "The sensor is above 32! then full" - switch.turn_on: relay_1 - switch.turn_on: relay_2 else: - logger.log: "The sensor value is below 32!" - switch.turn_on: relay_1 - delay: 1min - switch.turn_on: relay_4 - delay: 30min - switch.turn_on: relay_2 idle_action: - switch.turn_off: relay_2 - switch.turn_off: relay_1 mqtt: broker: 192.168.x.x username: xxx password: xxx

April 4, 2023 · 3 min · 482 words · Benedikt Gabriel Egilsson

Upgrade Hyper-V Hosts with Azure Arc

Azure Arc is a powerful tool for managing and updating hybrid infrastructure, including standalone Hyper-V hosts. Demo how to use Azure Arc to update a standalone Hyper-V host, using a scheduled update task in Automation Account and pre and post scripts that shut down and start up VMs. Pre-requisites # Stop all running VMs on the Hyper-V host and wait for them to shut down # save the names of the running VMs to a file $filePath = "C:\temp\runningvm.txt" (Get-vm | Where-Object { $_.State -eq "Running" }).name | Out-File $filePath $runningvm = Get-Content $filePath foreach ($Name in $runningvm) { Stop-VM $Name do { $VM1 = get-vm -Name $Name Write-Progress -Activity "Waiting for the VM to shutdown" } until ($Null -eq $VM1.Heartbeat) } # send a webhook to Teams to notify that the VMs have been shut down $webhookUri = "" $body = @{ "@context" = "http://schema.org/extensions" "@type" = "MessageCard" "themeColor" = "d70000" "title" = "Send Webhook to Teams" "text" = "This is a message sent from Powershell" } Invoke-RestMethod -Uri $webhookUri -Method Post -Body (ConvertTo-Json -InputObject $body) Post-requisites ...

April 4, 2023 · 2 min · 354 words · Benedikt Gabriel Egilsson

Getting licenses assigned to user accounts

Create a report of licenses assigned to Azure AD user accounts using the Microsoft Graph API This is a rewrite from here practical365.com My misson was to rewrite the script to use the Microsoft Graph API instead of the MgGraph PowerShell module. {: .prompt-info } $ClientID = '' $ClientSecret = '' $tenant_Id = '' # Connect to Graph # $Body = @{ Grant_Type = "client_credentials" resource = "https://graph.microsoft.com" client_id = $clientId client_secret = $clientSecret } $ConnectGraph = Invoke-RestMethod -Uri "https://login.microsoft.com/$tenant_Id/oauth2/token?api-version=1.0" -Method POST -Body $Body # Variable Collections # $CSVOutputFile = "c:\temp\Microsoft365LicensesReport.CSV" $today = Get-Date $Headers = @{ 'Content-Type' = "application/json" 'Authorization' = "Bearer $($ConnectGraph.access_token)" } $token = $ConnectGraph.access_token # Force TLS 1.2. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 function Get-GraphData { param ( [parameter(Mandatory)] [string]$AccessToken, [parameter(Mandatory)] [string]$Uri ) $Headers = @{ 'Authorization' = "Bearer $AccessToken" } do { $Results = Invoke-RestMethod -Uri $Uri -Headers $Headers -ErrorAction Stop $QueryResults += $Results.value $Uri = $Results.'@odata.nextLink' } while ($Uri) return $QueryResults } #This request get SecureScore [array]$Skus = Get-GraphData -AccessToken $Token -Uri "https://graph.microsoft.com/beta/subscribedSkus" #[Array]$Skus = Get-MgSubscribedSku # Generate CSV of all product SKUs used in tenant [array]$Sku = $Skus | Select-Object SkuId, SkuPartNumber # Generate list of all service plans used in SKUs in tenant $SPData = [System.Collections.Generic.List[Object]]::new() ForEach ($Sk in $Skus) { ForEach ($SP in $Sk.ServicePlans) { $SPLine = [PSCustomObject][Ordered]@{ ServicePlanId = $SP.ServicePlanId ServicePlanName = $SP.ServicePlanName ServicePlanDisplayName = $SP.ServicePlanName } $SPData.Add($SPLine) } } $SkuHashTable = @{} ForEach ($Line in $Sku) { $SkuHashTable.Add([string]$Line.SkuId, [string]$Line.skuPartNumber) } $ServicePlanHashTable = @{} ForEach ($Line2 in $SPData) { $ServicePlanHashTable.Add([string]$Line2.ServicePlanId, [string]$Line2.ServicePlanDisplayName) } [Array]$Users = Get-GraphData -AccessToken $Token -Uri "https://graph.microsoft.com/beta/users" $Report = [System.Collections.Generic.List[Object]]::new() ForEach ($User in $users) { If ([string]::IsNullOrWhiteSpace($User.AssignedLicenses) -eq $true) { # Only process account if it has some licenses Write-Host "Processing" $User.DisplayName [array]$LicenseInfo = $Null; [array]$DisabledPlans = $Null ForEach ($License in $User.AssignedLicenses) { If ($SkuHashTable.ContainsKey($License.SkuId) -eq $True) { # We found a match in the SKU hash table $LicenseInfo += $SkuHashTable.Item($License.SkuId) } Else { # Nothing doing, so output the SkuID $LicenseInfo += $License } # Report any disabled service plans in licenses If ([string]::IsNullOrWhiteSpace($License.DisabledPlans) -eq $False ) { # Check if disabled service plans in a license ForEach ($DisabledPlan in $License.DisabledPlans) { # Try and find what service plan is disabled If ($ServicePlanHashTable.ContainsKey($DisabledPlan) -eq $True) { # We found a match in the Service Plans hash table $DisabledPlans += $ServicePlanHashTable.Item($DisabledPlan) } Else { # Nothing doing, so output the Service Plan ID $DisabledPlans += $DisabledPlan } } # End ForEach disabled plans } # End if check for disabled plans } # End Foreach Licenses # Report information [string]$DisabledPlans = $DisabledPlans -join ", " [string]$LicenseInfo = $LicenseInfo -join (", ") $ReportLine = [PSCustomObject][Ordered]@{ User = $User.DisplayName UPN = $User.UserPrincipalName Country = $User.Country Department = $User.Department Title = $User.JobTitle Licenses = $LicenseInfo "Disabled Plans" = $DisabledPlans } Write-Host $ReportLine $Report.Add($ReportLine) } #end If account is licensed Else { $UnlicensedAccounts++ } } # End ForEach Users $Report | Export-CSV -NoTypeInformation $CSVOutputFile

April 1, 2023 · 3 min · 480 words · Benedikt Gabriel Egilsson

MS Graph API - Get Secure Score for tenant

Secure Score for O365 Tenant Getting the secure score for a tenant is a bit more complicated than getting the last logon time of a user. You only need to call the following endpoint: https://graph.microsoft.com/beta/security/secureScores and divide the result by 100 to get the percentage. # Needs permission SecurityEvents.Read.All $ClientID = '' $ClientSecret = '' $tenant_Id = '' # Connect to Graph # $Body = @{ Grant_Type = "client_credentials" resource = "https://graph.microsoft.com" client_id = $clientId client_secret = $clientSecret } $ConnectGraph = Invoke-RestMethod -Uri "https://login.microsoft.com/$tenant_Id/oauth2/token?api-version=1.0" -Method POST -Body $Body # Variable Collections # $path = "C:\temp\" $Headers = @{ 'Content-Type' = "application/json" 'Authorization' = "Bearer $($ConnectGraph.access_token)" } $token = $ConnectGraph.access_token # Force TLS 1.2. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 function Get-GraphData { param ( [parameter(Mandatory)] [string]$AccessToken, [parameter(Mandatory)] [string]$Uri ) $Headers = @{ 'Authorization' = "Bearer $AccessToken" } do { $Results = Invoke-RestMethod -Uri $Uri -Headers $Headers -ErrorAction Stop $QueryResults += $Results.value $Uri = $Results.'@odata.nextLink' } while ($Uri) return $QueryResults } #This request get SecureScore $uri = "https://graph.microsoft.com/beta/security/secureScores" $Result = @() [array]$Response = Get-GraphData -AccessToken $Token -Uri $uri if ($Response) { ForEach ($Respons in $Response) { $SecureScore = $Respons.currentScore / $Respons.maxScore * 100 $Result += New-Object PSObject -property $([ordered]@{ CreatedDateTime = $Respons.createdDateTime CurrentScore = $Respons.currentScore MaxScore = $Respons.maxScore LicensedUserCount = $Respons.licensedUserCount ActiveUserCount = $Respons.activeUserCount SecureScore = [math]::Round($SecureScore, 2) }) } } else { Write-Host "No SecureScore data found" } # export to csv $Result | export-csv -path "$path\SecureScore.csv" -NoTypeInformation

March 25, 2023 · 2 min · 235 words · Benedikt Gabriel Egilsson

Getting Started with MSGraph API

Authentication and Authorization MSGraph API is a powerful tool for authentication and authorization that allows developers to access Microsoft services and data. It provides a secure way to authenticate users, authorize applications, and manage user data. With MSGraph API, developers can easily integrate their applications with Microsoft services like Outlook, OneDrive, Office 365, Azure Active Directory, and more. In this article, we’ll explore how to get started with MSGraph API concepts and use cases of the API. Authentication We will use the Azure CLI to create a service principal and get the app id and secret. We will also use the Azure CLI to view all the API permissions available in Microsoft Graph. Get the Azure CLI from here {: .prompt-info } ...

March 25, 2023 · 2 min · 352 words · Benedikt Gabriel Egilsson

MS Graph API - Get User info

Microsoft Graph API can be used to get the last logon time of a user. To do this, you will need to make a GET request to the /users/{id}/lastLogonTimeStamp endpoint. This endpoint will return the last logon time of the user in the form of a timestamp. You can then use this timestamp to determine the exact date and time of the user’s last logon. This query get’s. DisplayName,UserPrincipalName,UsageLocation,Contry,LastSignInDateTime,IsLicensed,IsGuestUser {: .prompt-info } ...

March 25, 2023 · 2 min · 316 words · Benedikt Gabriel Egilsson