Back to script library
Entra / Microsoft 365 · Applications

Report expiring app secrets

Check Entra ID registered applications for app secrets due to expire within the specified window.

Connect & set up

Run these once per session. All scopes are read-only unless the script makes changes.

Connect-MgGraph -Scopes 'Application.Read.All', 'Mail.Send' -NoWelcome

Run it

The main script. Copy it, or download the .ps1 and run it from your console.

param(
[int] $LookbackDays = 7
)
Function Add-MessageRecipients {
# Function to build an addressee list to send email
[cmdletbinding()]
Param(
[array]$ListOfAddresses )
ForEach ($SMTPAddress in $ListOfAddresses) {
@{ emailAddress = @{address = $SMTPAddress}}
}
}
function Get-ServicePrincipalRoleById {
# Original from https://github.com/michevnew/PowerShell/blob/master/app_Permissions_inventory_GraphSDK.ps1
# this function checks a hash table containing details of the service principals used by standard
# apps like the Microsoft Graph. The returned value is the properties of the service principal,
# including the set of roles (permissions) supported by the app. The script can check a
# resource identifier for a permission against the set of roles to find the display name (role name).
# Putting this data in a hash table and retrieving the data from the table is more
# performant than retrieving the permission names each time we process a script by running the
# Get-MgServicePrincipal cmdlet like this:
# $P = (Get-MgServicePrincipal -ServicePrincipalId $AppRoleAssignment.resourceId).appRoles | Where-Object id -match $AppRoleAssignment.AppRoleId | Select-Object -ExpandProperty Value
Param(
#Service principal object
[Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()]$SpID)
If (!$SPHashTable[$SpID]) {
$SPHashTable[$SpID] = Get-MgServicePrincipal -ServicePrincipalId $SpID -Verbose:$false -ErrorAction Stop
}
return $SPHashTable[$spID]
}
$CheckDate = [datetime]::UtcNow.AddDays(-$LookbackDays).ToString("s") + "Z"
# Define the warning period to check for app secrets that are about to expire
[int]$ExpirationWarningPeriod = 30
# CSV Output file
$CSVOutputFile = "C:\temp\AppSecretsAndCerts.CSV"
# Define hash table to hold data for standard apps like the Microsoft Graph (appId = 00000003-0000-0000-c000-000000000000)
$SPHashTable = @{}
# A CSS to use when highlighting issues in the emailed report
$EmailCSS = @"
<style>
BODY{font-family: Arial; font-size: 8pt;}
H1{font-size: 22px; font-family: 'Segoe UI Light';}
H2{font-size: 18px; font-family: 'Segoe UI Light';}
H3{font-size: 16px; font-family: 'Segoe UI Light';}
table {
border-collapse: collapse;
font-size: 10px;
width: 100%;
}
th, td {
border: 1px solid black;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
.active {
background-color: #00FF00;
}
.expiring {
background-color: #FFFF00;
}
.expired {
background-color: #FF0000;
}
</style>
"@
# Recipient for the email sent at the end of the script - define the addresses you want to use here. They can be single recipients,
# distribution lists, or Microsoft 365 groups. Each recipient address is defined as an element in an array
[array]$EmailRecipient = "Email.Admins@office365itpros.com", "Kim.Akers@office365itpros.com"
# When run interactively, email will be sent from the account running the script. This is commented out for use with Azure Automation
# If used with the Mail.Send permission in an Azure Automation runbook, the sender can be any mailbox in the organization
$MsgFrom = (Get-MgContext).Account
# $MsgFrom = "Asomeaccount@somedomain.com"
[array]$HighPriorityPermissions = "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"
# Start of processing
# -------------------
Connect-MgGraph -Scopes 'Application.Read.All', 'Mail.Send' -NoWelcome
# Find registered Entra ID apps that are limited to our organization (not multi-organization)
[array]$RegisteredApps = Get-MgApplication -All -Property Id, appId, displayName, keyCredentials, passwordCredentials, signInAudience -filter "signInAudience eq 'AzureADMyOrg'" | Sort-Object DisplayName
# Remove SharePoint helper apps https://learn.microsoft.com/en-us/answers/questions/1187017/sharepoint-online-client-extensibility-web-applica
$RegisteredApps = $RegisteredApps | Where-Object DisplayName -notLike "SharePoint Online Client Extensibility Web Application Principal*"
If (!($RegisteredApps)) {
Write-Host "Can't retrieve details of any Entra ID registered apps - exiting"
Break
} Else {
Write-Host ("{0} registered applications found - proceeeding to analyze app secrets" -f $RegisteredApps.count)
}
# Populate an array with details of Service Principals for apps that run in this tenant
[array]$ServicePrincipals = Get-MgServicePrincipal -All | Where-Object SignInAudience -match 'AzureADMyOrg'
$Report = [System.Collections.Generic.List[Object]]::new()
$Report2 = [System.Collections.Generic.List[Object]]::new() # For app permissions
ForEach ($App in $RegisteredApps) {
Write-Host ("Processing {0} app" -f $App.DisplayName)
$AppOwnersOutput = "No app owner registered"
# Check for application owners
[array]$AppOwners = Get-MgApplicationOwner -ApplicationId $App.Id
If ($AppOwners) {
$AppOwnersOutput = $AppOwners.additionalProperties.displayName -join ", "
}
# Get the app secrets (if any are defined for the app
[array]$AppSecrets = $App.passwordCredentials
ForEach ($AppSecret in $AppSecrets) {
$ExpirationDays = $null; $Status = $null
If ($null -ne $AppSecret.endDateTime) {
$ExpirationDays = (New-TimeSpan -Start $CheckDate -End $AppSecret.endDateTime).Days
# Figure out app secret status based on the number of days until it expires
If ($ExpirationDays -lt 0) {
$Status = "Expired"
} ElseIf ($ExpirationDays -gt 0 -and $ExpirationDays -le $ExpirationWarningPeriod) {
$Status = "Expiring soon"
} Else {
$Status = "Active"
}
# Record what we found
$DataLine = [PSCustomObject] @{
"App Name" = $App.DisplayName
"App Id" = $App.Id
Owners = $AppOwnersOutput
"Credential name" = $AppSecret.DisplayName
"Created" = $AppSecret.startDateTime
"Credential Id" = $AppSecret.KeyId
"Expiration" = $AppSecret.endDateTime
"Days Until Expiry" = $ExpirationDays
Status = $Status
RecordType = "Secret"
}
}
$Report.Add($DataLine)
}
# Process certificates
[array]$Certificates = $App.keyCredentials
ForEach ($Certificate in $Certificates) {
$ExpirationDays = $null; $Status = $null
If ($null -ne $Certificate.endDateTime) {
# Write-Host ("Certificate {0} has end date {1}" -f $Certificate.displayName, $Certificate.endDateTime)
$ExpirationDays = (New-TimeSpan -Start $CheckDate -End $Certificate.endDateTime).Days
# Figure out app secret status based on the number of days until it expires
If ($ExpirationDays -lt 0) {
$Status = "Expired"
} ElseIf ($ExpirationDays -gt 0 -and $ExpirationDays -le $ExpirationWarningPeriod) {
$Status = "Expiring soon"
} Else {
$Status = "Active"
}
# Record what we found
$DataLine = [PSCustomObject] @{
"App Name" = $App.DisplayName
"App Id" = $App.Id
Owners = $AppOwnersOutput
"Credential name" = $Certificate.DisplayName
"Created" = $Certificate.StartDateTime
"Credential Id" = $Certificate.KeyId
"Expiration" = $Certificate.endDateTime
"Days Until Expiry" = $ExpirationDays
Status = $Status
RecordType = "Certificate"
"Certificate type" = $Certificate.type
}
$Report.Add($DataLine)
}
}
# Retrieve permissions for the app
$SP = $ServicePrincipals | Where-Object AppId -match $App.AppId
# Get permissions assigned to app
$PermissionsOutput = $null; [array]$Permissions = $null
If ($SP) {
[array]$AppRoleAssignments = Get-MgServicePrincipalAppRoleAssignment -All -ServicePrincipalId $SP.id -ErrorAction Stop -Verbose:$false
# For each assigned permission, find its name
Foreach ($AppRoleAssignment in $AppRoleAssignments) {
$Permission = (Get-ServicePrincipalRoleById $AppRoleAssignment.resourceId).AppRoles | Where-Object id -match $AppRoleAssignment.AppRoleId | Select-Object -ExpandProperty Value
If ($Permission -in $HighPriorityPermissions) {
$Permission = $Permission + " *"
}
$Permissions += $Permission
}
$PermissionsOutput = $Permissions -Join ", "
}
$DataLine2 = [PSCustomObject] @{
"App Name" = $App.DisplayName
"App Id" = $App.Id
Permissions = $PermissionsOutput.Trim()
}
$Report2.Add($DataLine2)
}
$Report = $Report | Sort-Object RecordType, "App Name"
$Report | Export-Csv -NoTypeInformation $CSVOutputFile
# Get set of apps with permissions
$Report2 = $Report2 | Where-Object {([string]::IsNullOrWhiteSpace($_.Permissions)) -eq $false}
# Email the report
Write-Host ("All done - emailing details to {0}" -f ($EmailRecipient -join ", "))
$ToRecipientList = @( $EmailRecipient )
[array]$MsgToRecipients = Add-MessageRecipients -ListOfAddresses $ToRecipientList
$MsgSubject = "Entra ID Registered App Credentials Report"
$HtmlHead = "<h2>Expiring and Active Credentials</h2><p>Current status of Entra ID registered apps and the credentials found for each app.</p>"
$HtmlBody = $Report | Select-Object "App Name", Status, RecordType, Owners, "Credential Name", Expiration, "Days until expiry" | ConvertTo-Html -Fragment
# Add the color coding for the status values
$HTMLBody = $HTMLBody -replace "<head>", "<head>`n$EmailCSS`n"
$HTMLBody = $HTMLBody -replace "<td>Active</td>", "<td style=`"background-color: #00FF00;`">active</td>"
$HTMLBody = $HtmlBody -replace "<td>Expiring Soon</td>", "<td style=`"background-color: #FFFF00;`">expiring</td>"
$HtmlBody = $HtmlBody -replace "<td>Expired</td>", "<td style=`"background-color: #FF0000;`">expired</td>"
# Add details about apps with high-value permissions
$HTMLBody2 = $Report2 | ConvertTo-HTML -Fragment
$HTMLBody2 = "<p><h2>Applications with High-Priority Permissions</h2><p>" + $HTMLBody2 + "Astericked permissions are important<p></p>"
$HTMLMsg = "</body></html><p>" + $HTMLHead + $HTMLBody + $HTMLBody2 + "<p>"
# Construct the message body
$MsgBody = @{
Content = "$($HTMLMsg)"
ContentType = 'html'
}
$Message = @{subject = $MsgSubject}
$Message += @{toRecipients = $MsgToRecipients}
$Message += @{body = $MsgBody}
$Params = @{'message' = $Message}
$Params += @{'saveToSentItems' = $True}
$Params += @{'isDeliveryReceiptRequested' = $True}
# And send the message using the parameters that we've filled in
Send-MgUserMail -UserId $MsgFrom -BodyParameter $Params
Write-Output ("Message containing information about expiring App Secrets for mailboxes sent to {0}!" -f ($EmailRecipient -join ", "))
Write-Output ("Full details are available in the CSV file {0}" -f $CSVOutputFile)

Parameters

ParameterDefaultNotes
-LookbackDays7Number of days ahead to flag expiring app secrets.
Attribution