diff --git a/Notify Entra App Credential Expiry/AppCertsAndSecretsExpiryNotification.ps1 b/Notify Entra App Credential Expiry/AppCertsAndSecretsExpiryNotification.ps1 new file mode 100644 index 0000000..1abb366 --- /dev/null +++ b/Notify Entra App Credential Expiry/AppCertsAndSecretsExpiryNotification.ps1 @@ -0,0 +1,238 @@ +<# +============================================================================================= +Name: Send Microsoft Entra App Credentials Expiry Notifications +Version: 1.0 +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. + + +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 +) + + +$Date = Get-Date +$CSVFilePath ="$(Get-Location)\AppCertsAndSecretsExpiryNotificationSummary_$((Get-Date -format yyyy-MMM-dd-ddd` hh-mm` tt).ToString()).csv" + + +# Function to connect to Microsoft Graph +function Connect_ToMgGraph { + # Check if Microsoft Graph module is installed + $MsGraphModule = Get-Module Microsoft.Graph -ListAvailable + if ($MsGraphModule -eq $null) { + Write-Host "`nImportant: 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 + } else { + Write-Host "Exiting. `nNote: Microsoft Graph module must be available in your system to run the script" -ForegroundColor Red + Exit + } + } + + Write-Host "`nConnecting 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) { + Write-Host "Connected to Microsoft Graph PowerShell using account: $((Get-MgContext).Account)" + } + else { + Write-Host "Connected to Microsoft Graph PowerShell using certificate-based authentication." + } + } else { + Write-Host "Failed to connect to Microsoft Graph." -ForegroundColor Red + Exit + } +} + + +# Function to Send Email +function SendEmail { + $Script:TableContent += "" + $TableStyle = "" + + $MailContent = "$($TableStyle) +

Hello Admin,

+

These application credentials are soon to expire:

+ $($Script:TableContent) +

To prevent authentication failures and service disruptions, please renew the expiring secret or certificate via the App registrations in Microsoft Entra admin center.

+

If you have any questions, feel free to contact IT support.

+

Best regards,
IT Admin Team

" + + $params = @{ + message = @{ + subject = "Entra App Registration Credentials Expiry Notification" + body = @{ + contentType = "HTML" + content = $MailContent + } + toRecipients = $toRecipients + } + } + Send-MgUserMail -UserId $FromAddress -BodyParameter $params +} + + +Connect_ToMgGraph +$LoggedInAccount = (Get-MgContext).Account +if ($LoggedInAccount -ne $null){ + if ([string]::IsNullOrEmpty($FromAddress)) { + $FromAddress = $LoggedInAccount + } +} else { + if ([string]::IsNullOrEmpty($FromAddress)) { + Write-Host "`nError: FromAddress is required when using certificate-based authentication." -ForegroundColor Red + Exit + } +} + + +$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 = "" +$Script:TableContent += "" + + +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="-" } + + $EmailAddresses = ($Recipients -split ",").Trim() + $toRecipients = @() + foreach ($Email in $EmailAddresses) { + $toRecipients += @{ + emailAddress = @{ + address = $Email + } + } + } + + #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 += "" + } + } + } + + #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 += "" + } + } + } +} + + + +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 \ No newline at end of file
App NameApp Creation TimeCredential TypeCredential NameCreation TimeExpiry DateFriendly Expiry Date
$($AppName)$($AppCreationDate)$($CredentialType)$($DisplayName)$($CreatedTime)$($ExpiryDate)$($FriendlyExpiryTime)
$($AppName)$($AppCreationDate)$($CredentialType)$($DisplayName)$($CreatedTime)$($ExpiryDate)$($FriendlyExpiryTime)