Hudu – M365 Azure AD App Documentation and Monitoring
When I was working on my DNS monitoring script I realised I could easily apply the same principles in that, to Azure AD Applications. Azure AD Applications can present a huge security risk as they can be used to give unattended full access to all your M365 data. We disable users being able to consent to these by default, but there are always situations where apps can slip through. This script will document all applications present in customer tenants. It will also optionally send you alerts via Teams and/or email when a new application is detected or the existing permissions of applications in case there is a situation where one is compromised and the permissions extended.
I decided to jump this up my priority list after talking about it with Ray and Kelvin on this weeks Technical Deficit
The script will create an Asset per application:

On creation of a new application it will send an alert via Teams:

Or via email:

On permission changes you will also get an alert detailing what has changed:


Running the script
To run the script please make sure you are on at least version 2.1.5.3 of Hudu, by clicking on your name in the top right.
The first time you run the script I recommend you turn off the alerts, otherwise you will get notified for every single app it finds. You can reuse the Webhook from Hudu if you have already configured a Teams one, or configure a new one if you would like the alerts to go elsewhere.
#####################################################################
# Get a Hudu API Key from https://yourhududomain.com/admin/api_keys
$HuduAPIKey = "abcdefghijklmn1234556789"
# Set the base domain of your Hudu instance without a trailing /
$HuduBaseDomain = "https://your.hudu.domain"
$HuduAssetLayoutName = "M365 Azure AD App Permissions"
#####################################################################
########################## Azure AD ###########################
$customerExclude [email protected]("Example Customer 1","Example Customer 2")
$ApplicationId = "Your APP ID"
$ApplicationSecret = ConvertTo-SecureString -AsPlainText "Your App Secret" -Force
$TenantID = "Your Tenant ID"
$RefreshToken = "Your Long Refresh Token"
$ExchangeRefreshToken = "Your Long Exchange Refresh Token"
$upn = '[email protected]'
#####################################################################
# Enable sending alerts on dns change to a teams webhook
$enableTeamsAlerts = $false
$teamsWebhook = "https://yourteamswebhookurl.com/1234567898"
# Enable sending alerts on dns change to an email address
$enableEmailAlerts = $false
$mailTo = "[email protected]"
$mailFrom = "[email protected]"
$mailServer = "mail.domain.com"
$mailPort = "25"
$mailUseSSL = $true
$mailUser = "user"
$mailPass = "pass"
#####################################################################
function Send-TeamsAlert {
Param (
[PSCustomObject]$JSONBody
)
if ($enableTeamsAlerts) {
$TeamMessageBody = ConvertTo-Json $JSONBody -Depth 100
$parameters = @{
"URI" = $teamsWebhook
"Method" = 'POST'
"Body" = $TeamMessageBody
"ContentType" = 'application/json'
}
$result = Invoke-RestMethod @parameters
}
}
function Send-EmailAlert {
Param (
[string]$body,
[string]$mailSubject
)
if ($enableEmailAlerts){
$password = ConvertTo-SecureString $mailPass -AsPlainText -Force
$mailcred = New-Object System.Management.Automation.PSCredential ($mailUser, $password)
$sendMailParams = @{
From = $mailFrom
To = $mailTo
Subject = $mailSubject
Body = $body
SMTPServer = $mailServer
UseSsl = $mailUseSSL
Credential = $mailcred
}
Send-MailMessage @sendMailParams -BodyAsHtml
}
}
function Check-PermChange {
Param (
[string]$currentPerm = '',
[string]$newPerm = '',
[string]$permType = '',
[string]$appName = '',
[string]$companyName = ''
)
$Comp = Compare-Object -ReferenceObject $($currentPerm -split "`n") -DifferenceObject $($newPerm -split "`n")
if ($Comp){
# Send Teams Alert
$removed = ($Comp | where-object -filter {$_.SideIndicator -eq "<="}).InputObject | out-string
$newperms = ($Comp | where-object -filter {$_.SideIndicator -eq "=>"}).InputObject | out-string
$outText = ""
if ($removed){
$outText = "<h3>Removed Permissions</h3><table>$removed</table>"
}
if ($newperms){
$outText = "$outText<h3>New Permissions</h3><table>$newperms</table>"
}
$JSONBody = [PSCustomObject][Ordered]@{
"@type" = "MessageCard"
"@context" = "http://schema.org/extensions"
"summary" = "$companyName - $appName - Azure AD App $permType permission change detected"
"themeColor" = '0078D7'
"sections" = @(
@{
"activityTitle" = "$companyName - $appName - Azure AD App $permType permission change detected"
"markdown" = $true
},
@{
"startGroup" = $true
"text" = $outText
}
)
}
Send-TeamsAlert -JSONBody $JSONBody
$oldVal = ($Comp | where-object -filter {$_.SideIndicator -eq "<="}).InputObject | out-string
$newVal = ($Comp | where-object -filter {$_.SideIndicator -eq "=>"}).InputObject | out-string
$mailSubject = "$companyName - $appName - Azure AD App $permType permission change detected"
$body = "
<style>
table{
border-collapse: collapse;
margin: 5px 0;
font-size: 0.8em;
font-family: sans-serif;
min-width: 400px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
}
h2, p{
font-size: 0.8em;
font-family: sans-serif;
}
th, td {
padding: 5px 5px;
max-width: 400px;
width:auto;
}
thead tr {
background-color: #009879;
color: #ffffff;
text-align: left;
}
tr {
border-bottom: 1px solid #dddddd;
}
tr:nth-of-type(even) {
background-color: #f3f3f3;
}
</style>
<h3>$mailSubject</h3>
$outText
</table>
"
Send-EmailAlert -mailSubject $mailSubject -body $body
}
}
#Get the Hudu API Module if not installed
if (Get-Module -ListAvailable -Name HuduAPI) {
Import-Module HuduAPI
} else {
Install-Module HuduAPI -Force
Import-Module HuduAPI
}
#Set Hudu logon information
New-HuduAPIKey $HuduAPIKey
New-HuduBaseUrl $HuduBaseDomain
$Layout = Get-HuduAssetLayouts -name $HuduAssetLayoutName
if (!$Layout) {
$AssetLayoutFields = @(
@{
label = 'Logo'
field_type = 'RichText'
show_in_list = 'false'
position = 1
},
@{
label = 'Homepage'
field_type = 'RichText'
show_in_list = 'false'
position = 2
},
@{
label = 'Publisher Domain'
field_type = 'RichText'
show_in_list = 'false'
position = 3
},
@{
label = 'OAuth2 Permissions'
field_type = 'RichText'
show_in_list = 'false'
position = 4
},
@{
label = 'App Permissions'
field_type = 'RichText'
show_in_list = 'false'
position = 5
},
@{
label = 'Delegated Permissions'
field_type = 'RichText'
show_in_list = 'false'
position = 6
},
@{
label = 'Granted Date'
field_type = 'Text'
show_in_list = 'false'
position = 7
}
)
Write-Host "Creating New Asset Layout $HuduAssetLayoutName"
$NewLayout = New-HuduAssetLayout -name $HuduAssetLayoutName -icon "fas fa-sitemap" -color "#00adef" -icon_color "#ffffff" -include_passwords $false -include_photos $false -include_comments $false -include_files $false -fields $AssetLayoutFields
$Layout = Get-HuduAssetLayouts -name $HuduAssetLayoutName
}
#Connect to your Azure AD Account.
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID
Connect-AzureAD -AadAccessToken $aadGraphToken.AccessToken -AccountId $UPN -MsAccessToken $graphToken.AccessToken -TenantId $tenantID | Out-Null
$Customers = Get-AzureADContract -All:$true
Disconnect-AzureAD
#Login to Hudu
New-HuduAPIKey $HuduAPIKey
New-HuduBaseUrl $HuduBaseDomain
foreach ($Customer in $Customers) {
#Check if customer should be excluded
if (-Not ($customerExclude -contains $customer.DisplayName)){
$Applications = ''
#First lets check for the company
#Check if they are in Hudu before doing any unnessisary work
$defaultdomain = $customer.DefaultDomainName
$hududomain = Get-HuduWebsites -name "https://$defaultdomain"
if ($($hududomain.id.count) -gt 0) {
write-host "Processing $($customer.Displayname)" -foregroundColor green
$CustAadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes "https://graph.windows.net/.default" -ServicePrincipal -Tenant $customer.CustomerContextId
$CustGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes "https://graph.microsoft.com/.default" -ServicePrincipal -Tenant $customer.CustomerContextId
Connect-AzureAD -AadAccessToken $CustAadGraphToken.AccessToken -AccountId $upn -MsAccessToken $CustGraphToken.AccessToken -TenantId $customer.CustomerContextId | out-null
$Applications = Get-AzureADApplication -All:$true
foreach ($app in $Applications){
$allsp = Get-AzureADServicePrincipal -All $true
write-host "$($app.displayName)" -foregroundColor green
$appName = $app.DisplayName
if ($appName){
$appsp = Get-AzureADServicePrincipal -All $true | Where-Object {$_.AppId -eq $app.AppId}
$AppDelegatedDate = ""
$grantedDate = ""
if ($appsp.ObjectId){
$AppDelegatedDate = ((Get-AzureADServiceAppRoleAssignedTo -ObjectId $appsp.ObjectId).CreationTimestamp)| Sort-Object {[DateTime]$_} | Select -First 1 | Out-String
}
if ($AppDelegatedDate -eq ""){
$AppDelegatedDate = "Unknown"
}
$DelegatedPermissions = [System.Collections.ArrayList]@()
$AppPermissions = [System.Collections.ArrayList]@()
$OAuth2Permissions = [System.Collections.ArrayList]@()
foreach ($oauth in $app.Oauth2Permissions){
$perm = $oauth | select @{N='Permission'; E={$_.value}}, @{N='Name'; E={$_.AdminConsentDisplayName}}, @{N='Description'; E={$_.AdminConsentDescription}}
$null = $OAuth2Permissions.add($perm)
}
foreach ($reqperm in $App.requiredResourceAccess){
$sp = $allsp | Where-Object {$_.AppId -eq $reqperm.ResourceAppId}
foreach ($permission in $($reqperm.ResourceAccess)){
if ($permission.type -eq "Scope") {
$perm = $sp.Oauth2Permissions | Where-Object {$_.Id -eq $permission.id} | select @{N='Permission'; E={$_.value}}, @{N='Name'; E={$_.AdminConsentDisplayName}}, @{N='Description'; E={$_.AdminConsentDescription}}
$null = $DelegatedPermissions.add($perm)
}
if ($permission.type -eq "Role") {
$perm = $sp.AppRoles | Where-Object {$_.Id -eq $permission.id} | select @{N='Permission'; E={$_.value}}, @{N='Name'; E={$_.DisplayName}}, @{N='Description'; E={$_.Description}}
$null = $AppPermissions.add($perm)
}
}
}
$OAuthHTML = ($OAuth2Permissions | ConvertTo-Html -Fragment | Out-String) -replace "[^a-zA-Z0-9. ,&`"[email protected]<>/#_;]", ''
$OAuthHTML = $OAuthHTML -replace "'", ''
$AppHTML = ($AppPermissions | ConvertTo-Html -Fragment | Out-String) -replace "[^a-zA-Z0-9. ,&`"[email protected]<>/#_;]", ''
$AppHTML = $AppHTML -replace "'", ''
$DelegatedHTML = ($DelegatedPermissions | ConvertTo-Html -Fragment | Out-String) -replace "[^a-zA-Z0-9. ,&`"[email protected]<>/#_;]", ''
$DelegatedHTML = $DelegatedHTML -replace "'", ''
if ($($app.AppLogoURL)){
$LogoHTML = "<img src=`"$($app.AppLogoURL)`"></img>"
} else {
$LogoHTML = ''
}
$AssetFields = @{
'logo' = $LogoHTML
'homepage' = $($app.Homepage)
'publisher_domain' = $($app.PublisherDomain)
'oauth2_permissions' = $OAuthHTML
'app_permissions' = $AppHTML
'delegated_permissions' = $DelegatedHTML
'granted_date' = $AppDelegatedDate
}
$companyid = $hududomain.company_id
$companyName = $hududomain.company_name
#Swap out # as Hudu doesn't like it when searching
$AssetName = $appName
#Check if there is already an asset
$Asset = Get-HuduAssets -name $AssetName -companyid $companyid -assetlayoutid $Layout.id
#If the Asset does not exist, we edit the body to be in the form of a new asset, if not, we just upload.
if (!$Asset) {
Write-Host "Creating new Asset"
$outText = ""
if (($OAuthHTML | measure-object -line).lines -gt 2){
$outText = "OAuth Permissions: $OAuthHTML"
}
if (($AppHTML | measure-object -line).lines -gt 2){
write-host "AppHTML"
$outText = "$outText App Permissions: $AppHTML"
}
if (($DelegatedHTML | measure-object -line).lines -gt 2){
$outText = "$outText Delegated Permissions: $DelegatedHTML"
}
$JSONBody = [PSCustomObject][Ordered]@{
"@type" = "MessageCard"
"@context" = "http://schema.org/extensions"
"summary" = "$companyName - $appName - Azure AD - New Application Detected"
"themeColor" = '0078D7'
"sections" = @(
@{
"activityTitle" = "$companyName - $appName - Azure AD: New Application Detected"
"markdown" = $true
},
@{
"startGroup" = $true
"text" = $outText
}
)
}
Send-TeamsAlert -JSONBody $JSONBody
Write-Host "Sent Teams Alert"
$mailSubject = "$companyName - $appName - Azure AD: New Application Detected"
$body = "
<style>
table{
border-collapse: collapse;
margin: 5px 0;
font-size: 0.8em;
font-family: sans-serif;
min-width: 400px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
}
h2, p{
font-size: 0.8em;
font-family: sans-serif;
}
th, td {
padding: 5px 5px;
max-width: 400px;
width:auto;
}
thead tr {
background-color: #009879;
color: #ffffff;
text-align: left;
}
tr {
border-bottom: 1px solid #dddddd;
}
tr:nth-of-type(even) {
background-color: #f3f3f3;
}
</style>
<h3>$mailSubject</h3>
$outText
"
Send-EmailAlert -mailSubject $mailSubject -body $body
Write-Host "Sent Email Alert"
$Asset = New-HuduAsset -name $AssetName -company_id $companyid -asset_layout_id $Layout.id -fields $AssetFields
Write-Host "Created Asset"
}
else {
$oauth_cur_value = ($Asset.fields | where-object -filter {$_.label -eq "OAuth2 Permissions"}).value
$app_cur_value = ($Asset.fields | where-object -filter {$_.label -eq "App Permissions"}).value
$del_cur_value = ($Asset.fields | where-object -filter {$_.label -eq "Delegated Permissions"}).value
Check-PermChange -currentPerm $oauth_cur_value -newPerm $OAuthHTML -permType "OAuth2" -appName $AssetName -companyName $hududomain.company_name
Check-PermChange -currentPerm $app_cur_value -newPerm $AppHTML -permType "App" -appName $AssetName -companyName $hududomain.company_name
Check-PermChange -currentPerm $del_cur_value -newPerm $DelegatedHTML -permType "Delegated" -appName $AssetName -companyName $hududomain.company_name
Write-Host "Updating Asset"
$Asset = Set-HuduAsset -asset_id $Asset.id -name $AssetName -company_id $companyid -asset_layout_id $Layout.id -fields $AssetFields
}
}
}
Disconnect-AzureAD
}
}
}
The script is also on Github here: https://github.com/lwhitelock/HuduAutomation/blob/main/M365_Apps_Hudu.ps1