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 ValueParam(#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 permissionsForEach ($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.IdIf ($AppOwners) {$AppOwnersOutput = $AppOwners.additionalProperties.displayName -join ", "}# Get the app secrets (if any are defined for the app[array]$AppSecrets = $App.passwordCredentialsForEach ($AppSecret in $AppSecrets) {$ExpirationDays = $null; $Status = $nullIf ($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 expiresIf ($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.IdOwners = $AppOwnersOutput"Credential name" = $AppSecret.DisplayName"Created" = $AppSecret.startDateTime"Credential Id" = $AppSecret.KeyId"Expiration" = $AppSecret.endDateTime"Days Until Expiry" = $ExpirationDaysStatus = $StatusRecordType = "Secret"}}$Report.Add($DataLine)}# Process certificates[array]$Certificates = $App.keyCredentialsForEach ($Certificate in $Certificates) {$ExpirationDays = $null; $Status = $nullIf ($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 expiresIf ($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.IdOwners = $AppOwnersOutput"Credential name" = $Certificate.DisplayName"Created" = $Certificate.StartDateTime"Credential Id" = $Certificate.KeyId"Expiration" = $Certificate.endDateTime"Days Until Expiry" = $ExpirationDaysStatus = $StatusRecordType = "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 = $nullIf ($SP) {[array]$AppRoleAssignments = Get-MgServicePrincipalAppRoleAssignment -All -ServicePrincipalId $SP.id -ErrorAction Stop -Verbose:$false# For each assigned permission, find its nameForeach ($AppRoleAssignment in $AppRoleAssignments) {$Permission = (Get-ServicePrincipalRoleById $AppRoleAssignment.resourceId).AppRoles | Where-Object id -match $AppRoleAssignment.AppRoleId | Select-Object -ExpandProperty ValueIf ($Permission -in $HighPriorityPermissions) {$Permission = $Permission + " *"}$Permissions += $Permission}$PermissionsOutput = $Permissions -Join ", "}$DataLine2 = [PSCustomObject] @{"App Name" = $App.DisplayName"App Id" = $App.IdPermissions = $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 reportWrite-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 inSend-MgUserMail -UserId $MsgFrom -BodyParameter $ParamsWrite-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
Author
Office365itpros