PS script for Entra ID Application report including secret/cert details


As Microsoft has discontinued legacy authentication and excluding service accounts from multi-factor authentication comes with own risk, everyone moved or would need to move to app registrations sooner or later. This leads to another problem that how to monitor all service principals, keep on renewing certificates/ secrets and also keep an eye on the service principals with excessive permissions (get real, most admins don’t understand the difference between application permissions/ delegated permissions and end up creating apps with dangerous permissions).

I wrote the below script which should help in addressing the challenges and considering it uses only Graph API calls, you need only the authentication module Microsoft.Graph.Authentication

<#
Author : Nitish Kumar (nitish@nitishkumar.net)
Performs Entra ID Assessment
version 1.0 | 17/07/2024 Initial version
version 1.2 | 28/07/2024 Application details performance improvements
Disclaimer: This script is designed to only read data from the entra id and should not cause any problems or change configurations but author do not claim to be responsible for any issues. Do due dilligence before running in the production environment
#>
Import-module Microsoft.Graph.Authentication
# Output formating options
$logopath = "https://raw.githubusercontent.com/laymanstake/laymanstake/master/images/logo.png&quot;
$ReportPath = "c:\temp\EntraIDReport_$(get-date -Uformat "%Y%m%d-%H%M%S").html"
$CopyRightInfo = " @Copyright Nitish Kumar <a href='https://github.com/laymanstake'>Visit nitishkumar.net</a>"
# CSS codes to format the report
$header = @"
<style>
body { background-color: #D3D3D3; }
h1 { font-family: Arial, Helvetica, sans-serif; color: #e68a00; font-size: 28px; }
h2 { font-family: Arial, Helvetica, sans-serif; color: #000099; font-size: 16px; }
table { font-size: 12px; border: 1px; font-family: Arial, Helvetica, sans-serif; }
td { padding: 4px; margin: 0px; border: 1; }
th { background: #395870; background: linear-gradient(#49708f, #293f50); color: #fff; font-size: 11px; text-transform: uppercase; padding: 10px 15px; vertical-align: middle; }
tbody tr:nth-child(even) { background: #f0f0f2; }
CreationDate { font-family: Arial, Helvetica, sans-serif; color: #ff3300; font-size: 12px; }
</style>
"@
If ($logopath) {
$header = $header + "<img src=$logopath alt='Company logo' width='150' height='150' align='right'>"
}
# Function to parse datetime string with different cultures
function Convert-ToDateTime {
param (
[string[]]$dateStrings
)
# List of cultures to test
$cultures = @('en-US', 'en-GB', 'fr-FR', 'de-DE', 'es-ES', 'en-IN')
$results = @()
if(-Not $dateStrings){
return $null
}
foreach ($dateString in $dateStrings) {
if ([string]::IsNullOrEmpty($dateString)) {
$results += $null
continue
}
$parsed = $null
foreach ($culture in $cultures) {
try {
$cultureInfo = [System.Globalization.CultureInfo]::GetCultureInfo($culture)
$parsed = [datetime]::Parse($dateString, $cultureInfo)
break
} catch {
# Continue to the next culture if parsing fails
continue
}
}
if (-NOT $parsed) {
throw "Unable to parse date string: $dateString"
}
$results += $parsed.ToString("dd-MM-yyyy HH:mm:ss")
}
return $results
}
function Get-SensitiveApps {
[CmdletBinding()]
Param(
[Parameter(ValueFromPipeline = $true, mandatory = $false)][array]$Sensitivepermissions = ("User.Read.All", "User.ReadWrite.All", "Mail.ReadWrite", "Files.ReadWrite.All", "Calendars.ReadWrite", "Mail.Send", "User.Export.All", "Directory.Read.All", "Exchange.ManageAsApp", "Directory.ReadWrite.All", "Sites.ReadWrite.All", "Application.ReadWrite.All", "Group.ReadWrite.All", "ServicePrincipalEndPoint.ReadWrite.All", "GroupMember.ReadWrite.All", "RoleManagement.ReadWrite.Directory", "AppRoleAssignment.ReadWrite.All")
)
# Populate a set of hash tables with permissions used for different Office 365 management functions
$GraphApp = (invoke-MgGraphRequest -uri "https://graph.microsoft.com/v1.0/serviceprincipals?`$filter=appid eq '00000003-0000-0000-c000-000000000000'").value
$GraphRoles = @{}
ForEach ($Role in $GraphApp.AppRoles) { $GraphRoles.Add([string]$Role.Id, [string]$Role.Value) }
$ExoPermissions = @{}
$ExoApp = (invoke-MgGraphRequest -uri "https://graph.microsoft.com/v1.0/serviceprincipals?`$filter=appid eq '00000002-0000-0ff1-ce00-000000000000'").value
ForEach ($Role in $ExoApp.AppRoles) { $ExoPermissions.Add([string]$Role.Id, [string]$Role.Value) }
$O365Permissions = @{}
$O365API = (invoke-MgGraphRequest -uri "https://graph.microsoft.com/v1.0/serviceprincipals?`$filter=DisplayName eq 'Office 365 Management APIs'").value
ForEach ($Role in $O365API.AppRoles) { $O365Permissions.Add([string]$Role.Id, [string]$Role.Value) }
$AzureADPermissions = @{}
$AzureAD = (invoke-MgGraphRequest -uri "https://graph.microsoft.com/v1.0/serviceprincipals?`$filter=DisplayName eq 'Windows Azure Active Directory'").value
ForEach ($Role in $AzureAD.AppRoles) { $AzureADPermissions.Add([string]$Role.Id, [string]$Role.Value) }
$TeamsPermissions = @{}
$TeamsApp = (invoke-MgGraphRequest -uri "https://graph.microsoft.com/v1.0/serviceprincipals?`$filter=DisplayName eq 'Skype and Teams Tenant Admin API'").value
ForEach ($Role in $TeamsApp.AppRoles) { $TeamsPermissions.Add([string]$Role.Id, [string]$Role.Value) }
$RightsManagementPermissions = @{}
$RightsManagementApp = (invoke-MgGraphRequest -uri "https://graph.microsoft.com/v1.0/serviceprincipals?`$filter=DisplayName eq 'Microsoft Rights Management Services'").value
ForEach ($Role in $RightsManagementApp.AppRoles) { $RightsManagementPermissions.Add([string]$Role.Id, [string]$Role.Value) }
$Appdetails = @()
$sps = @()
$managedidentities = @()
$appcreds = @()
$approles = @()
$Sensitivepermissions = ("User.Read.All", "User.ReadWrite.All", "Mail.ReadWrite", "Files.ReadWrite.All", "Calendars.ReadWrite", "Mail.Send", "User.Export.All", "Directory.Read.All", "Exchange.ManageAsApp", "Directory.ReadWrite.All", "Sites.ReadWrite.All", "Application.ReadWrite.All", "Group.ReadWrite.All", "ServicePrincipalEndPoint.ReadWrite.All", "GroupMember.ReadWrite.All", "RoleManagement.ReadWrite.Directory", "AppRoleAssignment.ReadWrite.All")
$uri = "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=tags/any(t:t+eq+'WindowsAzureActiveDirectoryIntegratedApp')&`$top=999&`$select=id,appid,displayname,createdDateTime,accountEnabled,servicePrincipalType,signInAudience,appRoleAssignmentRequired,appOwnerOrganizationId"
do {
$response = Invoke-MgGraphRequest -Uri $uri
$apps = $response.value
$SPs += $apps
$uri = $response.'@odata.nextLink'
} while ($uri)
$Uri = "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=ServicePrincipalType eq 'ManagedIdentity'&`$top=999&`$select=id,appid,displayname,createdDateTime,accountEnabled,servicePrincipalType,signInAudience,appRoleAssignmentRequired,appOwnerOrganizationId"
do {
$response = Invoke-MgGraphRequest -Uri $uri
$apps = $response.value
$managedidentities += $apps
$uri = $response.'@odata.nextLink'
} while ($uri)
$AllApps = $SPs + $managedidentities
$Uri = "https://graph.microsoft.com/v1.0/applications?`$select=appid,passwordCredentials,keycredentials&`$top=999"
do {
$response = Invoke-MgGraphRequest -Uri $uri
$apps = $response.value
$appcreds += $apps
$uri = $response.'@odata.nextLink'
} while ($uri)
$Uri = "https://graph.microsoft.com/v1.0/serviceprincipals?`$top=999&`$expand=appRoleAssignments&`$select=appId,appRoleAssignments"
do {
$response = Invoke-MgGraphRequest -Uri $uri
$apps = $response.value
$approles += $apps
$uri = $response.'@odata.nextLink'
} while ($uri)
$i = 0
$count = $AllApps.count
ForEach ($app in $AllApps) {
$i++
Write-Progress -Activity "Processing $($app.displayName)" -Status "$i of $count completed" -PercentComplete ($i * 100 / $count)
$Roles = $null
$Roles = $approles | Where-Object { $_.appid -eq $app.appid }
[array]$Permission = $Null
$spermissions = $null
if (($Roles.count) -gt 0) {
ForEach ($Approle in $Roles.appRoleAssignments) {
Switch ($AppRole.ResourceDisplayName) {
"Microsoft Graph" {
$Permission += $GraphRoles[$AppRole.AppRoleId]
}
"Office 365 Exchange Online" {
$Permission += $ExoPermissions[$AppRole.AppRoleId]
}
"Office 365 Management APIs" {
$Permission += $O365Permissions[$AppRole.AppRoleId]
}
"Windows Azure Active Directory" {
$Permission += $AzureADPermissions[$AppRole.AppRoleId]
}
"Skype and Teams Tenant Admin API" {
$Permission += $TeamsPermissions[$AppRole.AppRoleId]
}
"Microsoft Rights Management Services" {
$Permission += $RightsManagementPermissions[$AppRole.AppRoleId]
}
}
}
if ($Permission) {
$spermissions = (compare-object -ReferenceObject ($Permission | Where-Object { $_ }) -DifferenceObject $Sensitivepermissions -IncludeEqual | Where-Object { $_.SideIndicator -eq "==" }).inputobject
}
}
$secrets = @()
$secrets = $appcreds | Where-Object { $_.appid -eq $app.appid }
$passwords = $secrets.passwordcredentials | ForEach-Object { [pscustomobject]@{displayname = $_.displayname; startdatetime = $_.startdatetime; enddatetime = $_.enddatetime } }
$certs = $secrets.keycredentials | ForEach-Object { [pscustomobject]@{displayname = $_.displayname; startdatetime = $_.startdatetime; enddatetime = $_.enddatetime; usage = $_.usage; type = $_.type; customKeyIdentifier = $_.customKeyIdentifier } }
$temp = [pscustomobject]@{
id = $app.id
displayName = $app.displayName
createdDateTime = $app.createdDateTime
enabled = $app.accountEnabled
servicePrincipalType = $app.servicePrincipalType
permissions = $permission -join "`n"
sensitivepermissions = $spermissions -join "`n"
secretdisplayname = $passwords.displayname -join "`n"
secretstartdate = (Convert-ToDateTime -dateStrings $passwords.startdatetime) -join "`n"
secretenddate = (Convert-ToDateTime -dateStrings $passwords.enddatetime) -join "`n"
certdisplayname = $certs.displayname -join "`n"
certthumbprint = $certs.customKeyIdentifier -join "`n"
certstartdate = (Convert-ToDateTime -dateStrings $certs.startdatetime) -join "`n"
certenddate = (Convert-ToDateTime -dateStrings $certs.enddatetime) -join "`n"
certusage = $certs.usage -join "`n"
certtype = $certs.type -join "`n"
signInAudience = $app.signInAudience
appRoleAssignmentRequired = $app.appRoleAssignmentRequired
appOwnerOrganizationId = $app.appOwnerOrganizationId
}
$Appdetails += $temp
}
return $Appdetails
}
$threshold = 30 # number of days after which cert/secret would be expired
$apps = @()
$expiringsecrets = @()
$expiringcerts = @()
$sensitiveapps = @()
disconnect-mggraph
Connect-MgGraph -NoWelcome -scopes Directory.read.all
$ConnectionDetail = Get-mgContext
clear-host
if ($ConnectionDetail.scopes -contains "Directory.Read.All") {
$apps = Get-SensitiveApps
$expiringsecrets = ($apps | Where-Object { $_.secretenddate }) | Where-Object { (($_.secretenddate -split "`n" | ForEach-Object { [datetime]::ParseExact($_, "dd-MM-yyyy HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture) }) | Measure-Object -Maximum).maximum -lt (get-date).Adddays($threshold) }
$expiringcerts = ($apps | Where-Object { $_.certenddate }) | Where-Object { (($_.certenddate -split "`n" | ForEach-Object { [datetime]::ParseExact($_, "dd-MM-yyyy HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture)}) | Measure-Object -Maximum).maximum -lt (get-date).Adddays($threshold) }
$sensitiveapps = $apps | Where-Object { $_.sensitivepermissions }
}
$sensitiveapps | export-csv -nti c:\temp\appreport.csv
$expiringcerts | export-csv -nti c:\temp\appreport-expiringcerts.csv
$expiringsecrets | export-csv -nti c:\temp\appreport-expiringsecerts.csv
if ($sensitiveapps) {
$sensitiveappssummary = ($sensitiveapps | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Sensitive apps Summary</h2>") -replace "`n", "<br>"
}
if ($expiringcerts) {
$expiringcertssummary = ($expiringcerts | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Expiriring certificates Summary</h2>") -replace "`n", "<br>"
}
if ($expiringsecrets) {
$expiringsecretssummary = ($expiringsecrets | ConvertTo-Html -As Table -Fragment -PreContent "<h2>Expiring secrets Summary</h2>") -replace "`n", "<br>"
}
$ReportRaw = ConvertTo-HTML -Body "$sensitiveappssummary $expiringcertssummary $expiringsecretssummary" -Head $header -Title "Report on Entra ID: $($TenantBasicDetail.Displayname)" -PostContent "<p id='CreationDate'>Creation Date: $(Get-Date) $CopyRightInfo </p>"
# To preseve HTMLformatting in description
$ReportRaw = [System.Web.HttpUtility]::HtmlDecode($ReportRaw)
$ReportRaw | Out-File $ReportPath
Invoke-item $ReportPath
# $expiringsecrets , $expiringcerts and $sensitiveapps variables can be used to get particular details.
<# $MailCredential = Get-Credential -Message "Enter the password for the email account: " -UserName "contactfor_nitish@hotmail.com"
$body = Get-Content $ReportPath1 -Raw
New-Email -RecipientAddressTo "nitish@nitishkumar.net" -SenderAddress "contactfor_nitish@hotmail.com" -SMTPServer "smtp.office365.com" -SMTPServerPort 587 -Subject "AD Assessment Report $(get-date -Uformat "%Y%m%d-%H%M%S")" -Body $body -credential $MailCredential #>

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.