2025-05-02 10:57:39 +05:30
<#
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
Name : Send Microsoft Entra App Credentials Expiry Notifications
2025-06-18 13:48:58 +05:30
Version : 1.1
2025-05-02 10:57:39 +05:30
Website : o365reports . com
Script Highlights :
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
1 . Sends app credential expiry notifications to specific users .
2 . Sends notifications for expiring certificates alone , client secrets alone , or both .
3 . Exports a list of apps with expiring credentials within the specified days in CSV format .
4 . Allows sending emails on behalf of other users .
5 . Automatically install the Microsoft Graph PowerShell module ( if not installed already ) upon your confirmation .
6 . The script can be executed with an MFA-enabled account too .
7 . It can be executed with certificate-based authentication ( CBA ) too .
8 . The script is scheduler-friendly .
2025-06-18 13:48:58 +05:30
Change Log
~ ~ ~ ~ ~ ~ ~ ~ ~ ~
V1 . 0 ( Apr 29 , 2025 ) - File created
V1 . 1 ( Jun 14 , 2025 ) - Minor code improvements .
2025-05-02 10:57:39 +05:30
For detailed Script execution : https : / / o365reports . com / 2025 / 04 / 29 / send-entra -app -credential -expiry -notifications
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
#>
Param
(
[ Parameter ( Mandatory = $True ) ]
[ int ] $SoonToExpireInDays ,
[ Parameter ( Mandatory = $True ) ]
[ string ] $Recipients ,
[ string ] $FromAddress ,
[ Switch ] $ClientSecretsOnly ,
[ Switch ] $CertificatesOnly ,
[ Switch ] $StoreReportLocally ,
[ string ] $TenantId ,
[ string ] $ClientId ,
[ string ] $CertificateThumbprint
)
$CSVFilePath = " $( Get-Location ) \AppCertsAndSecretsExpiryNotificationSummary_ $( ( Get-Date -format yyyy-MMM -dd -ddd ` hh-mm ` tt ) . ToString ( ) ) .csv "
2025-06-18 13:48:58 +05:30
# Check if Microsoft Graph module is installed
$MsGraphModule = Get-Module Microsoft . Graph -ListAvailable
if ( $MsGraphModule -eq $null ) {
Write-Host " `n Important: Microsoft Graph module is unavailable. It is mandatory to have this module installed in the system to run the script successfully. "
$confirm = Read-Host " Are you sure you want to install Microsoft Graph module? [Y] Yes [N] No "
if ( $confirm -match " [yY] " ) {
Write-Host " Installing Microsoft Graph module... "
Install-Module Microsoft . Graph -Scope CurrentUser -AllowClobber
Write-Host " Microsoft Graph module is installed in the machine successfully " -ForegroundColor Magenta
2025-05-02 10:57:39 +05:30
} else {
2025-06-18 13:48:58 +05:30
Write-Host " Exiting. `n Note: Microsoft Graph module must be available in your system to run the script " -ForegroundColor Red
Exit
2025-05-02 10:57:39 +05:30
}
2025-06-18 13:48:58 +05:30
}
2025-05-02 10:57:39 +05:30
2025-06-18 13:48:58 +05:30
Write-Host " `n Connecting to Microsoft Graph... "
if ( ( $TenantId -ne " " ) -and ( $ClientId -ne " " ) -and ( $CertificateThumbprint -ne " " ) ) {
# Use certificate-based authentication if TenantId, ClientId, and CertificateThumbprint are provided
Connect-MgGraph -TenantId $TenantId -AppId $ClientId -CertificateThumbprint $CertificateThumbprint -NoWelcome
} else {
# Use delegated permissions (Scopes) if credentials are not provided
Connect-MgGraph -Scopes " Application.Read.All " , " Mail.Send.Shared " , " User.Read.All " -NoWelcome
}
# Verify connection
if ( ( Get-MgContext ) -ne $null ) {
if ( ( Get-MgContext ) . Account -ne $null ) {
$LoggedInAccount = ( Get-MgContext ) . Account
if ( [ string ] :: IsNullOrEmpty ( $FromAddress ) ) {
$FromAddress = $LoggedInAccount
2025-05-02 10:57:39 +05:30
}
2025-06-18 13:48:58 +05:30
Write-Host " Connected to Microsoft Graph PowerShell using account: $( $LoggedInAccount ) "
}
else {
Write-Host " Connected to Microsoft Graph PowerShell using certificate-based authentication. "
if ( [ string ] :: IsNullOrEmpty ( $FromAddress ) ) {
Write-Host " `n Error: FromAddress is required when using certificate-based authentication. " -ForegroundColor Red
Exit
2025-05-02 10:57:39 +05:30
}
}
2025-06-18 13:48:58 +05:30
} else {
Write-Host " Failed to connect to Microsoft Graph. " -ForegroundColor Red
Exit
2025-05-02 10:57:39 +05:30
}
# Function to Send Email
function SendEmail {
2025-06-18 13:48:58 +05:30
$EmailAddresses = ( $Recipients -split " , " ) . Trim ( )
$toRecipients = @ ( )
foreach ( $Email in $EmailAddresses ) {
$toRecipients + = @ {
emailAddress = @ {
address = $Email
}
}
}
2025-05-02 10:57:39 +05:30
$Script:TableContent + = " </table> "
$TableStyle = " <style>
table { width : 100 % ; border-collapse : collapse ; font-family : Arial , sans-serif ; }
th , td { border : 1px solid black ; padding : 8px ; text-align : left ; }
th { background-color : #f2f2f2; }
< / style > "
$MailContent = " $( $TableStyle )
< p > Hello Admin , < / p >
< p > These application credentials are soon to expire : < / p >
$ ( $Script:TableContent )
< p > To prevent authentication failures and service disruptions , please renew the expiring secret or certificate via the < a href = 'https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade/quickStartType~/null/sourceType/Microsoft_AAD_IAM' target = '_blank' > App registrations < / a > in Microsoft Entra admin center . < / p >
< p > If you have any questions , feel free to contact IT support . < / p >
< p > Best regards , < br > IT Admin Team < / p > "
$params = @ {
message = @ {
subject = " Entra App Registration Credentials Expiry Notification "
body = @ {
contentType = " HTML "
content = $MailContent
}
toRecipients = $toRecipients
}
}
2025-06-18 13:48:58 +05:30
Send-MgUserMail -UserId $FromAddress -BodyParameter $params
}
2025-05-02 10:57:39 +05:30
$ExportResult = $null
$AppCount = 0
$Script:ProcessedCount = 0
$RequiredProperties = @ ( 'DisplayName' , 'AppId' , 'Id' , 'KeyCredentials' , 'PasswordCredentials' , 'CreatedDateTime' , 'SigninAudience' )
if ( ( $CertificatesOnly . IsPresent ) -or ( $ClientSecretsOnly . IsPresent ) -or ( $SoonToExpireInDays -ne " " ) ) {
$SwitchPresent = $True
}
else {
$SwitchPresent = $false
}
# Create an HTML table with data
$Script:TableContent = " <table> "
$Script:TableContent + = " <tr><th>App Name</th><th>App Creation Time</th><th>Credential Type</th><th>Credential Name</th><th>Creation Time</th><th>Expiry Date</th><th>Friendly Expiry Date</th></tr> "
Get-MgApplication -All -Property $RequiredProperties | ForEach-Object {
$AppCount + +
$AppName = $_ . DisplayName
Write-Progress -Activity " `n Processed App registration: $AppCount - $AppName "
$AppId = $_ . Id
$Secrets = $_ . PasswordCredentials
$Certificates = $_ . KeyCredentials
$AppCreationDate = $_ . CreatedDateTime
$SigninAudience = $_ . SignInAudience
$AppOwners = ( Get-MgApplicationOwner -ApplicationId $AppId ) . AdditionalProperties . userPrincipalName
$Owners = $AppOwners -join " , "
if ( $owners -eq " " ) { $Owners = " - " }
#Process through Secret keys
if ( ! ( $CertificatesOnly . IsPresent ) -or ( $SwitchPresent -eq $false ) ) {
foreach ( $Secret in $Secrets ) {
$CredentialType = " Client Secret "
$DisplayName = $Secret . DisplayName
$Id = $Secret . KeyId
$CreatedTime = $Secret . StartDateTime
$ExpiryDate = $Secret . EndDateTime
$ExpiryStatusCalculation = ( New-TimeSpan -Start ( Get-Date ) . Date -End $ExpiryDate ) . Days
$FriendlyExpiryTime = " Expires in $ExpiryStatusCalculation days "
if ( ( $ExpiryStatusCalculation -gt 0 ) -and ( $ExpiryStatusCalculation -le $SoonToExpireInDays ) ) {
$Script:ProcessedCount + +
if ( $StoreReportLocally . IsPresent ) {
$ExportResult = [ PSCustomObject ] @ { 'App Name' = $AppName ; 'App Owners' = $Owners ; 'App Creation Time' = $AppCreationDate ; 'Credential Type' = $CredentialType ; 'Credential Name' = $DisplayName ; 'Credential Id' = $Id ; 'Creation Time' = $CreatedTime ; 'Expiry Date' = $ExpiryDate ; 'Days to Expiry' = $ExpiryStatusCalculation ; 'Friendly Expiry Date' = $FriendlyExpiryTime ; 'App Id' = $AppId }
$ExportResult | Export-Csv -Path $CSVFilePath -Notype -Append
}
$Script:TableContent + = " <tr><td> $( $AppName ) </td><td> $( $AppCreationDate ) </td><td> $( $CredentialType ) </td><td> $( $DisplayName ) </td><td> $( $CreatedTime ) </td><td> $( $ExpiryDate ) </td><td> $( $FriendlyExpiryTime ) </td></tr> "
}
}
}
#Process through Certificates
if ( ! ( $ClientSecretsOnly . IsPresent ) -or ( $SwitchPresent -eq $false ) ) {
foreach ( $Certificate in $Certificates ) {
$CredentialType = " Certificate "
$DisplayName = $Certificate . DisplayName
$Id = $Certificate . KeyId
$CreatedTime = $Certificate . StartDateTime
$ExpiryDate = $Certificate . EndDateTime
$ExpiryStatusCalculation = ( New-TimeSpan -Start ( Get-Date ) . Date -End $ExpiryDate ) . Days
$FriendlyExpiryTime = " Expires in $ExpiryStatusCalculation days "
if ( ( $ExpiryStatusCalculation -gt 0 ) -and ( $ExpiryStatusCalculation -le $SoonToExpireInDays ) ) {
$Script:ProcessedCount + +
if ( $StoreReportLocally . IsPresent ) {
$ExportResult = [ PSCustomObject ] @ { 'App Name' = $AppName ; 'App Owners' = $Owners ; 'App Creation Time' = $AppCreationDate ; 'Credential Type' = $CredentialType ; 'Credential Name' = $DisplayName ; 'Credential Id' = $Id ; 'Creation Time' = $CreatedTime ; 'Expiry Date' = $ExpiryDate ; 'Days to Expiry' = $ExpiryStatusCalculation ; 'Friendly Expiry Date' = $FriendlyExpiryTime ; 'App Id' = $AppId }
$ExportResult | Export-Csv -Path $CSVFilePath -Notype -Append
}
$Script:TableContent + = " <tr><td> $( $AppName ) </td><td> $( $AppCreationDate ) </td><td> $( $CredentialType ) </td><td> $( $DisplayName ) </td><td> $( $CreatedTime ) </td><td> $( $ExpiryDate ) </td><td> $( $FriendlyExpiryTime ) </td></tr> "
}
}
}
}
if ( $Script:ProcessedCount -ne 0 ) {
SendEmail
Write-Host ` n " $Script:ProcessedCount app(s) credential is about to expire in $SoonToExpireInDays days. " -ForegroundColor Yellow
Write-Host ` n " Email has been sent successfully. " -ForegroundColor Yellow
if ( ( Test-Path -Path $CSVFilePath ) -eq " True " ) {
Write-Host ` n " The output file saved in: " -NoNewline -ForegroundColor Yellow
Write-Host $CSVFilePath
}
} else {
Write-Host ` n " No app(s) found with secrets or certificates expiring in the next $SoonToExpireInDays days. " -ForegroundColor Yellow
}
# Disconnect from Microsoft Graph
Disconnect-MgGraph | Out-Null
Write-Host ` n ~ ~ Script prepared by AdminDroid Community ~ ~ ` n -ForegroundColor Green
Write-Host " ~~ Check out " -NoNewline -ForegroundColor Green ; Write-Host " admindroid.com " -ForegroundColor Yellow -NoNewline ; Write-Host " to get access to 1900+ Microsoft 365 reports. ~~ " -ForegroundColor Green