<# .TITLE App Deployment Failure Alert Notification .SYNOPSIS Automated runbook to monitor application deployment failures in Intune and send email alerts for deployment issues. .DESCRIPTION This script is designed to run as a scheduled Azure Automation runbook that monitors application deployment status in Microsoft Intune and identifies applications with high failure rates or deployment issues. It tracks deployment success rates, identifies required applications with failures, and sends email notifications to administrators with detailed deployment reports. The script helps maintain application availability and user productivity by proactively alerting on deployment failures and providing actionable insights for remediation. .TAGS Notification .MINROLE Intune Administrator .PERMISSIONS DeviceManagementApps.Read.All,DeviceManagementManagedDevices.Read.All,Mail.Send .AUTHOR Ugur Koc .VERSION 1.5 .CHANGELOG 1.5 - Download the app installation report payload before parsing its JSON content and add MaxApps for bounded runbook validation 1.4 - Added Azure Automation contract validation, portal-safe boolean parameters, beta Graph endpoints, and terminating paging errors 1.3 - Azure Automation now records script progress, outcomes, and summaries in job history 1.2 - Mail now sends from a mandatory SenderUPN mailbox via /users/{upn}/sendMail (app-only managed identity cannot use /me); send failures now fail the run; per-app report and assignment calls are paced and the app listing uses select; pagination helper preserves single-item arrays 1.1 - Local runs now use MgGraphCommunity for WAM-free interactive sign-in (auto-installed if missing); app install status now read via deviceManagement/reports (mobileApps deviceStatuses was retired from the Graph service) 1.0 - Initial release .LASTUPDATE 2026-07-30 .EXECUTION RunbookOnly .OUTPUT Email .SCHEDULE Daily .CATEGORY Notification .EXAMPLE .\app-deployment-failure-alert.ps1 -FailureThresholdPercent 20 -EmailRecipients "" -SenderUPN "" Alerts when app deployment failure rate exceeds 20% and sends notifications to .EXAMPLE .\app-deployment-failure-alert.ps1 -FailureThresholdPercent 15 -EmailRecipients "," -SenderUPN "" Alerts when app deployment failure rate exceeds 15% and sends notifications to multiple recipients .NOTES - Requires Microsoft.Graph.Authentication module - For Azure Automation, configure Managed Identity with required permissions - Uses Microsoft Graph Mail API for email notifications only, sent from the SenderUPN mailbox - The Automation account's managed identity requires Mail.Send permission for the SenderUPN mailbox - Recommended to run as scheduled runbook (daily) - Consider your organization's application deployment requirements when setting threshold - Review application packages and deployment settings based on findings - Critical for maintaining application availability and user productivity - Local interactive sign-in uses the MgGraphCommunity module to avoid the Graph SDK's mandatory WAM broker on Windows #> [CmdletBinding()] param( [Parameter(Mandatory = $true, HelpMessage = "Maximum acceptable failure percentage for app deployments")] [ValidateRange(5, 50)] [int]$FailureThresholdPercent, [Parameter(Mandatory = $true, HelpMessage = "Comma-separated list of email addresses to send notifications")] [ValidateNotNullOrEmpty()] [string]$EmailRecipients, [Parameter(Mandatory = $true, HelpMessage = "Mailbox UPN used as the notification sender (managed identity needs Mail.Send permission for it)")] [ValidateNotNullOrEmpty()] [string]$SenderUPN, [Parameter(Mandatory = $false, HelpMessage = "Maximum number of applications to evaluate; 0 evaluates all applications")] [ValidateRange(0, 10000)] [int]$MaxApps = 0, [Parameter(Mandatory = $false, HelpMessage = "Force module installation without prompting")] [ValidateSet("true", "false", "1", "0", '$true', '$false')] [string]$ForceModuleInstall ) # Normalize the local module-install override for Azure Automation parameter binding. $forceModuleInstallRaw = [string]$ForceModuleInstall Remove-Variable -Name ForceModuleInstall if ([string]::IsNullOrWhiteSpace($forceModuleInstallRaw)) { $ForceModuleInstall = $false } elseif ($forceModuleInstallRaw.Trim().ToLowerInvariant() -in @("true", "1", '$true')) { $ForceModuleInstall = $true } elseif ($forceModuleInstallRaw.Trim().ToLowerInvariant() -in @("false", "0", '$false')) { $ForceModuleInstall = $false } else { throw "Parameter 'ForceModuleInstall' accepts only true, false, 1, 0, $true, or $false." } # ============================================================================ # ENVIRONMENT DETECTION AND SETUP # ============================================================================ function Initialize-RequiredModule { param( [string[]]$ModuleNames, [bool]$IsAutomationEnvironment, [bool]$ForceInstall = $false ) foreach ($ModuleName in $ModuleNames) { Write-Verbose "Checking module: $ModuleName" $module = Get-Module -ListAvailable -Name $ModuleName | Select-Object -First 1 if (-not $module) { if ($IsAutomationEnvironment) { $errorMessage = @" Module '$ModuleName' is not available in this Azure Automation Account. To resolve this issue: 1. Go to Azure Portal 2. Navigate to your Automation Account 3. Go to 'Modules' > 'Browse Gallery' 4. Search for '$ModuleName' 5. Click 'Import' and wait for installation to complete Required modules for this script: - Microsoft.Graph.Authentication "@ throw $errorMessage } else { Write-Information "Module '$ModuleName' not found. Attempting to install..." -InformationAction Continue if (-not $ForceInstall) { $response = Read-Host "Install module '$ModuleName'? (Y/N)" if ($response -notmatch '^[Yy]') { throw "Module '$ModuleName' is required but installation was declined." } } try { $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") $scope = if ($isAdmin) { "AllUsers" } else { "CurrentUser" } Write-Information "Installing '$ModuleName' in scope '$scope'..." -InformationAction Continue Install-Module -Name $ModuleName -Scope $scope -Force -AllowClobber -Repository PSGallery Write-Information "✓ Successfully installed '$ModuleName'" -InformationAction Continue } catch { throw "Failed to install module '$ModuleName': $($_.Exception.Message)" } } } try { Write-Verbose "Importing module: $ModuleName" Import-Module -Name $ModuleName -Force -ErrorAction Stop Write-Verbose "✓ Successfully imported '$ModuleName'" } catch { throw "Failed to import module '$ModuleName': $($_.Exception.Message)" } } } # Detect execution environment if ($PSPrivateMetadata.JobId.Guid) { Write-Output "Running inside Azure Automation Runbook" $IsAzureAutomation = $true } else { Write-Output "Running locally in IDE or terminal" $IsAzureAutomation = $false } # Initialize required modules $RequiredModuleList = @( "Microsoft.Graph.Authentication" ) # MgGraphCommunity gives WAM-free interactive sign-in for local runs if (-not $IsAzureAutomation) { $RequiredModuleList += "MgGraphCommunity" } try { Initialize-RequiredModule -ModuleNames $RequiredModuleList -IsAutomationEnvironment $IsAzureAutomation -ForceInstall $ForceModuleInstall Write-Verbose "✓ All required modules are available" } catch { Write-Error "Module initialization failed: $_" exit 1 } # ============================================================================ # AUTHENTICATION # ============================================================================ try { if ($IsAzureAutomation) { Write-Output "Connecting to Microsoft Graph using Managed Identity..." Connect-MgGraph -Identity -NoWelcome -ErrorAction Stop Write-Output "✓ Successfully connected to Microsoft Graph using Managed Identity" } else { Write-Output "Connecting to Microsoft Graph with interactive authentication..." $Scopes = @( "DeviceManagementApps.Read.All", "DeviceManagementManagedDevices.Read.All", "Mail.Send" ) Connect-MgGraphCommunity -Scopes $Scopes -NoWelcome -ErrorAction Stop Write-Output "✓ Successfully connected to Microsoft Graph" } } catch { Write-Error "Failed to connect to Microsoft Graph: $($_.Exception.Message)" exit 1 } # ============================================================================ # HELPER FUNCTIONS # ============================================================================ function Get-MgGraphAllPage { param( [Parameter(Mandatory = $true)] [string]$Uri, [int]$DelayMs = 100 ) $AllResults = @() $NextLink = $Uri $RequestCount = 0 do { try { if ($RequestCount -gt 0) { Start-Sleep -Milliseconds $DelayMs } $Response = Invoke-MgGraphRequest -Uri $NextLink -Method GET $RequestCount++ if ($null -ne $Response.value) { $AllResults += $Response.value } else { $AllResults += $Response } $NextLink = $Response.'@odata.nextLink' } catch { if ($_.Exception.Message -like "*429*" -or $_.Exception.Message -like "*throttled*") { Write-Information "`nRate limit hit, waiting 60 seconds..." -InformationAction Continue Start-Sleep -Seconds 60 continue } throw "Error fetching data from $NextLink : $($_.Exception.Message)" } } while ($NextLink) # Comma keeps PowerShell from unrolling a single-item array on return return , $AllResults } # Retrieves app installation status rows from the reports endpoint with paging # (the mobileApps deviceStatuses endpoint was retired from the Graph service) function Get-AppInstallStatusReportRow { param( [Parameter(Mandatory = $true)] [string]$AppId, [int]$PageSize = 500, [int]$DelayMs = 100 ) $ReportUri = "https://graph.microsoft.com/beta/deviceManagement/reports/retrieveDeviceAppInstallationStatusReport" $AllRows = @() $Skip = 0 # MaxValue sentinel keeps the loop alive if the very first page hits a 429 # (a zero sentinel would end the do/while before any retry could happen) $TotalRows = [int]::MaxValue do { try { if ($Skip -gt 0) { Start-Sleep -Milliseconds $DelayMs } $Body = @{ filter = "(ApplicationId eq '$AppId')" top = $PageSize skip = $Skip select = @("DeviceId", "DeviceName", "UserPrincipalName", "Platform", "AppVersion", "InstallState", "InstallStateDetail", "ErrorCode", "LastModifiedDateTime") } | ConvertTo-Json -Depth 5 $ReportPath = Join-Path ([System.IO.Path]::GetTempPath()) "IntuneAutomation-AppDeploymentAlert-$([guid]::NewGuid().ToString('N')).json" try { Invoke-MgGraphRequest -Uri $ReportUri -Method POST -Body $Body -ContentType "application/json" -OutputFilePath $ReportPath | Out-Null $Response = Get-Content -LiteralPath $ReportPath -Raw | ConvertFrom-Json -AsHashtable } finally { if (Test-Path -LiteralPath $ReportPath) { Remove-Item -LiteralPath $ReportPath -Force } } # The report returns columnar JSON - map Schema columns to row indexes, # column order is declared by Schema, not by the request $ColumnIndex = @{} for ($i = 0; $i -lt $Response['Schema'].Count; $i++) { $ColumnIndex[$Response['Schema'][$i].Column] = $i } foreach ($Row in $Response['Values']) { $AllRows += [PSCustomObject]@{ DeviceId = $Row[$ColumnIndex['DeviceId']] DeviceName = $Row[$ColumnIndex['DeviceName']] UserPrincipalName = $Row[$ColumnIndex['UserPrincipalName']] Platform = $Row[$ColumnIndex['Platform']] AppVersion = $Row[$ColumnIndex['AppVersion']] InstallState = $Row[$ColumnIndex['InstallState']] InstallStateDetail = $Row[$ColumnIndex['InstallStateDetail']] ErrorCode = $Row[$ColumnIndex['ErrorCode']] LastModifiedDateTime = $Row[$ColumnIndex['LastModifiedDateTime']] } } $TotalRows = $Response['TotalRowCount'] $Skip += $PageSize } catch { if ($_.Exception.Message -like "*429*" -or $_.Exception.Message -like "*throttled*") { Write-Information "`nRate limit hit, waiting 60 seconds..." -InformationAction Continue Start-Sleep -Seconds 60 continue } throw "Error fetching install status report for app $AppId : $($_.Exception.Message)" } } while ($Skip -lt $TotalRows) # Comma preserves a single-row result as an array so .Count is correct return , $AllRows } function Get-AppType { param([string]$ODataType) switch ($ODataType) { "#microsoft.graph.win32LobApp" { return "Win32 App" } "#microsoft.graph.microsoftStoreForBusinessApp" { return "Store App" } "#microsoft.graph.webApp" { return "Web App" } "#microsoft.graph.officeSuiteApp" { return "Office Suite" } "#microsoft.graph.winGetApp" { return "WinGet App" } "#microsoft.graph.iosLobApp" { return "iOS LOB App" } "#microsoft.graph.iosStoreApp" { return "iOS Store App" } "#microsoft.graph.androidManagedStoreApp" { return "Android Store App" } "#microsoft.graph.androidLobApp" { return "Android LOB App" } "#microsoft.graph.macOSLobApp" { return "macOS LOB App" } "#microsoft.graph.macOSOfficeSuiteApp" { return "macOS Office Suite" } default { return "Other" } } } function Get-InstallIntentDisplay { param([string]$Intent) switch ($Intent) { "required" { return "Required" } "available" { return "Available" } "uninstall" { return "Uninstall" } "availableWithoutEnrollment" { return "Available (No Enrollment)" } default { return $Intent } } } function Get-InstallStateDisplay { param([string]$InstallState) switch ($InstallState) { "installed" { return "Installed" } "failed" { return "Failed" } "notInstalled" { return "Not Installed" } "uninstallFailed" { return "Uninstall Failed" } "pendingInstall" { return "Pending Install" } "unknown" { return "Unknown" } "notApplicable" { return "Not Applicable" } default { return $InstallState } } } function Get-DeploymentSeverity { param([string]$InstallState, [string]$Intent) if ($Intent -eq "required") { switch ($InstallState) { "failed" { return "Critical" } "uninstallFailed" { return "Critical" } "pendingInstall" { return "Warning" } "notInstalled" { return "Warning" } "installed" { return "Success" } default { return "Info" } } } else { switch ($InstallState) { "failed" { return "Warning" } "uninstallFailed" { return "Warning" } "installed" { return "Success" } default { return "Info" } } } } function Send-EmailNotification { [CmdletBinding(SupportsShouldProcess)] param( [string[]]$Recipients, [string]$Subject, [string]$Body ) try { foreach ($Recipient in $Recipients) { $Message = @{ subject = $Subject body = @{ contentType = "HTML" content = $Body } toRecipients = @( @{ emailAddress = @{ address = $Recipient } } ) } $RequestBody = @{ message = $Message } | ConvertTo-Json -Depth 10 if ($PSCmdlet.ShouldProcess($Recipient, "Send Email Notification")) { $Uri = "https://graph.microsoft.com/beta/users/$SenderUPN/sendMail" Invoke-MgGraphRequest -Uri $Uri -Method POST -Body $RequestBody -ContentType "application/json" | Out-Null Write-Information "✓ Email sent to $Recipient via Microsoft Graph" -InformationAction Continue } } return $true } catch { Write-Error "Failed to send email notification: $($_.Exception.Message)" return $false } } function New-EmailBody { param( [array]$AllApps, [array]$FailedApps, [array]$RequiredFailedApps, [hashtable]$AppStats, [int]$FailureThreshold ) $TotalApps = $AllApps.Count $AppsWithFailures = ($AllApps | Where-Object { $_.FailureCount -gt 0 }).Count $OverallFailureRate = if ($AppStats.TotalDeployments -gt 0) { [math]::Round(($AppStats.TotalFailures / $AppStats.TotalDeployments) * 100, 1) } else { 0 } $AppTypeSummary = $FailedApps | Group-Object AppType | Sort-Object Count -Descending $PlatformSummary = $FailedApps | Group-Object TargetPlatform | Sort-Object Count -Descending $Body = @"

📱 App Deployment Failure Alert

Failure threshold: $FailureThreshold% | Current overall rate: $OverallFailureRate%

Application Deployment Overview

$OverallFailureRate%

$TotalApps

Total Applications

$AppsWithFailures

Apps with Failures

$($RequiredFailedApps.Count)

Required Apps Failing

$($AppStats.TotalDeployments)

Total Deployments

$($AppStats.TotalSuccesses)

Successful Installs

$($AppStats.TotalFailures)

Failed Installs

"@ if ($RequiredFailedApps.Count -gt 0) { $Body += @"

🚨Required Applications with Failures - Critical Impact ($($RequiredFailedApps.Count) apps)

These required applications are failing to install and may impact user productivity:

"@ foreach ($App in ($RequiredFailedApps | Sort-Object FailureRate -Descending | Select-Object -First 15)) { $SuccessRate = [math]::Round((($App.TotalDeployments - $App.FailureCount) / $App.TotalDeployments) * 100, 1) $Body += @" "@ } if ($RequiredFailedApps.Count -gt 15) { $Body += @" "@ } $Body += "
Application Name App Type Platform Total Deployments Failures Failure Rate Success Rate
$($App.AppName) $($App.AppType) $($App.TargetPlatform) $($App.TotalDeployments) $($App.FailureCount) $($App.FailureRate)%
$SuccessRate%
... and $($RequiredFailedApps.Count - 15) more required applications with failures
" } if ($FailedApps.Count -gt $RequiredFailedApps.Count) { $AvailableFailedApps = $FailedApps | Where-Object { $_.InstallIntent -ne "Required" } $Body += @"

⚠️Available Applications with Failures ($($AvailableFailedApps.Count) apps)

These available applications have deployment failures that may affect user experience:

"@ foreach ($App in ($AvailableFailedApps | Sort-Object FailureRate -Descending | Select-Object -First 10)) { $Body += @" "@ } if ($AvailableFailedApps.Count -gt 10) { $Body += @" "@ } $Body += "
Application Name App Type Install Intent Platform Failures Failure Rate
$($App.AppName) $($App.AppType) $($App.InstallIntentDisplay) $($App.TargetPlatform) $($App.FailureCount) $($App.FailureRate)%
... and $($AvailableFailedApps.Count - 10) more available applications with failures
" } if ($AppTypeSummary.Count -gt 0) { $Body += @"

📊Failure Analysis by App Type

"@ foreach ($AppType in $AppTypeSummary) { $Body += @"
$($AppType.Name): $($AppType.Count) applications with failures
"@ } $Body += "
" } if ($PlatformSummary.Count -gt 0) { $Body += @"

🖥️Failure Analysis by Platform

"@ foreach ($Platform in $PlatformSummary) { $Body += @"
$($Platform.Name): $($Platform.Count) applications with failures
"@ } $Body += "
" } $Body += @"

💡Deployment Improvement Recommendations

Immediate Actions:

  • Prioritize Required Apps: Focus on fixing required applications first as they have the highest business impact
  • Review App Packages: Check application packages for corruption or compatibility issues
  • Validate Dependencies: Ensure all application dependencies are properly installed
  • Check Target Requirements: Verify device requirements match application specifications

Technical Investigation:

  • Review Install Logs: Examine detailed installation logs for root cause analysis
  • Test Deployment Groups: Validate deployments with smaller test groups first
  • Network Connectivity: Ensure devices have reliable connectivity for large app downloads
  • Storage Space: Verify target devices have sufficient storage for installations

Process Improvements:

  • Staging Deployments: Implement phased rollouts for better failure detection
  • Monitoring Enhancement: Set up proactive monitoring for deployment status
  • User Communication: Inform users about application issues and expected resolution times
  • Rollback Plans: Prepare rollback procedures for problematic deployments
"@ return $Body } # ============================================================================ # MAIN SCRIPT LOGIC # ============================================================================ try { Write-Output "Starting app deployment failure monitoring..." # Parse email recipients $EmailRecipientList = $EmailRecipients -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } if ($EmailRecipientList.Count -eq 0) { throw "No valid email recipients provided" } Write-Output "Email recipients: $($EmailRecipientList -join ', ')" Write-Output "Failure threshold: $FailureThresholdPercent%" # Initialize results arrays $AllApps = @() $FailedApps = @() $RequiredFailedApps = @() $NotificationFailed = $false $AppProcessingErrors = 0 # Initialize statistics $AppStats = @{ TotalDeployments = 0 TotalSuccesses = 0 TotalFailures = 0 TotalPending = 0 } # ======================================================================== # GET ALL MOBILE APPLICATIONS # ======================================================================== Write-Output "Retrieving mobile applications..." try { $AppsUri = "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps?`$select=id,displayName,publisher,createdDateTime,lastModifiedDateTime" $Apps = Get-MgGraphAllPage -Uri $AppsUri if ($MaxApps -gt 0) { $Apps = @($Apps | Select-Object -First $MaxApps) } Write-Output "Found $($Apps.Count) applications" foreach ($App in $Apps) { try { # Skip apps without essential information if (-not $App.id -or -not $App.displayName) { Write-Verbose "Skipping app with missing essential data" continue } Write-Verbose "Processing app: $($App.displayName)" # Get app install status for this application via the reports endpoint $InstallStatuses = Get-AppInstallStatusReportRow -AppId $App.id # Get app assignments to determine install intent $AppAssignmentsUri = "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/$($App.id)/assignments" $Assignments = Get-MgGraphAllPage -Uri $AppAssignmentsUri # Determine primary install intent (prioritize required) $InstallIntent = "available" $TargetPlatform = "Unknown" if ($Assignments) { foreach ($Assignment in $Assignments) { if ($Assignment.intent -eq "required") { $InstallIntent = "required" break } elseif ($Assignment.intent -eq "available" -and $InstallIntent -ne "required") { $InstallIntent = "available" } } } # Determine target platform from app type switch -Regex ($App.'@odata.type') { "win32|office" { $TargetPlatform = "Windows" } "ios" { $TargetPlatform = "iOS" } "android" { $TargetPlatform = "Android" } "macOS" { $TargetPlatform = "macOS" } "web" { $TargetPlatform = "Web" } default { $TargetPlatform = "Cross-Platform" } } # Calculate deployment statistics # InstallState values per the resultantAppState enum (Microsoft Graph beta): # 1 installed, 2 failed, 3 notInstalled, 4 uninstallFailed, 5 pendingInstall $TotalDeployments = $InstallStatuses.Count $SuccessfulInstalls = ($InstallStatuses | Where-Object { $_.InstallState -eq 1 }).Count $FailedInstalls = ($InstallStatuses | Where-Object { $_.InstallState -in @(2, 4) }).Count $PendingInstalls = ($InstallStatuses | Where-Object { $_.InstallState -eq 5 }).Count $FailureRate = if ($TotalDeployments -gt 0) { [math]::Round(($FailedInstalls / $TotalDeployments) * 100, 1) } else { 0 } $AppType = Get-AppType -ODataType $App.'@odata.type' $InstallIntentDisplay = Get-InstallIntentDisplay -Intent $InstallIntent $AppInfo = [PSCustomObject]@{ AppId = $App.id AppName = $App.displayName AppType = $AppType TargetPlatform = $TargetPlatform InstallIntent = $InstallIntent InstallIntentDisplay = $InstallIntentDisplay Publisher = $App.publisher TotalDeployments = $TotalDeployments SuccessfulInstalls = $SuccessfulInstalls FailedInstalls = $FailedInstalls FailureCount = $FailedInstalls PendingInstalls = $PendingInstalls FailureRate = $FailureRate CreatedDateTime = if ($App.createdDateTime) { [datetime]$App.createdDateTime } else { $null } LastModifiedDateTime = if ($App.lastModifiedDateTime) { [datetime]$App.lastModifiedDateTime } else { $null } } # Update overall statistics $AppStats.TotalDeployments += $TotalDeployments $AppStats.TotalSuccesses += $SuccessfulInstalls $AppStats.TotalFailures += $FailedInstalls $AppStats.TotalPending += $PendingInstalls $AllApps += $AppInfo # Categorize apps with failures if ($FailedInstalls -gt 0 -and $FailureRate -gt $FailureThresholdPercent) { $FailedApps += $AppInfo if ($InstallIntent -eq "required") { $RequiredFailedApps += $AppInfo } } # Pace per-app report and assignment calls to avoid throttling Start-Sleep -Milliseconds 200 } catch { $AppProcessingErrors++ Write-Warning "Error processing app '$($App.displayName)' (ID: $($App.id)): $($_.Exception.Message)" continue } } if ($AppProcessingErrors -gt 0) { throw "The report could not fully evaluate $AppProcessingErrors application(s). No healthy result will be reported from partial data." } Write-Output "✓ Processed $($AllApps.Count) applications successfully" Write-Output " • Applications with failures: $($FailedApps.Count)" Write-Output " • Required apps with failures: $($RequiredFailedApps.Count)" } catch { Write-Error "Failed to retrieve mobile applications: $($_.Exception.Message)" exit 1 } # ======================================================================== # CALCULATE OVERALL STATISTICS # ======================================================================== $OverallFailureRate = if ($AppStats.TotalDeployments -gt 0) { [math]::Round(($AppStats.TotalFailures / $AppStats.TotalDeployments) * 100, 1) } else { 0 } Write-Output " • Total deployments: $($AppStats.TotalDeployments)" Write-Output " • Successful installs: $($AppStats.TotalSuccesses)" Write-Output " • Failed installs: $($AppStats.TotalFailures)" Write-Output " • Overall failure rate: $OverallFailureRate%" # ======================================================================== # SEND NOTIFICATIONS IF DEPLOYMENT FAILURES DETECTED # ======================================================================== $RequiresNotification = ($OverallFailureRate -gt $FailureThresholdPercent) -or ($RequiredFailedApps.Count -gt 0) -or ($FailedApps.Count -gt 0) if ($RequiresNotification) { Write-Output "Preparing email notification for app deployment failures..." $Subject = if ($RequiredFailedApps.Count -gt 0) { "[Intune Alert] CRITICAL: $($RequiredFailedApps.Count) Required App(s) Failing to Deploy" } elseif ($OverallFailureRate -gt $FailureThresholdPercent) { "[Intune Alert] APP DEPLOYMENT ISSUES: $OverallFailureRate% Failure Rate (Threshold: $FailureThresholdPercent%)" } else { "[Intune Alert] APPLICATION MONITORING: $($FailedApps.Count) App(s) with Deployment Failures" } $EmailBody = New-EmailBody -AllApps $AllApps -FailedApps $FailedApps -RequiredFailedApps $RequiredFailedApps -AppStats $AppStats -FailureThreshold $FailureThresholdPercent $EmailSent = Send-EmailNotification -Recipients $EmailRecipientList -Subject $Subject -Body $EmailBody if ($EmailSent) { Write-Output "✓ Email notification sent to $($EmailRecipientList.Count) recipients" } else { Write-Warning "Email notification could not be delivered to all recipients" $NotificationFailed = $true } } else { Write-Output "✓ No significant app deployment failures detected. All applications are deploying successfully." } # ======================================================================== # DISPLAY SUMMARY # ======================================================================== Write-Output "`n📱 APP DEPLOYMENT FAILURE MONITORING SUMMARY" Write-Output "=============================================" Write-Output "Total Applications: $($AllApps.Count)" Write-Output "Failure Threshold: $FailureThresholdPercent%" Write-Output "Overall Failure Rate: $OverallFailureRate%" Write-Output "" Write-Output "Deployment Statistics:" Write-Output " • Total Deployments: $($AppStats.TotalDeployments)" Write-Output " • Successful: $($AppStats.TotalSuccesses)" Write-Output " • Failed: $($AppStats.TotalFailures)" Write-Output " • Pending: $($AppStats.TotalPending)" Write-Output "" Write-Output "Applications with Issues:" Write-Output " • Apps with failures: $($FailedApps.Count)" Write-Output " • Required apps failing: $($RequiredFailedApps.Count)" Write-Output "" if ($RequiredFailedApps.Count -gt 0) { Write-Output "Top Required Apps with Failures:" $TopRequiredFailed = $RequiredFailedApps | Sort-Object FailureRate -Descending | Select-Object -First 5 foreach ($App in $TopRequiredFailed) { Write-Output " 🔸 $($App.AppName) ($($App.AppType)) - $($App.FailureRate)% failure rate" } Write-Output "" } $StatusIcon = if ($OverallFailureRate -le $FailureThresholdPercent -and $RequiredFailedApps.Count -eq 0) { "✅" } else { "⚠️" } Write-Output "$StatusIcon Deployment Status: $(if ($OverallFailureRate -le $FailureThresholdPercent -and $RequiredFailedApps.Count -eq 0) { 'HEALTHY' } else { 'ISSUES DETECTED' })" Write-Output "`n✓ App deployment failure monitoring completed successfully" } catch { Write-Error "Script failed: $($_.Exception.Message)" exit 1 } finally { try { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null Write-Output "Disconnected from Microsoft Graph" } catch { # Silently ignore disconnect errors as they're not critical Write-Verbose "Disconnect error (ignored): $($_.Exception.Message)" } } # ============================================================================ # SCRIPT SUMMARY # ============================================================================ Write-Output " ======================================== Script Execution Summary ======================================== Script: App Deployment Failure Alert Total Applications Analyzed: $($AllApps.Count) Overall Failure Rate: $OverallFailureRate% Failure Threshold: $FailureThresholdPercent% Total Deployments: $($AppStats.TotalDeployments) Failed Deployments: $($AppStats.TotalFailures) Apps with Failures: $($FailedApps.Count) Required Apps Failing: $($RequiredFailedApps.Count) Email Recipients: $($EmailRecipientList.Count) Status: Completed ======================================== " # Fail the run if notification delivery failed if ($NotificationFailed) { exit 1 } # Copia sin cambios de https://github.com/ugurkocde/IntuneAutomation (commit 797bb2e). # MIT License # # Copyright (c) 2025 Ugur Koc | Microsoft MVP # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE.