<# .SYNOPSIS Generates a comprehensive inventory of Azure Web Apps and deployment slots across all management groups and subscriptions. .DESCRIPTION This script enumerates all Azure Web Apps and their deployment slots within active subscriptions across the entire Azure tenant, collecting detailed configuration properties, security settings, governance tags, and deployment information. The results are exported to a timestamped CSV file for analysis, compliance reporting, and security auditing. Key capabilities: - Multi-tenant Web App discovery across all management groups - Deployment slot enumeration and configuration analysis - Security configuration analysis (HTTPS, TLS versions, debugging, FTPS) - Last deployment date tracking via Azure Management API - Governance tag extraction for team ownership and compliance tracking - Identity configuration analysis (system/user-assigned managed identity) - Timestamped CSV export for audit trails and trend analysis The script processes both production Web Apps and their deployment slots, providing comprehensive visibility into the web application estate including security posture, deployment practices, and governance compliance. .PARAMETER None This script does not accept parameters and will process all Web Apps across all accessible subscriptions. Note: Visual Studio subscriptions are automatically excluded from processing. .OUTPUTS CSV File: " azure_webapps.csv" Contains columns for: - Resource identification (ID, name, type, kind, location, state) - Management hierarchy (management group, subscription, resource group) - Governance tags (team, product, environment, data classification) - Security configuration (HTTPS, TLS version, debugging, FTPS state) - Runtime configuration (PHP version, HTTP/2.0 support) - Identity configuration (managed identity type) - Deployment tracking (last successful deployment date) .EXAMPLE .\WebApps.ps1 Discovers all Web Apps and deployment slots, generates: "2024-10-30 1435 azure_webapps.csv" .NOTES File Name : WebApps.ps1 Author : Cloud Engineering Team Prerequisite : Azure PowerShell module (Az.Websites, Az.Resources, Az.Accounts) Copyright : (c) 2024 Effectory. All rights reserved. Version History: 1.0 - Initial release with comprehensive Web App inventory functionality .LINK https://docs.microsoft.com/en-us/azure/app-service/ https://docs.microsoft.com/en-us/powershell/module/az.websites/ .COMPONENT Requires Azure PowerShell modules: - Az.Websites (for Web App enumeration and configuration retrieval) - Az.Resources (for resource group and management group access) - Az.Accounts (for authentication and access token management) .ROLE Required Azure permissions: - Website Contributor or Reader on all App Service resources - Management Group Reader for organizational hierarchy access - Reader access on target subscriptions for deployment API calls .FUNCTIONALITY - Multi-subscription Web App discovery and slot enumeration - Security configuration analysis and compliance checking - Deployment tracking via Azure Management REST API - Identity configuration analysis and managed identity reporting - CSV export with comprehensive web application metadata #> #Requires -Modules Az.Websites, Az.Resources, Az.Accounts #Requires -Version 5.1 [CmdletBinding()] param() # Uncomment the following line if authentication is required #Connect-AzAccount <# .SYNOPSIS Retrieves the last successful deployment date for an Azure Web App or deployment slot. .DESCRIPTION This function calls the Azure Management REST API to retrieve deployment history and extract the last successful deployment timestamp for a Web App or slot. .PARAMETER siteName The name of the Azure Web App. .PARAMETER resourceGroupName The name of the resource group containing the Web App. .PARAMETER subscriptionId The subscription ID containing the Web App. .PARAMETER slotName Optional. The name of the deployment slot. If not provided, queries the production slot. .OUTPUTS String. The last successful deployment end time, or empty string if no deployments found. #> function GetDeployment { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $siteName, [Parameter(Mandatory = $true)] [string] $resourceGroupName, [Parameter(Mandatory = $true)] [string] $subscriptionId, [Parameter(Mandatory = $false)] [string] $slotName = "" ) try { # Get current Azure context for tenant ID $context = Get-AzContext if (-not $context) { Write-Warning "No Azure context found for deployment API call" return "" } # Get access token for Azure Management API $accessTokenInfo = Get-AzAccessToken -TenantId $context.Tenant.Id $access_token = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($accessTokenInfo.Token)) # Build API URL for deployments if ($slotName -ne "") { $url = "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Web/sites/$siteName/slots/$slotName/deployments?api-version=2022-03-01" } else { $url = "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Web/sites/$siteName/deployments?api-version=2022-03-01" } # Make API call to get deployment history $headers = @{ Authorization = "Bearer $access_token" } $response = Invoke-RestMethod -Uri $url -Method GET -Headers $headers -ErrorAction SilentlyContinue # Extract last successful deployment date if ($response -and $response.value -and $response.value.Length -gt 0) { $lastDeployment = $response.value[0] if ($lastDeployment.properties.last_success_end_time) { return $lastDeployment.properties.last_success_end_time } } return "" } catch { Write-Warning "Error retrieving deployment info for $siteName`: $($_.Exception.Message)" return "" } } class ResourceCheck { [string] $ResourceId = "" [string] $Kind = "" [string] $Location = "" [string] $ResourceName = "" [string] $ResourceGroup = "" [string] $ResourceType = "" [string] $State = "" [string] $ManagementGroupId = "" [string] $ManagementGroupName = "" [string] $SubscriptionId = "" [string] $SubscriptionName = "" [string] $Tag_Team = "" [string] $Tag_Product = "" [string] $Tag_Environment = "" [string] $Tag_Data = "" [string] $Tag_Deployment = "" [string] $Tag_CreatedOnDate = "" [string] $Prop_HttpsOnly = "" [string] $Prop_PhpVersion = "" [string] $Prop_RemoteDebuggingEnabled = "" [string] $Prop_MinTlsVersion = "" [string] $Prop_FtpsState = "" [string] $Prop_Http20Enabled = "" [string] $Prop_Identity = "" [string] $LastDeployDate = "" } # Initialize script execution $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" $startTime = Get-Date Write-Host "======================================================================================================================================================================" Write-Host "🌐 AZURE WEB APPS INVENTORY GENERATOR" -ForegroundColor Cyan Write-Host "======================================================================================================================================================================" Write-Host "⏰ Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Green Write-Host "" try { # Validate Azure authentication $context = Get-AzContext if (-not $context) { throw "No Azure context found. Please run Connect-AzAccount first." } Write-Host "🔐 Authenticated as: $($context.Account.Id)" -ForegroundColor Green Write-Host "🏢 Tenant: $($context.Tenant.Id)" -ForegroundColor Green Write-Host "" # Initialize output file and tracking variables [string] $date = Get-Date -Format "yyyy-MM-dd HHmm" $fileName = ".\$date azure_webapps.csv" Write-Host "📄 Output file: $fileName" -ForegroundColor Yellow Write-Host "" # Initialize counters for progress tracking $totalWebApps = 0 $totalSlots = 0 $processedManagementGroups = 0 $processedSubscriptions = 0 $securityIssues = @() $deploymentTrackingErrors = 0 # Get management groups for organizational structure Write-Host "🏗️ Discovering management group structure..." -ForegroundColor Cyan $managementGroups = Get-AzManagementGroup Write-Host "✅ Found $($managementGroups.Count) management groups" -ForegroundColor Green Write-Host "" # Process each management group foreach ($managementGroup in $managementGroups) { $processedManagementGroups++ Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------" Write-Host "🏗️ Management Group [$($managementGroup.DisplayName)]" -ForegroundColor Cyan Write-Host " ID: $($managementGroup.Id)" -ForegroundColor DarkGray try { # Get active non-Visual Studio subscriptions in this management group $subscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name | Where-Object State -eq "Active" | Where-Object DisplayName -NotLike "Visual Studio*" Write-Host " 📋 Found $($subscriptions.Count) active subscriptions (excluding Visual Studio)" -ForegroundColor Green foreach ($subscription in $subscriptions) { $processedSubscriptions++ Write-Host "" Write-Host " 🔄 Processing Subscription: $($subscription.DisplayName)" -ForegroundColor Yellow try { # Extract subscription ID and set context $scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length) $subscriptionId = $scope.Replace("/subscriptions/", "") Write-Host " ID: $subscriptionId" -ForegroundColor DarkGray Set-AzContext -SubscriptionId $subscriptionId | Out-Null # Get all resource groups in the subscription $allResourceGroups = Get-AzResourceGroup [ResourceCheck[]]$Result = @() $subscriptionWebApps = 0 $subscriptionSlots = 0 foreach ($group in $allResourceGroups) { Write-Host " 📁 Resource Group: $($group.ResourceGroupName)" -ForegroundColor DarkCyan -NoNewline try { # Get Web Apps in this resource group $allWebApps = Get-AzWebApp -ResourceGroupName $group.ResourceGroupName -ErrorAction SilentlyContinue if ($allWebApps.Count -gt 0) { Write-Host " - Found $($allWebApps.Count) Web Apps" -ForegroundColor Green $subscriptionWebApps += $allWebApps.Count } else { Write-Host " - No Web Apps" -ForegroundColor DarkGray } foreach ($webApp in $allWebApps) { Write-Host " 🌐 Web App: $($webApp.Name)" -ForegroundColor White try { # Analyze security configuration if (-not $webApp.HttpsOnly) { $securityIssues += "🔓 HTTPS not enforced: $($webApp.Name) in $($group.ResourceGroupName)" } if ($webApp.SiteConfig.MinTlsVersion -lt "1.2") { $securityIssues += "⚠️ TLS version below 1.2: $($webApp.Name) (version: $($webApp.SiteConfig.MinTlsVersion))" } if ($webApp.SiteConfig.RemoteDebuggingEnabled) { $securityIssues += "🐛 Remote debugging enabled: $($webApp.Name) in $($group.ResourceGroupName)" } if ($webApp.SiteConfig.FtpsState -eq "AllAllowed") { $securityIssues += "📂 FTPS allows unencrypted connections: $($webApp.Name) in $($group.ResourceGroupName)" } # Create resource check object for Web App [ResourceCheck] $resourceCheck = [ResourceCheck]::new() $resourceCheck.ResourceId = $webApp.Id $resourceCheck.Kind = $webApp.Kind $resourceCheck.Location = $webApp.Location $resourceCheck.State = $webApp.State $resourceCheck.ResourceName = $webApp.Name $resourceCheck.ResourceGroup = $webApp.ResourceGroup $resourceCheck.ResourceType = $webApp.Type $resourceCheck.ManagementGroupId = $managementGroup.Id $resourceCheck.ManagementGroupName = $managementGroup.DisplayName $resourceCheck.SubscriptionId = $subscriptionId $resourceCheck.SubscriptionName = $subscription.DisplayName $resourceCheck.Tag_Team = $webApp.Tags.team $resourceCheck.Tag_Product = $webApp.Tags.product $resourceCheck.Tag_Environment = $webApp.Tags.environment $resourceCheck.Tag_Data = $webApp.Tags.data $resourceCheck.Tag_CreatedOnDate = $webApp.Tags.CreatedOnDate $resourceCheck.Tag_Deployment = $webApp.Tags.drp_deployment $resourceCheck.Prop_HttpsOnly = $webApp.HttpsOnly $resourceCheck.Prop_PhpVersion = $webApp.SiteConfig.PhpVersion $resourceCheck.Prop_RemoteDebuggingEnabled = $webApp.SiteConfig.RemoteDebuggingEnabled $resourceCheck.Prop_MinTlsVersion = $webApp.SiteConfig.MinTlsVersion $resourceCheck.Prop_FtpsState = $webApp.SiteConfig.FtpsState $resourceCheck.Prop_Http20Enabled = $webApp.SiteConfig.Http20Enabled $resourceCheck.Prop_Identity = $webApp.Identity.Type # Get deployment information with error handling $deploymentDate = GetDeployment -siteName $webApp.Name -resourceGroupName $group.ResourceGroupName -subscriptionId $subscriptionId if ([string]::IsNullOrEmpty($deploymentDate)) { $deploymentTrackingErrors++ } $resourceCheck.LastDeployDate = $deploymentDate $Result += $resourceCheck $totalWebApps++ # Process deployment slots try { $allSlots = Get-AzWebAppSlot -Name $webApp.Name -ResourceGroupName $webApp.ResourceGroup -ErrorAction SilentlyContinue if ($allSlots.Count -gt 0) { Write-Host " 🔄 Found $($allSlots.Count) deployment slots" -ForegroundColor Cyan $subscriptionSlots += $allSlots.Count } foreach ($slotTemp in $allSlots) { try { Write-Host " 📍 Slot: $($slotTemp.Name)" -ForegroundColor DarkCyan [string] $slotName = $slotTemp.Name.Split("/")[1] $slot = Get-AzWebAppSlot -Name $webApp.Name -ResourceGroupName $webApp.ResourceGroup -Slot $slotName # Analyze slot security configuration if (-not $slot.HttpsOnly) { $securityIssues += "🔓 HTTPS not enforced on slot: $($slot.Name) in $($webApp.ResourceGroup)" } if ($slot.SiteConfig.MinTlsVersion -lt "1.2") { $securityIssues += "⚠️ TLS version below 1.2 on slot: $($slot.Name) (version: $($slot.SiteConfig.MinTlsVersion))" } if ($slot.SiteConfig.RemoteDebuggingEnabled) { $securityIssues += "🐛 Remote debugging enabled on slot: $($slot.Name) in $($webApp.ResourceGroup)" } [ResourceCheck] $resourceCheck = [ResourceCheck]::new() $resourceCheck.ResourceId = $slot.Id $resourceCheck.Kind = $slot.Kind $resourceCheck.Location = $slot.Location $resourceCheck.State = $slot.State $resourceCheck.ResourceName = $slot.Name $resourceCheck.ResourceGroup = $slot.ResourceGroup $resourceCheck.ResourceType = $slot.Type $resourceCheck.ManagementGroupId = $managementGroup.Id $resourceCheck.ManagementGroupName = $managementGroup.DisplayName $resourceCheck.SubscriptionId = $subscriptionId $resourceCheck.SubscriptionName = $subscription.DisplayName $resourceCheck.Tag_Team = $slot.Tags.team $resourceCheck.Tag_Product = $slot.Tags.product $resourceCheck.Tag_Environment = $slot.Tags.environment $resourceCheck.Tag_Data = $slot.Tags.data $resourceCheck.Tag_CreatedOnDate = $slot.Tags.CreatedOnDate $resourceCheck.Tag_Deployment = $slot.Tags.drp_deployment $resourceCheck.Prop_HttpsOnly = $slot.HttpsOnly $resourceCheck.Prop_PhpVersion = $slot.SiteConfig.PhpVersion $resourceCheck.Prop_RemoteDebuggingEnabled = $slot.SiteConfig.RemoteDebuggingEnabled $resourceCheck.Prop_MinTlsVersion = $slot.SiteConfig.MinTlsVersion $resourceCheck.Prop_FtpsState = $slot.SiteConfig.FtpsState $resourceCheck.Prop_Http20Enabled = $slot.SiteConfig.Http20Enabled $resourceCheck.Prop_Identity = $slot.Identity.Type # Get deployment information for slot $slotDeploymentDate = GetDeployment -siteName $webApp.Name -resourceGroupName $group.ResourceGroupName -subscriptionId $subscriptionId -slotName $slotName if ([string]::IsNullOrEmpty($slotDeploymentDate)) { $deploymentTrackingErrors++ } $resourceCheck.LastDeployDate = $slotDeploymentDate $Result += $resourceCheck $totalSlots++ } catch { Write-Host " ❌ Error processing slot '$($slotTemp.Name)': $($_.Exception.Message)" -ForegroundColor Red } } } catch { Write-Host " ❌ Error getting slots for '$($webApp.Name)': $($_.Exception.Message)" -ForegroundColor Red } } catch { Write-Host " ❌ Error processing Web App '$($webApp.Name)': $($_.Exception.Message)" -ForegroundColor Red } } } catch { Write-Host " - ❌ Error accessing resource group: $($_.Exception.Message)" -ForegroundColor Red } } # Export results for this subscription if ($Result.Count -gt 0) { $Result | Export-Csv -Path $fileName -Append -NoTypeInformation Write-Host " ✅ Exported $($Result.Count) Web App resources from subscription" -ForegroundColor Green Write-Host " Web Apps: $subscriptionWebApps, Deployment Slots: $subscriptionSlots" -ForegroundColor DarkGray } else { Write-Host " ℹ️ No Web Apps found in subscription" -ForegroundColor DarkYellow } } catch { Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red } } } catch { Write-Host " ❌ Error accessing management group: $($_.Exception.Message)" -ForegroundColor Red } Write-Host "" } # Calculate execution time and generate comprehensive summary report $endTime = Get-Date $executionTime = $endTime - $startTime Write-Host "======================================================================================================================================================================" Write-Host "📊 AZURE WEB APPS INVENTORY SUMMARY" -ForegroundColor Cyan Write-Host "======================================================================================================================================================================" Write-Host "⏰ Execution Time: $($executionTime.ToString('hh\:mm\:ss'))" -ForegroundColor Green Write-Host "🏗️ Management Groups Processed: $processedManagementGroups" -ForegroundColor Green Write-Host "📋 Subscriptions Processed: $processedSubscriptions" -ForegroundColor Green Write-Host "🌐 Total Web Apps Discovered: $totalWebApps" -ForegroundColor Green Write-Host "🔄 Total Deployment Slots: $totalSlots" -ForegroundColor Cyan Write-Host "📄 Results Exported To: $fileName" -ForegroundColor Yellow if (Test-Path $fileName) { $fileSize = (Get-Item $fileName).Length Write-Host "💾 File Size: $([math]::Round($fileSize/1KB, 2)) KB" -ForegroundColor Green } # Display deployment tracking statistics if ($deploymentTrackingErrors -gt 0) { Write-Host "⚠️ Deployment Tracking Issues: $deploymentTrackingErrors Web Apps/slots" -ForegroundColor Yellow Write-Host " (This may be due to API permissions or apps without deployment history)" -ForegroundColor DarkGray } else { Write-Host "✅ Deployment Tracking: Successfully retrieved for all Web Apps" -ForegroundColor Green } # Display security analysis summary if ($securityIssues.Count -gt 0) { Write-Host "" Write-Host "🚨 SECURITY ANALYSIS SUMMARY" -ForegroundColor Red Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------" Write-Host "Found $($securityIssues.Count) potential security concerns:" -ForegroundColor Yellow foreach ($issue in $securityIssues | Select-Object -First 15) { Write-Host " $issue" -ForegroundColor Yellow } if ($securityIssues.Count -gt 15) { Write-Host " ... and $($securityIssues.Count - 15) more issues (see CSV for complete details)" -ForegroundColor DarkYellow } Write-Host "" Write-Host "📋 Security Recommendations:" -ForegroundColor Cyan Write-Host " • Enforce HTTPS-only access on all Web Apps and slots" -ForegroundColor White Write-Host " • Upgrade minimum TLS version to 1.2 or higher" -ForegroundColor White Write-Host " • Disable remote debugging on production Web Apps" -ForegroundColor White Write-Host " • Configure FTPS to require SSL/TLS (disable 'AllAllowed')" -ForegroundColor White Write-Host " • Enable managed identities for secure Azure service authentication" -ForegroundColor White } else { Write-Host "" Write-Host "✅ SECURITY ANALYSIS: No major security concerns detected" -ForegroundColor Green } # Calculate and display Web App statistics $totalWebAppResources = $totalWebApps + $totalSlots $averageSlotsPerApp = if ($totalWebApps -gt 0) { [math]::Round($totalSlots / $totalWebApps, 1) } else { 0 } if ($totalWebApps -gt 0) { Write-Host "" Write-Host "📈 WEB APP DEPLOYMENT ANALYSIS:" -ForegroundColor Cyan Write-Host " Total Web App Resources: $totalWebAppResources (Apps + Slots)" -ForegroundColor White Write-Host " Average Deployment Slots per App: $averageSlotsPerApp" -ForegroundColor White if ($averageSlotsPerApp -gt 1) { Write-Host " 🔄 High Slot Usage: Good deployment strategy with staging/testing slots" -ForegroundColor Green } elseif ($averageSlotsPerApp -gt 0.5) { Write-Host " 📊 Moderate Slot Usage: Some apps using deployment slots" -ForegroundColor Yellow } else { Write-Host " 💡 Low Slot Usage: Consider implementing deployment slots for safer deployments" -ForegroundColor White } } Write-Host "" Write-Host "📈 NEXT STEPS:" -ForegroundColor Cyan Write-Host " 1. Review the generated CSV file for detailed Web App configurations" -ForegroundColor White Write-Host " 2. Address security recommendations identified above" -ForegroundColor White Write-Host " 3. Analyze deployment patterns and slot usage for optimization" -ForegroundColor White Write-Host " 4. Implement monitoring and alerting for critical Web Apps" -ForegroundColor White Write-Host " 5. Review governance tags for compliance with organizational standards" -ForegroundColor White Write-Host " 6. Consider implementing Azure Application Insights for application monitoring" -ForegroundColor White Write-Host "" Write-Host "✅ Azure Web Apps inventory completed successfully!" -ForegroundColor Green Write-Host "======================================================================================================================================================================" } catch { Write-Host "" Write-Host "❌ CRITICAL ERROR OCCURRED" -ForegroundColor Red Write-Host "======================================================================================================================================================================" Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red Write-Host "Line: $($_.InvocationInfo.ScriptLineNumber)" -ForegroundColor Red Write-Host "Position: $($_.InvocationInfo.OffsetInLine)" -ForegroundColor Red Write-Host "" Write-Host "🔧 TROUBLESHOOTING STEPS:" -ForegroundColor Yellow Write-Host " 1. Verify you are authenticated to Azure (Connect-AzAccount)" -ForegroundColor White Write-Host " 2. Ensure you have Website Contributor or Reader permissions on App Service resources" -ForegroundColor White Write-Host " 3. Check that the Management Group Reader role is assigned" -ForegroundColor White Write-Host " 4. Verify Azure PowerShell modules are installed and up to date" -ForegroundColor White Write-Host " 5. Confirm that deployment API permissions are available for deployment tracking" -ForegroundColor White Write-Host " 6. Try running the script with -Verbose for additional diagnostic information" -ForegroundColor White Write-Host "" Write-Host "📞 For additional support, contact the Cloud Engineering team" -ForegroundColor Cyan Write-Host "======================================================================================================================================================================" # Ensure we exit with error code for automation scenarios exit 1 } finally { # Reset progress preference $ProgressPreference = "Continue" }