added documetation

This commit is contained in:
Jurjen Ladenius
2025-11-03 08:12:01 +01:00
parent 8840b0e300
commit a226ca97ac
37 changed files with 8315 additions and 1481 deletions

View File

@@ -1,105 +1,294 @@
<#
.SYNOPSIS
Exports comprehensive Azure Alert Rules inventory across all enabled subscriptions with associated action groups and configuration details.
.DESCRIPTION
This script performs a complete audit of Azure Alert Rules across multiple alert types and all enabled subscriptions.
It inventories Smart Detector Alert Rules, Scheduled Query Rules, Metric Alerts, and Activity Log Alerts,
including their associated Action Groups, receivers, and tag information.
The script processes four main types of Azure alerts:
- Smart Detector Alert Rules (Application Insights anomaly detection)
- Scheduled Query Rules (Log Analytics/KQL-based alerts)
- Metric Alert Rules (Resource metric-based alerts)
- Activity Log Alert Rules (Azure Activity Log event alerts)
For each alert rule, the script captures detailed information including:
- Alert configuration and state
- Associated Action Groups and their receivers
- Tag information for governance tracking
- Subscription and resource group context
.PARAMETER None
This script does not accept parameters. It processes all enabled Azure subscriptions accessible to the current user.
.OUTPUTS
CSV file named with timestamp pattern: "yyyy-MM-dd HHmm alert rules.csv"
Also displays results in formatted table output to console.
CSV contains columns for alert details, action group information, and governance tags.
.EXAMPLE
.\AlertRules.ps1
Exports all alert rules from all enabled subscriptions to a timestamped CSV file and displays results.
.NOTES
Author: Cloud Engineering Team
Version: 1.0
Created: 2024
Prerequisites:
- Azure PowerShell module (Az) must be installed
- User must be authenticated (Connect-AzAccount)
- Requires read permissions on Azure Monitor, Action Groups, and resource tags across all subscriptions
- Tenant ID is hardcoded and may need adjustment for different environments
Security Considerations:
- Script uses Azure access tokens for REST API authentication
- Requires permissions to read alert rules and action groups across all subscriptions
- Output file contains sensitive alerting configuration information
Performance Notes:
- Processing time varies based on number of subscriptions and alert rules
- Script processes all enabled subscriptions sequentially
- REST API calls for Smart Detector rules add processing time
Alert Types Covered:
- microsoft.alertsmanagement/smartdetectoralertrules (Application Insights anomalies)
- microsoft.insights/scheduledqueryrules (Log Analytics queries)
- Microsoft.Insights/metricAlerts (Resource metrics)
- Microsoft.Insights/ActivityLogAlerts (Activity log events)
.LINK
https://docs.microsoft.com/en-us/azure/azure-monitor/alerts/
https://docs.microsoft.com/en-us/azure/azure-monitor/alerts/action-groups
#>
#Requires -Modules Az
#Connect-AzAccount
$access_token = (Get-AzAccessToken -TenantId "e9792fd7-4044-47e7-a40d-3fba46f1cd09").Token
# Get Azure access token for REST API calls (required for Smart Detector rules)
# Note: Tenant ID is hardcoded and should be updated for different environments
$access_token_secure = (Get-AzAccessToken -TenantId "e9792fd7-4044-47e7-a40d-3fba46f1cd09").Token
$access_token = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($access_token_secure))
# Set output field separator for array-to-string conversion
$ofs = ', '
function GetSmartDetectorActionGroupIds {
<#
.SYNOPSIS
Retrieves Action Group IDs and details for Smart Detector Alert Rules using Azure Management REST API.
.DESCRIPTION
This function queries the Azure Management REST API to retrieve detailed information about Smart Detector Alert Rules,
including their associated Action Groups. Smart Detector rules are used for Application Insights anomaly detection
and require REST API calls as they're not fully supported by PowerShell cmdlets.
.PARAMETER alertRuleName
The name of the Smart Detector Alert Rule to query.
.PARAMETER resourceGroupName
The resource group containing the Smart Detector Alert Rule.
.PARAMETER subscriptionId
The subscription ID containing the alert rule.
.OUTPUTS
Returns an array of custom objects containing alert rule details and associated Action Group IDs.
.EXAMPLE
GetSmartDetectorActionGroupIds -alertRuleName "Failure Anomalies - authorization-functions-v2" -resourceGroupName "authorization" -subscriptionId "3190b0fd-4a66-4636-a204-5b9f18be78a6"
Retrieves Action Group details for the specified Smart Detector Alert Rule.
.NOTES
- Uses REST API version 2019-06-01 for Smart Detector Alert Rules
- Requires valid Azure access token for authentication
- URL-encodes alert rule names to handle special characters
#>
function GetSmartDetectorActionGroupIds {
param (
[Parameter(Mandatory = $true)]
[string] $alertRuleName,
[Parameter(Mandatory = $true)]
[string] $resourceGroupName,
[Parameter(Mandatory = $true)]
[string] $subscriptionId
)
## example : GetSmartDetectorActionGroupIds -alertRuleName "Failure Anomalies - authorization-functions-v2" -resourceGroupName "authorization" -subscriptionId "3190b0fd-4a66-4636-a204-5b9f18be78a6"
$escapedAlertRuleName = [uri]::EscapeDataString($alertRuleName)
$url = "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/microsoft.alertsManagement/smartDetectorAlertRules/$escapedAlertRuleName`?api-version=2019-06-01"
$head = @{ Authorization =" Bearer $access_token" }
$response = Invoke-RestMethod -Uri $url -Method GET -Headers $head
$response | ForEach-Object {
$alert = $_
$alert.properties.actionGroups
| ForEach-Object {
$actionGroup = $_
$_.groupIds | ForEach-Object {
[pscustomobject]@{
Id = $alert.id
Name = $alert.name
Description = $alert.properties.description
State = $alert.properties.state
Alert = $alert.properties
ActionGroups = $alert.actionGroups
ActionGroup = $actionGroup
ActionGroupId = $_
try {
# URL-encode the alert rule name to handle special characters
$escapedAlertRuleName = [uri]::EscapeDataString($alertRuleName)
# Construct REST API URL for Smart Detector Alert Rule
$url = "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/microsoft.alertsManagement/smartDetectorAlertRules/$escapedAlertRuleName`?api-version=2019-06-01"
# Create authorization header with bearer token
$head = @{ Authorization = " Bearer $access_token" }
# Execute REST API call to retrieve Smart Detector rule details
$response = Invoke-RestMethod -Uri $url -Method GET -Headers $head
# Process response and extract Action Group information
$response | ForEach-Object {
$alert = $_
# Process each Action Group associated with the alert rule
$alert.properties.actionGroups | ForEach-Object {
$actionGroup = $_
# Extract individual Action Group IDs
$_.groupIds | ForEach-Object {
[pscustomobject]@{
Id = $alert.id
Name = $alert.name
Description = $alert.properties.description
State = $alert.properties.state
Alert = $alert.properties
ActionGroups = $alert.actionGroups
ActionGroup = $actionGroup
ActionGroupId = $_
}
}
}
}
}
catch {
Write-Warning "Failed to retrieve Smart Detector Alert Rule: $alertRuleName in $resourceGroupName. Error: $($_.Exception.Message)"
return $null
}
}
<#
.SYNOPSIS
Sanitizes alert rule descriptions for CSV export by removing newline characters.
.DESCRIPTION
This utility function cleans up alert rule descriptions by replacing newline and carriage return
characters with hyphens to ensure proper CSV formatting. It also removes duplicate hyphens
that might result from the replacement process.
.PARAMETER description
The description string to sanitize. Can be null or empty.
.OUTPUTS
Returns a cleaned description string suitable for CSV export, or empty string if input is null.
.EXAMPLE
GetDecentDescription -description "Line 1`nLine 2`rLine 3"
Returns "Line 1 - Line 2 - Line 3"
.NOTES
- Handles null input gracefully
- Replaces both Unix (`n) and Windows (`r) newline characters
- Removes duplicate hyphens that may result from consecutive newlines
#>
function GetDecentDescription {
param (
[AllowEmptyString()]
[string] $description
)
if ($null -eq $description) {
""
# Handle null or empty descriptions
if ($null -eq $description -or $description -eq "") {
return ""
}
else {
$description.Replace("`n"," - ").Replace("`r"," - ").Replace(" - - "," - ")
# Replace newline characters with hyphens and clean up duplicates
return $description.Replace("`n", " - ").Replace("`r", " - ").Replace(" - - ", " - ")
}
}
# Main script execution begins
Write-Host "======================================================================================================================================================================"
Write-Host "Starting comprehensive Azure Alert Rules inventory across all enabled subscriptions."
Write-Host "======================================================================================================================================================================"
# Generate timestamped filename for CSV export
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date alert rules.csv"
$fileName = ".\$date alert rules.csv"
Write-Host "Output file: $fileName"
# Retrieve all enabled Azure subscriptions
Write-Host "Retrieving enabled subscriptions..."
$subscriptions = Get-AzSubscription | Where-Object State -eq "Enabled"
Write-Host "Found $($subscriptions.Count) enabled subscription(s) to process."
# Class definition for structured alert rule data
class AlertRule {
[string] $SubscriptionId = ""
[string] $SubscriptionName = ""
[string] $Id = ""
[string] $ResourceGroupName = ""
[string] $Type = ""
[string] $Name = ""
[string] $Description = ""
[string] $State = ""
[string] $ActionGroupId = ""
[string] $ActionGroupName = ""
[string] $ActionGroupResourceGroupName = ""
[string] $ActionGroupEnabled = ""
[string] $ActionGroupArmRoleReceivers = ""
[string] $ActionGroupEmailReceivers = ""
[string] $AzureFunctionReceivers = ""
[string] $Tag_Team = ""
[string] $Tag_Product = ""
[string] $Tag_Environment = ""
[string] $Tag_Data = ""
[string] $Tag_CreatedOnDate = ""
[string] $Tag_Deployment = ""
# Subscription and resource context
[string] $SubscriptionId = "" # Azure subscription GUID
[string] $SubscriptionName = "" # Subscription display name
[string] $Id = "" # Full Azure resource ID of the alert rule
[string] $ResourceGroupName = "" # Resource group containing the alert rule
[string] $Type = "" # Azure resource type of the alert rule
[string] $Name = "" # Alert rule name
# Alert rule configuration
[string] $Description = "" # Alert rule description (sanitized for CSV)
[string] $State = "" # Alert rule state (Enabled/Disabled)
# Action Group associations
[string] $ActionGroupId = "" # Associated Action Group resource ID
[string] $ActionGroupName = "" # Action Group name
[string] $ActionGroupResourceGroupName = "" # Resource group containing the Action Group
[string] $ActionGroupEnabled = "" # Action Group enabled status
# Action Group receiver details
[string] $ActionGroupArmRoleReceivers = "" # ARM role-based receivers (comma-separated)
[string] $ActionGroupEmailReceivers = "" # Email receivers (comma-separated)
[string] $AzureFunctionReceivers = "" # Azure Function receivers (comma-separated)
# Governance and metadata tags
[string] $Tag_Team = "" # Team responsible for the alert
[string] $Tag_Product = "" # Product/service associated with the alert
[string] $Tag_Environment = "" # Environment (dev, test, prod, etc.)
[string] $Tag_Data = "" # Data classification tag
[string] $Tag_CreatedOnDate = "" # Creation date tag
[string] $Tag_Deployment = "" # Deployment pipeline tag
}
[Microsoft.Azure.Commands.Insights.OutputClasses.PSActionGroupResource[]]$actionGroups = @()
foreach ($subscription in $subscriptions)
{
Set-AzContext -SubscriptionId $subscription.Id | out-null
# Pre-load all Action Groups from all subscriptions for efficient lookup
Write-Host "Pre-loading Action Groups from all subscriptions for efficient processing..."
[Microsoft.Azure.PowerShell.Cmdlets.Monitor.ActionGroup.Models.IActionGroupResource[]]$actionGroups = @()
foreach ($subscription in $subscriptions) {
Write-Host " Loading Action Groups from subscription: $($subscription.Name)"
Set-AzContext -SubscriptionId $subscription.Id | Out-Null
$actionGroups += Get-AzActionGroup
}
Write-Host "Loaded $($actionGroups.Count) Action Group(s) across all subscriptions."
Write-Host ""
# Initialize result collection for all alert rules
[AlertRule[]]$Result = @()
foreach ($subscription in $subscriptions)
{
Set-AzContext -SubscriptionId $subscription.Id
# Process each subscription for alert rules
foreach ($subscription in $subscriptions) {
Write-Host "======================================================================================================================================================================"
Write-Host "Processing subscription: [$($subscription.Name)] - $($subscription.Id)"
Write-Host "======================================================================================================================================================================"
# Set Azure context to current subscription
Set-AzContext -SubscriptionId $subscription.Id | Out-Null
# Process Smart Detector Alert Rules (Application Insights anomaly detection)
Write-Host "Processing Smart Detector Alert Rules..."
$smartDetectorRules = Get-AzResource -ResourceType "microsoft.alertsmanagement/smartdetectoralertrules"
foreach ($smartDetectorRule in $smartDetectorRules)
{
Write-Host " Found $($smartDetectorRules.Count) Smart Detector Alert Rule(s)"
foreach ($smartDetectorRule in $smartDetectorRules) {
# Retrieve Action Group details for the Smart Detector rule via REST API
$actions = GetSmartDetectorActionGroupIds -alertRuleName $smartDetectorRule.Name -resourceGroupName $smartDetectorRule.ResourceGroupName -subscriptionId $subscription.Id
# Handle Smart Detector rules without Action Groups
if (($null -eq $actions) -or ($actions.Length -eq 0)) {
# Create alert rule entry without Action Group details
[AlertRule] $AlertRule = [AlertRule]::new()
$AlertRule.SubscriptionId = $subscription.Id
@@ -108,6 +297,8 @@ foreach ($subscription in $subscriptions)
$AlertRule.Name = $smartDetectorRule.Name
$AlertRule.Type = $smartDetectorRule.ResourceType
$AlertRule.ResourceGroupName = $smartDetectorRule.ResourceGroupName
# Extract governance tags
$AlertRule.Tag_Team = $smartDetectorRule.Tags.team
$AlertRule.Tag_Product = $smartDetectorRule.Tags.product
$AlertRule.Tag_Environment = $smartDetectorRule.Tags.environment
@@ -118,11 +309,15 @@ foreach ($subscription in $subscriptions)
$Result += $AlertRule
}
else {
foreach($action in $actions) {
# Process Smart Detector rules with Action Groups
foreach ($action in $actions) {
# Create alert rule entry with Action Group details
[AlertRule] $AlertRule = [AlertRule]::new()
$actionGroup = $actionGroups | where { $_.id -eq [uri]::UnescapeDataString($action.ActionGroupId) }
# Find corresponding Action Group from pre-loaded collection
$actionGroup = $actionGroups | Where-Object { $_.id -eq [uri]::UnescapeDataString($action.ActionGroupId) }
# Populate basic alert rule information
$AlertRule.SubscriptionId = $subscription.Id
$AlertRule.SubscriptionName = $subscription.Name
$AlertRule.Id = $smartDetectorRule.Id
@@ -133,15 +328,19 @@ foreach ($subscription in $subscriptions)
$AlertRule.State = $action.State
$AlertRule.ActionGroupId = $action.ActionGroupId
# Populate Action Group details if found
if ($null -ne $actionGroup) {
$AlertRule.ActionGroupName = $actionGroup.Name
$AlertRule.ActionGroupResourceGroupName = $actionGroup.ResourceGroupName
$AlertRule.ActionGroupEnabled = $actionGroup.Enabled
# Extract receiver information (convert arrays to comma-separated strings)
$AlertRule.ActionGroupArmRoleReceivers = [string] ( $actionGroup.ArmRoleReceivers | ForEach-Object { $_.Name } )
$AlertRule.ActionGroupEmailReceivers = [string] ( $actionGroup.EmailReceivers | ForEach-Object { $_.EmailAddress } )
$AlertRule.AzureFunctionReceivers = [string] ($actionGroup.AzureFunctionReceivers | ForEach-Object { $_.FunctionName } )
}
# Extract governance tags
$AlertRule.Tag_Team = $smartDetectorRule.Tags.team
$AlertRule.Tag_Product = $smartDetectorRule.Tags.product
$AlertRule.Tag_Environment = $smartDetectorRule.Tags.environment
@@ -154,14 +353,19 @@ foreach ($subscription in $subscriptions)
}
}
# microsoft.insights/scheduledqueryrules
# Process Scheduled Query Rules (Log Analytics/KQL-based alerts)
Write-Host "Processing Scheduled Query Rules (Log Analytics alerts)..."
$scheduledQueryRules = Get-AzScheduledQueryRule
$scheduledQueryRulesResources = Get-AzResource -ResourceType "microsoft.insights/scheduledqueryrules"
foreach($scheduledQueryRule in $scheduledQueryRules) {
$resource = $scheduledQueryRulesResources | where { $_.id -eq $scheduledQueryRule.Id }
Write-Host " Found $($scheduledQueryRules.Count) Scheduled Query Rule(s)"
foreach ($scheduledQueryRule in $scheduledQueryRules) {
# Get corresponding resource for tag information
$resource = $scheduledQueryRulesResources | Where-Object { $_.id -eq $scheduledQueryRule.Id }
if (($null -eq $scheduledQueryRule.ActionGroup) -or ($scheduledQueryRule.ActionGroup.Length -eq 0))
{
# Handle Scheduled Query Rules without Action Groups
if (($null -eq $scheduledQueryRule.ActionGroup) -or ($scheduledQueryRule.ActionGroup.Length -eq 0)) {
# Create alert rule entry without Action Group details
[AlertRule] $AlertRule = [AlertRule]::new()
$AlertRule.SubscriptionId = $subscription.Id
$AlertRule.SubscriptionName = $subscription.Name
@@ -171,57 +375,71 @@ foreach ($subscription in $subscriptions)
$AlertRule.ResourceGroupName = $resource.ResourceGroupName
$AlertRule.Description = GetDecentDescription $scheduledQueryRule.Description
$AlertRule.State = $scheduledQueryRule.Enabled -eq $true ? "Enabled" : "Disabled"
$AlertRule.Tag_Team = $smartDetectorRule.Tags.team
$AlertRule.Tag_Product = $smartDetectorRule.Tags.product
$AlertRule.Tag_Environment = $smartDetectorRule.Tags.environment
$AlertRule.Tag_Data = $smartDetectorRule.Tags.data
$AlertRule.Tag_CreatedOnDate = $smartDetectorRule.Tags.CreatedOnDate
$AlertRule.Tag_Deployment = $smartDetectorRule.Tags.drp_deployment
# Extract governance tags from the resource (note: using $resource instead of $smartDetectorRule)
$AlertRule.Tag_Team = $resource.Tags.team
$AlertRule.Tag_Product = $resource.Tags.product
$AlertRule.Tag_Environment = $resource.Tags.environment
$AlertRule.Tag_Data = $resource.Tags.data
$AlertRule.Tag_CreatedOnDate = $resource.Tags.CreatedOnDate
$AlertRule.Tag_Deployment = $resource.Tags.drp_deployment
$Result += $AlertRule
}
else {
foreach($action in $scheduledQueryRule.ActionGroup) {
# Process Scheduled Query Rules with Action Groups
foreach ($action in $scheduledQueryRule.ActionGroup) {
# Create alert rule entry with Action Group details
[AlertRule] $AlertRule = [AlertRule]::new()
$actionGroup = $actionGroups | where { $_.id -eq [uri]::UnescapeDataString($action) }
# Find corresponding Action Group from pre-loaded collection
$actionGroup = $actionGroups | Where-Object { $_.id -eq [uri]::UnescapeDataString($action) }
# Populate basic alert rule information
$AlertRule.SubscriptionId = $subscription.Id
$AlertRule.SubscriptionName = $subscription.Name
$AlertRule.Id = $scheduledQueryRule.Id
$AlertRule.Name = $scheduledQueryRule.Name
$AlertRule.Type = $scheduledQueryRule.Type
$AlertRule.ResourceGroupName = $resource.ResourceGroupName
$AlertRule.Description = GetDecentDescription $scheduledQueryRule.Description
$AlertRule.Description = GetDecentDescription $scheduledQueryRule.Description
$AlertRule.State = $scheduledQueryRule.Enabled -eq $true ? "Enabled" : "Disabled"
$AlertRule.ActionGroupId = $action
# Populate Action Group details if found
if ($null -ne $actionGroup) {
$AlertRule.ActionGroupName = $actionGroup.Name
$AlertRule.ActionGroupResourceGroupName = $actionGroup.ResourceGroupName
$AlertRule.ActionGroupEnabled = $actionGroup.Enabled
# Extract receiver information (convert arrays to comma-separated strings)
$AlertRule.ActionGroupArmRoleReceivers = [string] ( $actionGroup.ArmRoleReceivers | ForEach-Object { $_.Name } )
$AlertRule.ActionGroupEmailReceivers = [string] ( $actionGroup.EmailReceivers | ForEach-Object { $_.EmailAddress } )
$AlertRule.AzureFunctionReceivers = [string] ($actionGroup.AzureFunctionReceivers | ForEach-Object { $_.FunctionName } )
}
$AlertRule.Tag_Team = $smartDetectorRule.Tags.team
$AlertRule.Tag_Product = $smartDetectorRule.Tags.product
$AlertRule.Tag_Environment = $smartDetectorRule.Tags.environment
$AlertRule.Tag_Data = $smartDetectorRule.Tags.data
$AlertRule.Tag_CreatedOnDate = $smartDetectorRule.Tags.CreatedOnDate
$AlertRule.Tag_Deployment = $smartDetectorRule.Tags.drp_deployment
# Extract governance tags from the resource
$AlertRule.Tag_Team = $resource.Tags.team
$AlertRule.Tag_Product = $resource.Tags.product
$AlertRule.Tag_Environment = $resource.Tags.environment
$AlertRule.Tag_Data = $resource.Tags.data
$AlertRule.Tag_CreatedOnDate = $resource.Tags.CreatedOnDate
$AlertRule.Tag_Deployment = $resource.Tags.drp_deployment
$Result += $AlertRule
}
}
}
# Microsoft.Insights/metricAlerts
# Process Metric Alert Rules (Resource metric-based alerts)
Write-Host "Processing Metric Alert Rules..."
$metricAlerts = Get-AzMetricAlertRuleV2
foreach($metricAlert in $metricAlerts) {
if (($null -eq $metricAlert.Actions) -or ($metricAlert.Actions.Length -eq 0))
{
Write-Host " Found $($metricAlerts.Count) Metric Alert Rule(s)"
foreach ($metricAlert in $metricAlerts) {
# Handle Metric Alerts without Action Groups
if (($null -eq $metricAlert.Actions) -or ($metricAlert.Actions.Length -eq 0)) {
# Create alert rule entry without Action Group details
[AlertRule] $AlertRule = [AlertRule]::new()
$AlertRule.SubscriptionId = $subscription.Id
$AlertRule.SubscriptionName = $subscription.Name
@@ -231,6 +449,8 @@ foreach ($subscription in $subscriptions)
$AlertRule.ResourceGroupName = $metricAlert.ResourceGroup
$AlertRule.Description = GetDecentDescription $metricAlert.Description
$AlertRule.State = $metricAlert.Enabled -eq $true ? "Enabled" : "Disabled"
# Extract governance tags
$AlertRule.Tag_Team = $metricAlert.Tags.team
$AlertRule.Tag_Product = $metricAlert.Tags.product
$AlertRule.Tag_Environment = $metricAlert.Tags.environment
@@ -241,11 +461,15 @@ foreach ($subscription in $subscriptions)
$Result += $AlertRule
}
else {
foreach($action in $metricAlert.Actions) {
# Process Metric Alerts with Action Groups
foreach ($action in $metricAlert.Actions) {
# Create alert rule entry with Action Group details
[AlertRule] $AlertRule = [AlertRule]::new()
$actionGroup = $actionGroups | where { $_.id -eq [uri]::UnescapeDataString($action.ActionGroupId) }
# Find corresponding Action Group from pre-loaded collection
$actionGroup = $actionGroups | Where-Object { $_.id -eq [uri]::UnescapeDataString($action.ActionGroupId) }
# Populate basic alert rule information
$AlertRule.SubscriptionId = $subscription.Id
$AlertRule.SubscriptionName = $subscription.Name
$AlertRule.Id = $metricAlert.Id
@@ -256,15 +480,19 @@ foreach ($subscription in $subscriptions)
$AlertRule.State = $metricAlert.Enabled -eq $true ? "Enabled" : "Disabled"
$AlertRule.ActionGroupId = $action.ActionGroupId
# Populate Action Group details if found
if ($null -ne $actionGroup) {
$AlertRule.ActionGroupName = $actionGroup.Name
$AlertRule.ActionGroupResourceGroupName = $actionGroup.ResourceGroupName
$AlertRule.ActionGroupEnabled = $actionGroup.Enabled
# Extract receiver information (convert arrays to comma-separated strings)
$AlertRule.ActionGroupArmRoleReceivers = [string] ( $actionGroup.ArmRoleReceivers | ForEach-Object { $_.Name } )
$AlertRule.ActionGroupEmailReceivers = [string] ( $actionGroup.EmailReceivers | ForEach-Object { $_.EmailAddress } )
$AlertRule.AzureFunctionReceivers = [string] ($actionGroup.AzureFunctionReceivers | ForEach-Object { $_.FunctionName } )
}
# Extract governance tags
$AlertRule.Tag_Team = $metricAlert.Tags.team
$AlertRule.Tag_Product = $metricAlert.Tags.product
$AlertRule.Tag_Environment = $metricAlert.Tags.environment
@@ -277,13 +505,15 @@ foreach ($subscription in $subscriptions)
}
}
# Microsoft.Insights/ActivityLogAlerts
# Process Activity Log Alert Rules (Azure Activity Log event alerts)
Write-Host "Processing Activity Log Alert Rules..."
$activityLogAlerts = Get-AzActivityLogAlert
foreach($activityLogAlert in $activityLogAlerts) {
if (($null -eq $activityLogAlert.ActionGroup) -or ($activityLogAlert.ActionGroup.Length -eq 0))
{
Write-Host " Found $($activityLogAlerts.Count) Activity Log Alert Rule(s)"
foreach ($activityLogAlert in $activityLogAlerts) {
# Handle Activity Log Alerts without Action Groups
if (($null -eq $activityLogAlert.ActionGroup) -or ($activityLogAlert.ActionGroup.Length -eq 0)) {
# Create alert rule entry without Action Group details
[AlertRule] $AlertRule = [AlertRule]::new()
$AlertRule.SubscriptionId = $subscription.Id
$AlertRule.SubscriptionName = $subscription.Name
@@ -293,6 +523,8 @@ foreach ($subscription in $subscriptions)
$AlertRule.ResourceGroupName = $activityLogAlert.ResourceGroupName
$AlertRule.Description = GetDecentDescription $activityLogAlert.Description
$AlertRule.State = $activityLogAlert.Enabled -eq $true ? "Enabled" : "Disabled"
# Extract governance tags
$AlertRule.Tag_Team = $activityLogAlert.Tags.team
$AlertRule.Tag_Product = $activityLogAlert.Tags.product
$AlertRule.Tag_Environment = $activityLogAlert.Tags.environment
@@ -303,11 +535,15 @@ foreach ($subscription in $subscriptions)
$Result += $AlertRule
}
else {
foreach($action in $activityLogAlert.ActionGroup) {
# Process Activity Log Alerts with Action Groups
foreach ($action in $activityLogAlert.ActionGroup) {
# Create alert rule entry with Action Group details
[AlertRule] $AlertRule = [AlertRule]::new()
$actionGroup = $actionGroups | where { $_.id -eq [uri]::UnescapeDataString($action.Id) }
# Find corresponding Action Group from pre-loaded collection
$actionGroup = $actionGroups | Where-Object { $_.id -eq [uri]::UnescapeDataString($action.Id) }
# Populate basic alert rule information
$AlertRule.SubscriptionId = $subscription.Id
$AlertRule.SubscriptionName = $subscription.Name
$AlertRule.Id = $activityLogAlert.Id
@@ -318,15 +554,19 @@ foreach ($subscription in $subscriptions)
$AlertRule.State = $activityLogAlert.Enabled -eq $true ? "Enabled" : "Disabled"
$AlertRule.ActionGroupId = $action.Id
# Populate Action Group details if found
if ($null -ne $actionGroup) {
$AlertRule.ActionGroupName = $actionGroup.Name
$AlertRule.ActionGroupResourceGroupName = $actionGroup.ResourceGroupName
$AlertRule.ActionGroupEnabled = $actionGroup.Enabled
# Extract receiver information (convert arrays to comma-separated strings)
$AlertRule.ActionGroupArmRoleReceivers = [string] ( $actionGroup.ArmRoleReceivers | ForEach-Object { $_.Name } )
$AlertRule.ActionGroupEmailReceivers = [string] ( $actionGroup.EmailReceivers | ForEach-Object { $_.EmailAddress } )
$AlertRule.AzureFunctionReceivers = [string] ($actionGroup.AzureFunctionReceivers | ForEach-Object { $_.FunctionName } )
}
# Extract governance tags
$AlertRule.Tag_Team = $activityLogAlert.Tags.team
$AlertRule.Tag_Product = $activityLogAlert.Tags.product
$AlertRule.Tag_Environment = $activityLogAlert.Tags.environment
@@ -338,8 +578,54 @@ foreach ($subscription in $subscriptions)
}
}
}
Write-Host "Completed processing subscription: $($subscription.Name)"
Write-Host ""
}
# Export results and display summary
Write-Host "======================================================================================================================================================================"
Write-Host "Exporting results and generating summary..."
Write-Host "======================================================================================================================================================================"
# Export comprehensive alert rules data to CSV
$Result | Export-Csv -Path $fileName -NoTypeInformation -Force
$Result | ft
# Generate summary statistics
$summaryStats = @{
TotalAlertRules = $Result.Count
SmartDetectorRules = ($Result | Where-Object { $_.Type -eq "microsoft.alertsmanagement/smartdetectoralertrules" }).Count
ScheduledQueryRules = ($Result | Where-Object { $_.Type -eq "microsoft.insights/scheduledqueryrules" }).Count
MetricAlerts = ($Result | Where-Object { $_.Type -eq "Microsoft.Insights/metricAlerts" }).Count
ActivityLogAlerts = ($Result | Where-Object { $_.Type -eq "Microsoft.Insights/ActivityLogAlerts" }).Count
EnabledRules = ($Result | Where-Object { $_.State -eq "Enabled" }).Count
DisabledRules = ($Result | Where-Object { $_.State -eq "Disabled" }).Count
RulesWithActionGroups = ($Result | Where-Object { $_.ActionGroupId -ne "" }).Count
RulesWithoutActionGroups = ($Result | Where-Object { $_.ActionGroupId -eq "" }).Count
}
Write-Host "Alert Rules Inventory Summary:"
Write-Host "==============================="
Write-Host "Total Alert Rules Found: $($summaryStats.TotalAlertRules)"
Write-Host ""
Write-Host "By Alert Type:"
Write-Host " Smart Detector Rules: $($summaryStats.SmartDetectorRules)"
Write-Host " Scheduled Query Rules (Log Analytics): $($summaryStats.ScheduledQueryRules)"
Write-Host " Metric Alert Rules: $($summaryStats.MetricAlerts)"
Write-Host " Activity Log Alert Rules: $($summaryStats.ActivityLogAlerts)"
Write-Host ""
Write-Host "By State:"
Write-Host " Enabled Rules: $($summaryStats.EnabledRules)"
Write-Host " Disabled Rules: $($summaryStats.DisabledRules)"
Write-Host ""
Write-Host "By Action Group Association:"
Write-Host " Rules with Action Groups: $($summaryStats.RulesWithActionGroups)"
Write-Host " Rules without Action Groups: $($summaryStats.RulesWithoutActionGroups)"
Write-Host ""
Write-Host "Results exported to: $fileName"
Write-Host "======================================================================================================================================================================"
# Display formatted table output to console
Write-Host ""
Write-Host "Detailed Alert Rules (displaying first 50 rows):"
$Result | Select-Object -First 50 | Format-Table -AutoSize

View File

@@ -1,56 +1,288 @@
#Connect-AzAccount
<#
.SYNOPSIS
Exports detailed information about Azure Application Insights resources across all enabled subscriptions.
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date appinsights.csv"
.DESCRIPTION
This script analyzes all Application Insights resources across enabled Azure subscriptions and
collects comprehensive information including:
- Basic resource metadata (ID, name, resource group, subscription)
- Log Analytics workspace associations
- Resource tags for governance and organization
The script is particularly useful for:
- Application Insights inventory and governance
- Monitoring workspace associations for centralized logging
- Tag compliance auditing
- Cost management and resource organization
$subscriptions = Get-AzSubscription | Where-Object State -eq "Enabled"
.PARAMETER SubscriptionFilter
Optional array of subscription IDs to analyze. If not specified, all enabled subscriptions are processed.
class AppInsightsCheck {
[string] $SubscriptionId = ""
[string] $SubscriptionName = ""
[string] $Id = ""
[string] $ResourceGroupName = ""
[string] $Name = ""
[string] $WorkspaceResourceId = ""
[string] $Tag_Team = ""
[string] $Tag_Product = ""
[string] $Tag_Environment = ""
[string] $Tag_Data = ""
[string] $Tag_CreatedOnDate = ""
[string] $Tag_Deployment = ""
.PARAMETER OutputPath
Custom path for the output CSV file. If not specified, creates a timestamped file in the current directory.
.EXAMPLE
.\AppInsightsWorkspace.ps1
Analyzes all Application Insights resources across all enabled subscriptions.
.EXAMPLE
.\AppInsightsWorkspace.ps1 -SubscriptionFilter @("12345678-1234-1234-1234-123456789012", "87654321-4321-4321-4321-210987654321")
Analyzes Application Insights resources in specific subscriptions only.
.EXAMPLE
.\AppInsightsWorkspace.ps1 -OutputPath "C:\Reports\appinsights-analysis.csv"
Analyzes all Application Insights resources and saves to a custom location.
.OUTPUTS
Creates a CSV file with the following columns:
- SubscriptionId: Azure subscription unique identifier
- SubscriptionName: Azure subscription display name
- Id: Application Insights resource ID
- ResourceGroupName: Resource group containing the Application Insights resource
- Name: Application Insights resource name
- WorkspaceResourceId: Associated Log Analytics workspace resource ID (if any)
- Tag_Team: Value of 'team' tag
- Tag_Product: Value of 'product' tag
- Tag_Environment: Value of 'environment' tag
- Tag_Data: Value of 'data' tag
- Tag_CreatedOnDate: Value of 'CreatedOnDate' tag
- Tag_Deployment: Value of 'drp_deployment' tag
Also displays a formatted table of results in the console.
.NOTES
Author: Cloud Engineering Team
Created: 2025
Requires: PowerShell 5.1 or later, Az PowerShell module
Dependencies: Az.ApplicationInsights, Az.Accounts, Az.Resources modules
Prerequisites:
- Install Az PowerShell module: Install-Module -Name Az
- Connect to Azure: Connect-AzAccount
- Appropriate permissions to read Application Insights resources across target subscriptions
Performance Considerations:
- Processing time depends on the number of subscriptions and Application Insights resources
- The script switches contexts between subscriptions, which may take time with many subscriptions
- Large numbers of resources may result in longer execution times
Tag Analysis:
The script looks for specific tags commonly used for governance:
- team: Identifies the responsible team
- product: Associates the resource with a product or service
- environment: Indicates the environment (dev, test, prod, etc.)
- data: Data classification or sensitivity level
- CreatedOnDate: Resource creation timestamp
- drp_deployment: Deployment-related information
Workspace Association:
- Modern Application Insights resources should be associated with Log Analytics workspaces
- Resources without workspace associations may be using legacy standalone mode
- Workspace associations enable advanced querying and cross-resource analytics
.LINK
https://docs.microsoft.com/en-us/powershell/module/az.applicationinsights/
https://docs.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview
#>
param(
[Parameter(Mandatory = $false, HelpMessage = "Array of subscription IDs to analyze (analyzes all enabled subscriptions if not specified)")]
[string[]]$SubscriptionFilter = @(),
[Parameter(Mandatory = $false, HelpMessage = "Custom path for the output CSV file")]
[string]$OutputPath = ""
)
# Check Azure PowerShell authentication
Write-Host "Verifying Azure PowerShell authentication..." -ForegroundColor Yellow
try {
$azContext = Get-AzContext
if (-not $azContext) {
Write-Host "Not authenticated to Azure. Attempting to connect..." -ForegroundColor Yellow
Connect-AzAccount
$azContext = Get-AzContext
}
Write-Host "Azure authentication verified - Account: $($azContext.Account.Id)" -ForegroundColor Green
}
catch {
Write-Host "ERROR: Unable to authenticate to Azure. Please run 'Connect-AzAccount' manually." -ForegroundColor Red
exit 1
}
# Generate filename if not provided
if (-not $OutputPath) {
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$OutputPath = ".\$date appinsights.csv"
}
# Get target subscriptions based on filter or all enabled subscriptions
Write-Host "Retrieving target subscriptions..." -ForegroundColor Yellow
if ($SubscriptionFilter.Count -gt 0) {
$subscriptions = $SubscriptionFilter | ForEach-Object {
Get-AzSubscription -SubscriptionId $_ | Where-Object State -eq "Enabled"
} | Where-Object { $_ -ne $null }
Write-Host "Analyzing $($subscriptions.Count) filtered subscriptions" -ForegroundColor Green
} else {
$subscriptions = Get-AzSubscription | Where-Object State -eq "Enabled"
Write-Host "Analyzing all $($subscriptions.Count) enabled subscriptions" -ForegroundColor Green
}
# Define a class to structure Application Insights resource information
class AppInsightsCheck {
[string] $SubscriptionId = "" # Azure subscription unique identifier
[string] $SubscriptionName = "" # Azure subscription display name
[string] $Id = "" # Application Insights resource ID
[string] $ResourceGroupName = "" # Resource group containing the resource
[string] $Name = "" # Application Insights resource name
[string] $WorkspaceResourceId = "" # Associated Log Analytics workspace resource ID
[string] $Tag_Team = "" # Team responsible for the resource
[string] $Tag_Product = "" # Product or service association
[string] $Tag_Environment = "" # Environment designation (dev, test, prod)
[string] $Tag_Data = "" # Data classification or sensitivity level
[string] $Tag_CreatedOnDate = "" # Resource creation date from tags
[string] $Tag_Deployment = "" # Deployment-related information
}
# Initialize array to store Application Insights analysis results
[AppInsightsCheck[]]$Result = @()
$totalResourcesProcessed = 0
foreach ($subscription in $subscriptions)
{
Set-AzContext -SubscriptionId $subscription.Id
# Display analysis banner
Write-Host "`n========================================================================================================================================================================"
Write-Host "AZURE APPLICATION INSIGHTS ANALYSIS"
Write-Host "========================================================================================================================================================================"
$allAppinsights = Get-AzApplicationInsights
foreach ($appinsights in $allAppinsights)
{
[AppInsightsCheck] $AppInsightsCheck = [AppInsightsCheck]::new()
$AppInsightsCheck.SubscriptionId = $subscription.Id
$AppInsightsCheck.SubscriptionName = $subscription.Name
$AppInsightsCheck.Id = $appinsights.Id
$AppInsightsCheck.Name = $appinsights.Name
$AppInsightsCheck.ResourceGroupName = $appinsights.ResourceGroupName
$AppInsightsCheck.WorkspaceResourceId = $appinsights.WorkspaceResourceId
$resource = Get-AzResource -ResourceId $appinsights.Id
$AppInsightsCheck.Tag_Team = $resource.Tags.team
$AppInsightsCheck.Tag_Product = $resource.Tags.product
$AppInsightsCheck.Tag_Environment = $resource.Tags.environment
$AppInsightsCheck.Tag_Data = $resource.Tags.data
$AppInsightsCheck.Tag_CreatedOnDate = $resource.Tags.CreatedOnDate
$AppInsightsCheck.Tag_Deployment = $resource.Tags.drp_deployment
# Process each subscription to analyze Application Insights resources
foreach ($subscription in $subscriptions) {
Write-Host "`nAnalyzing subscription: $($subscription.Name) ($($subscription.Id))" -ForegroundColor Cyan
try {
# Switch to the current subscription context
Set-AzContext -SubscriptionId $subscription.Id -ErrorAction Stop | Out-Null
$Result += $AppInsightsCheck
# Get all Application Insights resources in the subscription
Write-Host " Retrieving Application Insights resources..." -ForegroundColor Gray
$allAppinsights = Get-AzApplicationInsights -ErrorAction Stop
if ($allAppinsights.Count -eq 0) {
Write-Host " No Application Insights resources found" -ForegroundColor Yellow
continue
}
Write-Host " Found $($allAppinsights.Count) Application Insights resources" -ForegroundColor Green
# Process each Application Insights resource
foreach ($appinsights in $allAppinsights) {
Write-Host " Processing: $($appinsights.Name)" -ForegroundColor Gray
try {
# Create new analysis object and populate basic information
[AppInsightsCheck] $AppInsightsCheck = [AppInsightsCheck]::new()
$AppInsightsCheck.SubscriptionId = $subscription.Id
$AppInsightsCheck.SubscriptionName = $subscription.Name
$AppInsightsCheck.Id = $appinsights.Id
$AppInsightsCheck.Name = $appinsights.Name
$AppInsightsCheck.ResourceGroupName = $appinsights.ResourceGroupName
$AppInsightsCheck.WorkspaceResourceId = $appinsights.WorkspaceResourceId
# Check workspace association
if ($appinsights.WorkspaceResourceId) {
Write-Host " Workspace-based Application Insights" -ForegroundColor Green
} else {
Write-Host " Legacy standalone Application Insights (consider migrating)" -ForegroundColor Yellow
}
# Retrieve detailed resource information for tags
Write-Host " Retrieving resource tags..." -ForegroundColor Gray
$resource = Get-AzResource -ResourceId $appinsights.Id -ErrorAction Stop
# Extract governance tags
$AppInsightsCheck.Tag_Team = $resource.Tags.team
$AppInsightsCheck.Tag_Product = $resource.Tags.product
$AppInsightsCheck.Tag_Environment = $resource.Tags.environment
$AppInsightsCheck.Tag_Data = $resource.Tags.data
$AppInsightsCheck.Tag_CreatedOnDate = $resource.Tags.CreatedOnDate
$AppInsightsCheck.Tag_Deployment = $resource.Tags.drp_deployment
# Report on tag compliance
$tagCount = @($AppInsightsCheck.Tag_Team, $AppInsightsCheck.Tag_Product, $AppInsightsCheck.Tag_Environment) | Where-Object { $_ } | Measure-Object | Select-Object -ExpandProperty Count
if ($tagCount -eq 3) {
Write-Host " All required tags present" -ForegroundColor Green
} else {
Write-Host " Missing required tags (team, product, environment)" -ForegroundColor Yellow
}
# Add to results
$Result += $AppInsightsCheck
$totalResourcesProcessed++
} catch {
Write-Host " ERROR processing resource: $($_.Exception.Message)" -ForegroundColor Red
# Still add basic info even if tag retrieval fails
[AppInsightsCheck] $AppInsightsCheck = [AppInsightsCheck]::new()
$AppInsightsCheck.SubscriptionId = $subscription.Id
$AppInsightsCheck.SubscriptionName = $subscription.Name
$AppInsightsCheck.Id = $appinsights.Id
$AppInsightsCheck.Name = $appinsights.Name
$AppInsightsCheck.ResourceGroupName = $appinsights.ResourceGroupName
$AppInsightsCheck.WorkspaceResourceId = $appinsights.WorkspaceResourceId
$Result += $AppInsightsCheck
$totalResourcesProcessed++
}
}
} catch {
Write-Host " ERROR accessing subscription: $($_.Exception.Message)" -ForegroundColor Red
continue
}
}
$Result | Export-Csv -Path $fileName -NoTypeInformation -Force
# Export results to CSV file
Write-Host "`nExporting results to: $OutputPath" -ForegroundColor Yellow
$Result | Export-Csv -Path $OutputPath -NoTypeInformation -Force
$Result | ft
# Calculate and display summary statistics
$totalSubscriptions = $subscriptions.Count
$workspaceBasedCount = ($Result | Where-Object { $_.WorkspaceResourceId -ne "" }).Count
$legacyCount = ($Result | Where-Object { $_.WorkspaceResourceId -eq "" }).Count
$taggedResourcesCount = ($Result | Where-Object { $_.Tag_Team -ne "" -and $_.Tag_Product -ne "" -and $_.Tag_Environment -ne "" }).Count
# Display completion summary
Write-Host "`n========================================================================================================================================================================"
Write-Host "APPLICATION INSIGHTS ANALYSIS COMPLETED SUCCESSFULLY!" -ForegroundColor Green
Write-Host "========================================================================================================================================================================"
Write-Host ""
Write-Host "SUMMARY STATISTICS:" -ForegroundColor Cyan
Write-Host "Subscriptions analyzed: $totalSubscriptions" -ForegroundColor Yellow
Write-Host "Total Application Insights resources: $totalResourcesProcessed" -ForegroundColor Yellow
Write-Host "Workspace-based resources: $workspaceBasedCount" -ForegroundColor Yellow
Write-Host "Legacy standalone resources: $legacyCount" -ForegroundColor Yellow
Write-Host "Resources with complete tags (team, product, environment): $taggedResourcesCount" -ForegroundColor Yellow
# Highlight areas needing attention
if ($legacyCount -gt 0) {
Write-Host ""
Write-Host "RECOMMENDATIONS:" -ForegroundColor Cyan
Write-Host "- $legacyCount legacy Application Insights resources should be migrated to workspace-based mode" -ForegroundColor Yellow
}
if ($taggedResourcesCount -lt $totalResourcesProcessed) {
$untaggedCount = $totalResourcesProcessed - $taggedResourcesCount
Write-Host "- $untaggedCount resources are missing required governance tags" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Output file: $OutputPath" -ForegroundColor Yellow
# Display results table
Write-Host ""
Write-Host "DETAILED RESULTS:" -ForegroundColor Cyan
$Result | Format-Table -Property SubscriptionName, Name, ResourceGroupName, @{
Name = 'WorkspaceAssociated'
Expression = { if ($_.WorkspaceResourceId) { 'Yes' } else { 'No' } }
}, Tag_Team, Tag_Product, Tag_Environment -AutoSize
Write-Host "========================================================================================================================================================================"

View File

@@ -1,72 +1,192 @@
#Connect-AzAccount
<#
.SYNOPSIS
Exports Azure Privileged Identity Management (PIM) role eligible assignments across all management groups, subscriptions, resource groups, and resources.
.DESCRIPTION
This script comprehensively inventories all PIM eligible role assignments across the entire Azure environment hierarchy.
It traverses management groups, subscriptions, resource groups, and individual resources to collect detailed information
about role eligibility schedules. The script uses Azure REST API calls to retrieve PIM data and exports results to CSV.
The script performs a complete audit of:
- Management Group level PIM assignments
- Subscription level PIM assignments
- Resource Group level PIM assignments
- Individual Resource level PIM assignments
All data is consolidated into a single CSV file with detailed scope and principal information.
.PARAMETER None
This script does not accept parameters. It processes all accessible management groups and their child resources.
.OUTPUTS
CSV file named with timestamp pattern: "yyyy-MM-dd HHmm azure_pim_assignments.csv"
Contains columns for scope hierarchy, role definitions, principals, and assignment metadata.
.EXAMPLE
.\AzurePIM.ps1
Exports all PIM eligible assignments to a timestamped CSV file in the current directory.
.NOTES
Author: Cloud Engineering Team
Version: 1.0
Created: 2024
Prerequisites:
- Azure PowerShell module (Az) must be installed
- User must be authenticated (Connect-AzAccount)
- Requires appropriate permissions to read PIM assignments across all scopes
- Tenant ID is hardcoded and may need adjustment for different environments
Security Considerations:
- Script uses Azure access tokens for REST API authentication
- Requires elevated permissions to access PIM data across all scopes
- Output file contains sensitive role assignment information
Performance Notes:
- Processing time varies significantly based on environment size
- Script processes resources sequentially which may take considerable time
- Consider running during off-peak hours for large environments
.LINK
https://docs.microsoft.com/en-us/azure/active-directory/privileged-identity-management/
https://docs.microsoft.com/en-us/rest/api/authorization/roleeligibilityscheduleinstances
#>
#Requires -Modules Az
#Connect-AzAccount
# Class definition for structured PIM assignment data
class ResourceCheck {
[string] $Level = ""
[string] $ManagementGroupId = ""
[string] $ManagementGroupName = ""
[string] $SubscriptionId = ""
[string] $SubscriptionName = ""
[string] $ResourceId = ""
[string] $ResourceGroup = ""
[string] $ResourceName = ""
[string] $ResourceType = ""
[string] $RoleEligibilityScheduleId = ""
[string] $Scope = ""
[string] $RoleDefinitionId = ""
[string] $RoleDefinitionName = ""
[string] $RoleDefinitionType = ""
[string] $PrincipalId = ""
[string] $PrincipalName = ""
[string] $PrincipalType = ""
[string] $Status = ""
[string] $StartDateTime = ""
[string] $EndDateTime = ""
[string] $CreatedOn = ""
# Hierarchy level indicators
[string] $Level = "" # Management Group, Subscription, Resource Group, or Resource
[string] $ManagementGroupId = "" # Management group identifier
[string] $ManagementGroupName = "" # Management group display name
[string] $SubscriptionId = "" # Azure subscription GUID
[string] $SubscriptionName = "" # Subscription display name
# Resource identification
[string] $ResourceId = "" # Full Azure resource identifier
[string] $ResourceGroup = "" # Resource group name
[string] $ResourceName = "" # Individual resource name
[string] $ResourceType = "" # Azure resource type
# PIM assignment details
[string] $RoleEligibilityScheduleId = "" # Unique PIM schedule identifier
[string] $Scope = "" # Assignment scope path
[string] $RoleDefinitionId = "" # Azure RBAC role definition ID
[string] $RoleDefinitionName = "" # Human-readable role name
[string] $RoleDefinitionType = "" # Role definition type
# Principal (user/group/service principal) information
[string] $PrincipalId = "" # Principal object ID
[string] $PrincipalName = "" # Principal display name
[string] $PrincipalType = "" # User, Group, or ServicePrincipal
# Assignment metadata
[string] $Status = "" # Assignment status (Active, Eligible, etc.)
[string] $StartDateTime = "" # Assignment start date/time
[string] $EndDateTime = "" # Assignment expiration date/time
[string] $CreatedOn = "" # Assignment creation timestamp
}
function GetEligibleAssignments {
<#
.SYNOPSIS
Retrieves PIM eligible role assignments for a specified scope using Azure Management REST API.
.DESCRIPTION
This function queries the Azure Management REST API to retrieve role eligibility schedule instances
for a given scope. It handles authentication using Azure access tokens and filters results to
exclude inherited assignments.
.PARAMETER scope
The Azure resource scope path to query for PIM assignments. Can be management group, subscription,
resource group, or individual resource scope.
.OUTPUTS
Returns an array of PIM assignment objects with properties containing assignment details,
or empty string if no assignments found.
.EXAMPLE
GetEligibleAssignments -scope "subscriptions/12345678-1234-1234-1234-123456789012"
Retrieves all PIM eligible assignments for the specified subscription.
.NOTES
- Uses hardcoded tenant ID which may need adjustment for different environments
- REST API version 2020-10-01 is used for compatibility
- Filters out inherited assignments to show only direct assignments
#>
function GetEligibleAssignments {
param (
[Parameter(Mandatory = $true)]
[string] $scope
)
$access_token_secure = (Get-AzAccessToken -TenantId "e9792fd7-4044-47e7-a40d-3fba46f1cd09").Token
$access_token = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($access_token_secure))
try {
# Get Azure access token for the specified tenant
# Note: Tenant ID is hardcoded and should be updated for different environments
$access_token_secure = (Get-AzAccessToken -TenantId "e9792fd7-4044-47e7-a40d-3fba46f1cd09").Token
$access_token = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($access_token_secure))
$url = "https://management.azure.com/$scope/providers/Microsoft.Authorization/roleEligibilityScheduleInstances?api-version=2020-10-01&`$filter=atScope()"
$head = @{ Authorization =" Bearer $access_token" }
$response = Invoke-RestMethod -Uri $url -Method GET -Headers $head
$response | ForEach-Object {
$responseValue = $_.value
if ($responseValue.Length -gt 0) {
return $responseValue | ForEach-Object {
return ($_.properties | Where-Object MemberType -NE "Inherited")
}
# Construct REST API URL for role eligibility schedule instances
$url = "https://management.azure.com/$scope/providers/Microsoft.Authorization/roleEligibilityScheduleInstances?api-version=2020-10-01&`$filter=atScope()"
# Create authorization header with bearer token
$head = @{ Authorization = " Bearer $access_token" }
# Execute REST API call to retrieve PIM assignments
$response = Invoke-RestMethod -Uri $url -Method GET -Headers $head
# Process response and filter out inherited assignments
$response | ForEach-Object {
$responseValue = $_.value
if ($responseValue.Length -gt 0) {
# Return only direct assignments (exclude inherited)
return $responseValue | ForEach-Object {
return ($_.properties | Where-Object MemberType -NE "Inherited")
}
}
else {
# Return empty string if no assignments found
return ""
}
}
else {
return ""
}
}
}
catch {
Write-Warning "Failed to retrieve PIM assignments for scope: $scope. Error: $($_.Exception.Message)"
return ""
}
}
# Main script execution begins
Write-Host "======================================================================================================================================================================"
Write-Host "Creating PIM assignments overview."
Write-Host "Creating comprehensive PIM assignments overview across all Azure scopes."
Write-Host "======================================================================================================================================================================"
# Generate timestamped filename for CSV export
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_pim_assignments.csv"
$fileName = ".\$date azure_pim_assignments.csv"
Write-Host "Output file: $fileName"
# Retrieve all management groups accessible to the current user
Write-Host "Retrieving management groups..."
$managementGroups = Get-AzManagementGroup
Write-Host "Found $($managementGroups.Count) management group(s) to process."
# Process each management group for PIM assignments
foreach ($managementGroup in $managementGroups)
{
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "Management group [$($managementGroup.Name)]"
Write-Host "Processing Management Group: [$($managementGroup.Name)] - $($managementGroup.DisplayName)"
# Retrieve PIM assignments at management group level
$assignments = GetEligibleAssignments -scope "providers/Microsoft.Management/managementGroups/$($managementGroup.Name)"
# Process management group level assignments
[ResourceCheck[]]$Result = @()
foreach ($assignment in $assignments) {
# Create structured object for management group assignment
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level = "Management Group"
$resourceCheck.ManagementGroupId = $managementGroup.Id
@@ -85,24 +205,38 @@ foreach ($managementGroup in $managementGroups)
$resourceCheck.CreatedOn = $assignment.createdOn
$Result += $resourceCheck
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
# Export management group assignments to CSV
if ($Result.Count -gt 0) {
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
Write-Host " Exported $($Result.Count) management group assignment(s)"
}
# Retrieve active subscriptions within the management group
$subscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name | Where-Object State -eq "Active"
Write-Host " Found $($subscriptions.Count) active subscription(s) in management group"
# Process each subscription for PIM assignments
foreach ($subscription in $subscriptions)
{
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
# Extract subscription ID from the full path
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
Write-Host "Subscription [$($subscription.DisplayName) - $subscriptionId]"
Write-Host "Processing Subscription: [$($subscription.DisplayName)] - $subscriptionId"
# Set Azure context to the current subscription
Set-AzContext -SubscriptionId $subscriptionId | Out-Null
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
# Retrieve PIM assignments at subscription level
$assignments = GetEligibleAssignments -scope $scope
# Process subscription level assignments
[ResourceCheck[]]$Result = @()
foreach ($assignment in $assignments) {
# Create structured object for subscription assignment
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level = "Subscription"
$resourceCheck.ManagementGroupId = $managementGroup.Id
@@ -123,18 +257,28 @@ foreach ($managementGroup in $managementGroups)
$resourceCheck.CreatedOn = $assignment.createdOn
$Result += $resourceCheck
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
# Export subscription assignments to CSV
if ($Result.Count -gt 0) {
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
Write-Host " Exported $($Result.Count) subscription assignment(s)"
}
# Retrieve all resource groups in the current subscription
$allResourceGroups = Get-AzResourceGroup
Write-Host " Found $($allResourceGroups.Count) resource group(s) to process"
# Process each resource group for PIM assignments
foreach ($group in $allResourceGroups) {
Write-Host " Processing Resource Group: $($group.ResourceGroupName)"
Write-Host $group.ResourceGroupName
# Retrieve PIM assignments at resource group level
$assignments = GetEligibleAssignments -scope $group.ResourceId
# Process resource group level assignments
[ResourceCheck[]]$Result = @()
foreach ($assignment in $assignments) {
# Create structured object for resource group assignment
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level = "Resource Group"
$resourceCheck.ManagementGroupId = $managementGroup.Id
@@ -156,16 +300,26 @@ foreach ($managementGroup in $managementGroups)
$resourceCheck.CreatedOn = $assignment.createdOn
$Result += $resourceCheck
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
# Export resource group assignments to CSV
if ($Result.Count -gt 0) {
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
Write-Host " Exported $($Result.Count) resource group assignment(s)"
}
# Retrieve all resources within the current resource group
$allResources = Get-AzResource -ResourceGroupName $group.ResourceGroupName
# Process each individual resource for PIM assignments
foreach ($resource in $allResources)
{
# Retrieve PIM assignments at individual resource level
$assignments = GetEligibleAssignments -scope $resource.ResourceId
# Process individual resource level assignments
[ResourceCheck[]]$Result = @()
foreach ($assignment in $assignments) {
# Create structured object for resource assignment
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level = "Resource"
$resourceCheck.ManagementGroupId = $managementGroup.Id
@@ -190,13 +344,27 @@ foreach ($managementGroup in $managementGroups)
$resourceCheck.CreatedOn = $assignment.createdOn
$Result += $resourceCheck
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
# Export individual resource assignments to CSV
if ($Result.Count -gt 0) {
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
Write-Host " Exported $($Result.Count) assignment(s) for resource: $($resource.Name)"
}
}
}
}
}
# Script completion summary
Write-Host "======================================================================================================================================================================"
Write-Host "PIM assignments export completed successfully."
Write-Host "Results saved to: $fileName"
Write-Host ""
Write-Host "Summary:"
Write-Host "- Processed $($managementGroups.Count) management group(s)"
Write-Host "- Traversed all subscriptions, resource groups, and individual resources"
Write-Host "- Exported all PIM eligible role assignments to CSV format"
Write-Host ""
Write-Host "Note: Review the CSV file for comprehensive PIM assignment details across all Azure scopes."
Write-Host "======================================================================================================================================================================"
Write-Host "Done."

View File

@@ -1,67 +1,225 @@
<#
.SYNOPSIS
Comprehensive Azure RBAC assignment analysis across entire Azure tenant.
.DESCRIPTION
This script analyzes RBAC assignments across all Azure resources in an Azure tenant,
providing a complete inventory of role assignments at every level of the Azure
resource hierarchy. The script generates a detailed CSV report with comprehensive
metadata including resource information, role assignments, and organizational tags.
Features:
• Recursive analysis of Management Groups, Subscriptions, Resource Groups, and Resources
• Complete RBAC assignment enumeration with role and principal details
• Organizational metadata collection (tags, locations, resource types)
• Hierarchical resource context (management group → subscription → resource group → resource)
• Timestamped CSV output for historical tracking and compliance reporting
• Comprehensive error handling to continue processing despite individual failures
.PARAMETER SubscriptionIds
Optional array of specific subscription IDs to process. If not provided, all active
subscriptions across all management groups will be processed. When specified, only
the listed subscriptions will be analyzed for RBAC assignments.
.PARAMETER OutputPath
Optional custom path for the output CSV file. If not provided, the file will be
created in the current directory with a timestamped filename.
.EXAMPLE
.\AzureRBAC.ps1
Process all management groups and subscriptions to generate complete RBAC inventory.
.EXAMPLE
.\AzureRBAC.ps1 -SubscriptionIds @("a134faf1-7a89-4f2c-8389-06d00bd5e2a7", "30ce4e64-4299-4b93-91b8-4c953f63678e")
Process only the specified subscriptions for RBAC analysis.
.EXAMPLE
.\AzureRBAC.ps1 -SubscriptionIds @("12345678-1234-1234-1234-123456789012") -OutputPath "C:\Reports\rbac-analysis.csv"
Process a specific subscription and save results to a custom location.
.OUTPUTS
CSV file: [YYYY-MM-DD HHMM] azure_rbac_assignments.csv
The output file contains the following columns:
- ResourceId: Azure resource identifier
- Id: Resource ID (for individual resources)
- Kind: Resource type (ManagementGroup, Subscription, ResourceGroup, Resource)
- Location: Azure region/location
- ResourceName: Name of the resource
- ResourceGroupName: Resource group containing the resource
- ResourceType: Azure resource type (e.g., Microsoft.Storage/storageAccounts)
- ManagementGroupId: Parent management group identifier
- ManagementGroupName: Parent management group display name
- SubscriptionId: Subscription identifier
- SubscriptionName: Subscription display name
- Tag_Team: Team tag value for organizational tracking
- Tag_Product: Product tag value for product alignment
- Tag_Environment: Environment tag value (Dev, Test, Prod, etc.)
- Tag_Data: Data classification tag value
- Tag_Delete: Deletion schedule tag value
- Tag_Split: Cost allocation split tag value
- RBAC_RoleAssignmentId: Unique role assignment identifier
- RBAC_Scope: Scope where the role assignment is effective
- RBAC_DisplayName: Display name of the assigned principal
- RBAC_SignInName: Sign-in name/UPN of the assigned principal
- RBAC_RoleDefinitionName: Name of the assigned Azure role
.NOTES
Requires PowerShell modules: Az.Accounts, Az.Resources
Requires appropriate Azure RBAC permissions:
• Reader access or higher on Management Groups
• Reader access or higher on Subscriptions
• Reader access or higher on Resource Groups and Resources
• Microsoft.Authorization/roleAssignments/read permission at appropriate scopes
Authentication:
• Must be authenticated to Azure (Connect-AzAccount) before running
• Service Principal or Managed Identity authentication supported
• Requires appropriate tenant-level permissions for comprehensive analysis
Performance Considerations:
• Processing time scales with number of resources and role assignments
• Large tenants may require several minutes to hours for complete analysis
• Network connectivity and API throttling may affect processing speed
• Memory usage scales with number of resources processed
Compatibility:
• PowerShell 5.1 and PowerShell 7.x
• Azure PowerShell module version 8.0 or later
• Windows, macOS, and Linux support
Output Management:
• CSV files are appended to support incremental data collection
• Timestamped filenames prevent overwrites
• Large result sets may generate substantial file sizes
• Consider data retention and storage management policies
.LINK
https://docs.microsoft.com/en-us/azure/role-based-access-control/
https://docs.microsoft.com/en-us/powershell/azure/
AUTHOR: Cloud Engineering Team
CREATED: Azure governance and compliance toolkit
VERSION: 2.0 - Simplified RBAC analysis without PIM detection
UPDATED: Focused on core RBAC assignment enumeration and organizational metadata
#>
# Script parameters
param(
[Parameter(Mandatory = $false, HelpMessage = "Array of subscription IDs to process. If not specified, all subscriptions will be processed.")]
[string[]]$SubscriptionIds = @(),
[Parameter(Mandatory = $false, HelpMessage = "Custom output path for the CSV file. If not specified, uses timestamped filename in current directory.")]
[string]$OutputPath = ""
)
#Connect-AzAccount
Import-Module Az.Accounts
Import-Module Az.Resources
# PowerShell class to represent Azure resource and RBAC assignment data
class ResourceCheck {
[string] $ResourceId = ""
[string] $Id = ""
[string] $Kind = ""
[string] $Location = ""
[string] $ResourceName = ""
[string] $ResourceGroupName = ""
[string] $ResourceType = ""
[string] $ManagementGroupId = ""
[string] $ManagementGroupName = ""
[string] $SubscriptionId = ""
[string] $SubscriptionName = ""
[string] $Tag_Team = ""
[string] $Tag_Product = ""
[string] $Tag_Environment = ""
[string] $Tag_Data = ""
[string] $Tag_Delete = ""
[string] $Tag_Split = ""
[string] $RBAC_RoleAssignmentId = ""
[string] $RBAC_Scope = ""
[string] $RBAC_DisplayName = ""
[string] $RBAC_SignInName = ""
[string] $RBAC_RoleDefinitionName = ""
# Resource identification and metadata
[string] $ResourceId = "" # Azure resource identifier (full ARM path)
[string] $Id = "" # Resource ID (used for individual resources)
[string] $Kind = "" # Resource level (ManagementGroup, Subscription, ResourceGroup, Resource)
[string] $Location = "" # Azure region/location where resource is deployed
[string] $ResourceName = "" # Name of the resource
[string] $ResourceGroupName = "" # Resource group containing the resource
[string] $ResourceType = "" # Azure resource type (e.g., Microsoft.Storage/storageAccounts)
# Organizational hierarchy context
[string] $ManagementGroupId = "" # Parent management group identifier
[string] $ManagementGroupName = "" # Parent management group display name
[string] $SubscriptionId = "" # Subscription identifier
[string] $SubscriptionName = "" # Subscription display name
# Organizational metadata tags (customize based on organizational tagging strategy)
[string] $Tag_Team = "" # Team responsible for the resource
[string] $Tag_Product = "" # Product/application alignment
[string] $Tag_Environment = "" # Environment classification (Dev, Test, Prod, etc.)
[string] $Tag_Data = "" # Data classification level
[string] $Tag_Delete = "" # Scheduled deletion information
[string] $Tag_Split = "" # Cost allocation split information
# RBAC assignment details
[string] $RBAC_RoleAssignmentId = "" # Unique identifier for the role assignment
[string] $RBAC_Scope = "" # Scope where the role assignment is effective
[string] $RBAC_DisplayName = "" # Display name of the assigned principal
[string] $RBAC_SignInName = "" # Sign-in name/UPN of the assigned principal
[string] $RBAC_RoleDefinitionName = "" # Name of the assigned Azure role
}
# Display script execution banner
Write-Host "========================================================================================================================================================================"
Write-Host "Creating resource RBAC assignment overview."
Write-Host "========================================================================================================================================================================"
Write-Host "Azure RBAC Assignment Analysis"
Write-Host "========================================================================================================================================================================"
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_rbac_assignments.csv"
# Display processing scope information
if ($SubscriptionIds.Count -gt 0) {
Write-Host "SCOPE: Processing specific subscriptions only"
Write-Host "Subscription IDs to process:"
foreach ($subId in $SubscriptionIds) {
Write-Host " - $subId"
}
} else {
Write-Host "SCOPE: Processing ALL active subscriptions across all management groups"
}
Write-Host ""
# Generate filename for output CSV (use custom path or default timestamped filename)
if ($OutputPath -ne "") {
$fileName = $OutputPath
Write-Host "Using custom output file: $fileName"
} else {
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_rbac_assignments.csv"
Write-Host "Using default timestamped filename: $fileName"
}
# Discover all management groups in the tenant
# This provides the top-level organizational structure for Azure resources
$managementGroups = Get-AzManagementGroup
# Process each management group in the tenant
foreach ($managementGroup in $managementGroups)
{
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "Management group [$($managementGroup.Name)]"
# Initialize collection for management group level role assignments
[ResourceCheck[]]$Result = @()
try {
# Get role assignments directly assigned to this management group (not inherited)
$roleAssignments = Get-AzRoleAssignment -Scope $managementGroup.Id | Where-Object Scope -eq $managementGroup.Id
# Process each role assignment at the management group level
foreach($roleAssignment in $roleAssignments) {
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ResourceId = ""
$resourceCheck.Kind = "ManagementGroup"
$resourceCheck.Location = ""
$resourceCheck.ResourceGroupName = ""
# Set management group context (no individual resource context at this level)
$resourceCheck.ResourceId = "" # No specific resource for MG assignments
$resourceCheck.Kind = "ManagementGroup" # Indicates this is a management group level assignment
$resourceCheck.Location = "" # Management groups don't have locations
$resourceCheck.ResourceGroupName = "" # No resource group context
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = ""
$resourceCheck.SubscriptionId = "" # No subscription context at MG level
$resourceCheck.SubscriptionName = ""
# Management groups don't have tags in the same way resources do
$resourceCheck.Tag_Team = ""
$resourceCheck.Tag_Product = ""
$resourceCheck.Tag_Environment = ""
$resourceCheck.Tag_Data = ""
$resourceCheck.Tag_Delete = ""
$resourceCheck.Tag_Split = ""
# Populate RBAC assignment details
$resourceCheck.RBAC_RoleAssignmentId = $roleAssignment.RoleAssignmentId
$resourceCheck.RBAC_Scope = $roleAssignment.Scope
$resourceCheck.RBAC_DisplayName = $roleAssignment.DisplayName
@@ -75,39 +233,68 @@ foreach ($managementGroup in $managementGroups)
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
$subscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name | Where-Object State -eq "Active"
# Get all active subscriptions under this management group
$allSubscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name | Where-Object State -eq "Active"
# Filter subscriptions if specific subscription IDs were provided
if ($SubscriptionIds.Count -gt 0) {
$subscriptions = $allSubscriptions | Where-Object {
$subscriptionId = $_.Id.Split('/')[-1] # Extract subscription ID from resource path
$subscriptionId -in $SubscriptionIds
}
if ($subscriptions.Count -eq 0) {
Write-Host "No matching subscriptions found in management group '$($managementGroup.DisplayName)' for the specified subscription IDs."
continue
}
Write-Host "Processing $($subscriptions.Count) filtered subscription(s) in management group '$($managementGroup.DisplayName)'"
} else {
$subscriptions = $allSubscriptions
Write-Host "Processing all $($subscriptions.Count) active subscription(s) in management group '$($managementGroup.DisplayName)'"
}
# Process each subscription under the current management group
foreach ($subscription in $subscriptions)
{
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
# Extract subscription ID from the full resource path
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
Write-Host "Subscription [$($subscription.DisplayName) - $subscriptionId]"
# Set Azure PowerShell context to the current subscription
Set-AzContext -SubscriptionId $subscriptionId | Out-Null
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
# Initialize result array for this subscription's role assignments
[ResourceCheck[]]$Result = @()
# Get role assignments directly assigned to this subscription scope
try {
$roleAssignments = Get-AzRoleAssignment -Scope $scope | Where-Object Scope -eq $scope
# Process each subscription-level role assignment
foreach($roleAssignment in $roleAssignments) {
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ResourceId = ""
$resourceCheck.Kind = "Subscription"
$resourceCheck.Location = ""
$resourceCheck.ResourceGroupName = ""
# Set subscription context (no individual resource context at this level)
$resourceCheck.ResourceId = "" # No specific resource for subscription assignments
$resourceCheck.Kind = "Subscription" # Indicates this is a subscription level assignment
$resourceCheck.Location = "" # Subscriptions don't have specific locations
$resourceCheck.ResourceGroupName = "" # No resource group context at subscription level
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
# Extract subscription-level tags if available
$resourceCheck.Tag_Team = $subscription.Tags.team
$resourceCheck.Tag_Product = $subscription.Tags.product
$resourceCheck.Tag_Environment = $subscription.Tags.environment
$resourceCheck.Tag_Data = $subscription.Tags.data
$resourceCheck.Tag_Delete = $subscription.Tags.delete
$resourceCheck.Tag_Split = $subscription.Tags.split
# Populate RBAC assignment details
$resourceCheck.RBAC_RoleAssignmentId = $roleAssignment.RoleAssignmentId
$resourceCheck.RBAC_Scope = $roleAssignment.Scope
$resourceCheck.RBAC_DisplayName = $roleAssignment.DisplayName
@@ -117,34 +304,46 @@ foreach ($managementGroup in $managementGroups)
$Result += $resourceCheck
}
} catch {
# Silently handle any errors during subscription-level role assignment processing
}
# Export subscription-level role assignments to CSV
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
# Get all resource groups within the current subscription
$resourceGroups = Get-AzResourceGroup
# Process each resource group for RBAC assignments
foreach ($resourceGroup in $resourceGroups) {
# Initialize result array for this resource group's role assignments
[ResourceCheck[]]$Result = @()
# Get role assignments at resource group level and below
try {
$roleAssignments = Get-AzRoleAssignment -Scope $resourceGroup.ResourceId | Where-Object Scope -Like "$($resourceGroup.ResourceId)*"
# Process each resource group-level role assignment
foreach($roleAssignment in $roleAssignments) {
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
# Set resource group context
$resourceCheck.ResourceId = $resourceGroup.ResourceId
$resourceCheck.Kind = "ResourceGroup"
$resourceCheck.Kind = "ResourceGroup" # Indicates this is a resource group level assignment
$resourceCheck.Location = $resourceGroup.Location
$resourceCheck.ResourceGroupName = $resourceGroup.ResourceGroupName
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
# Extract resource group-level tags if available
$resourceCheck.Tag_Team = $resourceGroup.Tags.team
$resourceCheck.Tag_Product = $resourceGroup.Tags.product
$resourceCheck.Tag_Environment = $resourceGroup.Tags.environment
$resourceCheck.Tag_Data = $resourceGroup.Tags.data
$resourceCheck.Tag_Delete = $resourceGroup.Tags.delete
$resourceCheck.Tag_Split = $resourceGroup.Tags.split
# Populate RBAC assignment details
$resourceCheck.RBAC_RoleAssignmentId = $roleAssignment.RoleAssignmentId
$resourceCheck.RBAC_Scope = $roleAssignment.Scope
$resourceCheck.RBAC_DisplayName = $roleAssignment.DisplayName
@@ -154,38 +353,51 @@ foreach ($managementGroup in $managementGroups)
$Result += $resourceCheck
}
} catch {
# Silently handle any errors during resource group-level role assignment processing
}
# Export resource group-level role assignments to CSV
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
}
# Get all individual resources within the current subscription
$allResources = Get-AzResource
# Process each individual resource for RBAC assignments
foreach ($resource in $allResources) {
# Initialize result array for this resource's role assignments
[ResourceCheck[]]$Result = @()
# Get role assignments directly assigned to this specific resource
try {
$roleAssignments = Get-AzRoleAssignment -Scope $resource.ResourceId | Where-Object Scope -eq $resource.ResourceId
# Process each resource-level role assignment
foreach($roleAssignment in $roleAssignments) {
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
# Set individual resource context with full metadata
$resourceCheck.ResourceId = $resource.ResourceId
$resourceCheck.Id = $resource.Id
$resourceCheck.Kind = "Resource"
$resourceCheck.Id = $resource.Id # Additional resource identifier
$resourceCheck.Kind = "Resource" # Indicates this is an individual resource assignment
$resourceCheck.Location = $resource.Location
$resourceCheck.ResourceName = $resource.ResourceName
$resourceCheck.ResourceGroupName = $resource.ResourceGroupName
$resourceCheck.ResourceType = $resource.ResourceType
$resourceCheck.ResourceType = $resource.ResourceType # e.g., Microsoft.Storage/storageAccounts
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
# Extract resource-level tags if available
$resourceCheck.Tag_Team = $resource.Tags.team
$resourceCheck.Tag_Product = $resource.Tags.product
$resourceCheck.Tag_Environment = $resource.Tags.environment
$resourceCheck.Tag_Data = $resource.Tags.data
$resourceCheck.Tag_Delete = $resource.Tags.delete
$resourceCheck.Tag_Split = $resource.Tags.split
# Populate RBAC assignment details
$resourceCheck.RBAC_RoleAssignmentId = $roleAssignment.RoleAssignmentId
$resourceCheck.RBAC_Scope = $roleAssignment.Scope
$resourceCheck.RBAC_DisplayName = $roleAssignment.DisplayName
@@ -195,11 +407,20 @@ foreach ($managementGroup in $managementGroups)
$Result += $resourceCheck
}
} catch {
# Silently handle any errors during individual resource-level role assignment processing
}
# Export individual resource-level role assignments to CSV
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
}
}
}
# Final completion message
Write-Host "========================================================================================================================================================================"
if ($SubscriptionIds.Count -gt 0) {
Write-Host "RBAC analysis complete for $($SubscriptionIds.Count) specified subscription(s)."
} else {
Write-Host "RBAC analysis complete for all active subscriptions across all management groups."
}
Write-Host "Results exported to: $fileName"
Write-Host "Done."

View File

@@ -1,86 +1,215 @@
<#
.SYNOPSIS
Exports Azure Storage blob listings with optional container filtering and blob prefix matching to CSV format.
.DESCRIPTION
This script inventories Azure Storage blobs across containers within a specified storage account.
It supports multiple modes of operation including containers-only listing, full blob enumeration,
container exclusion filtering, and blob prefix filtering.
The script uses Azure Storage continuation tokens to handle large datasets efficiently and
exports results in batches to prevent memory issues with very large storage accounts.
Key Features:
- Container-only mode for quick container inventory
- Full blob enumeration with metadata
- Container exclusion filtering for system containers
- Blob prefix filtering for targeted inventory
- Large dataset handling with continuation tokens
- CSV export with timestamped filenames
.PARAMETER subscriptionId
[Required] The Azure subscription ID containing the storage account.
.PARAMETER resourcegroupName
[Required] The resource group name containing the storage account.
.PARAMETER storageAccountName
[Required] The name of the Azure Storage account to inventory.
.PARAMETER containersOnly
[Optional] Switch to export only container information without blob details.
Default: $false (full blob enumeration)
.PARAMETER excludedContainers
[Optional] Array of container names to exclude from the inventory.
Useful for filtering out system containers like '$logs', '$blobchangefeed', etc.
Default: Empty array (no exclusions)
.PARAMETER blobPrefix
[Optional] Blob name prefix filter to limit results to blobs starting with specified string.
Useful for targeting specific blob hierarchies or naming patterns.
Default: Empty string (no prefix filtering)
.OUTPUTS
CSV file named with timestamp pattern: "yyyy-MM-dd HHmm - [StorageAccountName] - bloblist.csv"
Contains columns for subscription, resource group, storage account, container, blob name, and last modified date.
.EXAMPLE
.\AzureStoragebloblist.ps1 -subscriptionId "a134faf1-7a89-4f2c-8389-06d00bd5e2a7" -resourcegroupName "Default-Storage-WestEurope" -storageAccountName "ecestore"
.\AzureStoragebloblist.ps1 -subscriptionId "a134faf1-7a89-4f2c-8389-06d00bd5e2a7" -resourcegroupName "Default-Storage-WestEurope" -storageAccountName "mailingstore"
.\AzureStoragebloblist.ps1 -subscriptionId "a134faf1-7a89-4f2c-8389-06d00bd5e2a7" -resourcegroupName "Default-Storage-WestEurope" -storageAccountName "projectcenter"
.\AzureStoragebloblist.ps1 -subscriptionId "a134faf1-7a89-4f2c-8389-06d00bd5e2a7" -resourcegroupName "effectorycore" -storageAccountName "corerightsaggregator" -ContainersOnly $true
.\AzureStoragebloblist.ps1 -subscriptionId "14c2354d-45a9-4e0f-98ff-be58cdbcddc7" -resourcegroupName "ec-automation-prod" -storageAccountName "stecautomationprod"
.\AzureStoragebloblist.ps1 -subscriptionId "7feeb150-9ee0-4aea-992a-5f3a89d933e6" -resourcegroupName "Results" -storageAccountName "myeffectoryresults"
.\AzureStoragebloblist.ps1 -subscriptionId "3190b0fd-4a66-4636-a204-5b9f18be78a6" -resourcegroupName "authorization" -storageAccountName "authorizationv2"
.\AzureStoragebloblist.ps1 -subscriptionId "a134faf1-7a89-4f2c-8389-06d00bd5e2a7" -resourcegroupName "effectorycore" -storageAccountName "coremailings"
.\AzureStoragebloblist.ps1 -subscriptionId "7feeb150-9ee0-4aea-992a-5f3a89d933e6" -resourcegroupName "results-activity" -storageAccountName "effactivity" -excludedContainers "`$logs","`$blobchangefeed", "activitybackup-applease", "activitybackup-largemessages", "activitybackup-leases", "activitycleanup-applease", "activitycleanup-leases", "activityprojectors-largemessages", "activityprojectors-leases", "activityquestionnaireavailableactivitygenerat-largemessages", "activityquestionnaireavailableactivitygenerat-leases", "attachments", "azure-webjobs-hosts", "azure-webjobs-secrets", "testhubname-applease", "testhubname-largemessages", "testhubname-leases" -blobPrefix "projects"
.\AzureStoragebloblist.ps1 -subscriptionId "86945e42-fa5a-4bbc-948f-3f5407f15d3e" -resourcegroupName "hierarchy" -storageAccountName "hierarchyeff"
.\AzureStoragebloblist.ps1 -subscriptionId "6e2b45e4-5e7b-4628-8827-ec44e23d2f6b" -resourcegroupName "ParticipantIntegration-Settings" -storageAccountName "integrationsettings"
.\AzureStoragebloblist.ps1 -subscriptionId "1ab2120c-947c-40e2-96c7-460d3e9659de" -resourcegroupName "sa-backups" -storageAccountName "archivecommvault"
.\AzureStoragebloblist.ps1 -subscriptionId "1ab2120c-947c-40e2-96c7-460d3e9659de" -resourcegroupName "sa-backups" -storageAccountName "backupcommvault"
.\AzureStoragebloblist.ps1 -subscriptionId "2a07dfa7-69ee-4608-b2d5-14124fcccc31" -resourcegroupName "questionnaire-server-weu" -storageAccountName "questionnairestoreweu"
.\AzureStoragebloblist.ps1 -subscriptionId "f9ab522b-4895-492d-b8a8-ca6e1f60c2a8" -resourcegroupName "participant-exchange" -storageAccountName "participantexchangev2" -excludedContainers "leases","insights-metrics-pt1m","insights-logs-partitionkeystatistics","insights-logs-dataplanerequests","insights-logs-controlplanerequests","event-attachments","command-handlers","aggregates-streaming","aggregates","`$logs","`$blobchangefeed"
.\AzureStoragebloblist.ps1 -subscriptionId "14c2354d-45a9-4e0f-98ff-be58cdbcddc7" -resourcegroupName "ec-measurement" -storageAccountName "stecmeasurementprod"
.\AzureStoragebloblist.ps1 -subscriptionId "2a07dfa7-69ee-4608-b2d5-14124fcccc31" -resourcegroupName "questionnaire-server-weu" -storageAccountName "questionnairedataweu"
.\AzureStoragebloblist.ps1 -subscriptionId "54794e27-b714-4346-81bc-05eae7ccb5a5" -resourcegroupName "question-management-api-weu" -storageAccountName "qmprojectionsweu" -excludedContainers "`$logs","`$blobchangefeed"
.\AzureStoragebloblist.ps1 -subscriptionId "7feeb150-9ee0-4aea-992a-5f3a89d933e6" -resourcegroupName "Results" -storageAccountName "myeffectoryresults" -excludedContainers "`$logs","`$blobchangefeed", "attachments", "azure-webjobs-hosts", "azure-webjobs-secrets", "azure-webjobs-dashboard", "azure-webjobs-hosts", "azure-webjobs-secrets", "hierarchydatesettings-leases", "projectcalculations-leases","resultscleanup-applease","resultscleanup-leases","resultsgroupscorecalculator-leases","testhubname-leases"
.\AzureStoragebloblist.ps1 -subscriptionId "7feeb150-9ee0-4aea-992a-5f3a89d933e6" -resourcegroupName "results-calculation" -storageAccountName "resultscalculation" -excludedContainers "`$logs","`$blobchangefeed", "attachments", "azure-webjobs-hosts", "azure-webjobs-secrets", "local-leases", "local-applease", "calculations", "calculations-test"
.\AzureStoragebloblist.ps1 -subscriptionId "54794e27-b714-4346-81bc-05eae7ccb5a5" -resourcegroupName "question-management-internaldata_api-weu" -storageAccountName "qmidapiweustore"
.\AzureStoragebloblist.ps1 -subscriptionId "54794e27-b714-4346-81bc-05eae7ccb5a5" -resourcegroupName "question-management-library-weu" -storageAccountName "qmlibraryweu"
.\AzureStoragebloblist.ps1 -subscriptionId "54794e27-b714-4346-81bc-05eae7ccb5a5" -resourcegroupName "question-management-media-api-weu" -storageAccountName "qmmediaweu"
.\AzureStoragebloblist.ps1 -subscriptionId "54794e27-b714-4346-81bc-05eae7ccb5a5" -resourcegroupName "question-management-api-weu" -storageAccountName "qmprojectionsweu"
.\AzureStoragebloblist.ps1 -subscriptionId "2a07dfa7-69ee-4608-b2d5-14124fcccc31" -resourcegroupName "questionnaire-data-collector-api-weu" -storageAccountName "quedatacolstoreweu"
.\AzureStoragebloblist.ps1 -subscriptionId "34c83aa8-6a8f-4c5e-9c27-0f1730d233bb" -resourcegroupName "start-a-survey" -storageAccountName "startasurvey" -excludedContainers "active-projects","attachments","attachments-logs","azure-webjobs-hosts","azure-webjobs-secrets","durablefunctionshub-largemessages","durablefunctionshub-leases","event-documents","locales","locales-theme-names","pdf-temp","portal","public","schemas"
.\AzureStoragebloblist.ps1 -subscriptionId "7feeb150-9ee0-4aea-992a-5f3a89d933e6" -resourcegroupName "rg-yourfeedback-001" -storageAccountName "yourfeedback" -excludedContainers "`$logs","`$blobchangefeed"
Exports blobs with "projects" prefix while excluding system containers.
.NOTES
Author: Cloud Engineering Team
Version: 1.0
Created: 2024
Prerequisites:
- Azure PowerShell module (Az.Storage) must be installed
- User must be authenticated (Connect-AzAccount)
- Requires Storage Blob Data Reader permissions or higher on the target storage account
Performance Considerations:
- Uses continuation tokens to handle large datasets efficiently
- Processes results in batches of 100,000 items to manage memory usage
- Export operations are performed incrementally to prevent timeouts
- Large storage accounts may take considerable time to process
Security Notes:
- Requires appropriate RBAC permissions on storage account
- Consider using managed identities for automated scenarios
- Output file contains blob metadata and should be handled securely
Common Use Cases:
- Storage account auditing and inventory
- Data migration planning and assessment
- Cleanup operations and lifecycle management
- Compliance reporting and data discovery
.LINK
https://docs.microsoft.com/en-us/azure/storage/blobs/
https://docs.microsoft.com/en-us/powershell/module/az.storage/
#>
#Requires -Modules Az.Storage
param (
[string] $subscriptionId = "",
[string] $resourcegroupName = "",
[string] $storageAccountName = "",
[Parameter(Mandatory = $true, HelpMessage = "Azure subscription ID containing the storage account")]
[ValidateNotNullOrEmpty()]
[string] $subscriptionId,
[Parameter(Mandatory = $true, HelpMessage = "Resource group name containing the storage account")]
[ValidateNotNullOrEmpty()]
[string] $resourcegroupName,
[Parameter(Mandatory = $true, HelpMessage = "Azure Storage account name to inventory")]
[ValidateNotNullOrEmpty()]
[string] $storageAccountName,
[Parameter(Mandatory = $false, HelpMessage = "Export only container information without blob details")]
[bool] $containersOnly = $false,
[Parameter(Mandatory = $false, HelpMessage = "Array of container names to exclude from inventory")]
[string[]] $excludedContainers = @(),
[Parameter(Mandatory = $false, HelpMessage = "Blob name prefix filter for targeted inventory")]
[string] $blobPrefix = ""
)
if (("" -eq $subscriptionId) -or ("" -eq $resourcegroupName) -or ("" -eq $storageAccountName)) {
throw "Parameter(s) missing."
}
else {
Write-Host "Processing subscription [$subscriptionId], resource group [$resourcegroupName], storage account [$storageAccountName]"
}
# Parameter validation and initialization
Write-Host "======================================================================================================================================================================"
Write-Host "Starting Azure Storage Blob inventory process"
Write-Host "======================================================================================================================================================================"
# .\AzureStoragebloblist.ps1 -subscriptionId "a134faf1-7a89-4f2c-8389-06d00bd5e2a7" -resourcegroupName "Default-Storage-WestEurope" -storageAccountName "ecestore"
# .\AzureStoragebloblist.ps1 -subscriptionId "a134faf1-7a89-4f2c-8389-06d00bd5e2a7" -resourcegroupName "Default-Storage-WestEurope" -storageAccountName "mailingstore"
# .\AzureStoragebloblist.ps1 -subscriptionId "a134faf1-7a89-4f2c-8389-06d00bd5e2a7" -resourcegroupName "Default-Storage-WestEurope" -storageAccountName "projectcenter"
# .\AzureStoragebloblist.ps1 -subscriptionId "a134faf1-7a89-4f2c-8389-06d00bd5e2a7" -resourcegroupName "effectorycore" -storageAccountName "corerightsaggregator" -ContainersOnly $true
# .\AzureStoragebloblist.ps1 -subscriptionId "14c2354d-45a9-4e0f-98ff-be58cdbcddc7" -resourcegroupName "ec-automation-prod" -storageAccountName "stecautomationprod"
# .\AzureStoragebloblist.ps1 -subscriptionId "7feeb150-9ee0-4aea-992a-5f3a89d933e6" -resourcegroupName "Results" -storageAccountName "myeffectoryresults"
# .\AzureStoragebloblist.ps1 -subscriptionId "3190b0fd-4a66-4636-a204-5b9f18be78a6" -resourcegroupName "authorization" -storageAccountName "authorizationv2"
# .\AzureStoragebloblist.ps1 -subscriptionId "a134faf1-7a89-4f2c-8389-06d00bd5e2a7" -resourcegroupName "effectorycore" -storageAccountName "coremailings"
# .\AzureStoragebloblist.ps1 -subscriptionId "7feeb150-9ee0-4aea-992a-5f3a89d933e6" -resourcegroupName "results-activity" -storageAccountName "effactivity" -excludedContainers "`$logs","`$blobchangefeed", "activitybackup-applease", "activitybackup-largemessages", "activitybackup-leases", "activitycleanup-applease", "activitycleanup-leases", "activityprojectors-largemessages", "activityprojectors-leases", "activityquestionnaireavailableactivitygenerat-largemessages", "activityquestionnaireavailableactivitygenerat-leases", "attachments", "azure-webjobs-hosts", "azure-webjobs-secrets", "testhubname-applease", "testhubname-largemessages", "testhubname-leases" -blobPrefix "projects"
# .\AzureStoragebloblist.ps1 -subscriptionId "86945e42-fa5a-4bbc-948f-3f5407f15d3e" -resourcegroupName "hierarchy" -storageAccountName "hierarchyeff"
# .\AzureStoragebloblist.ps1 -subscriptionId "6e2b45e4-5e7b-4628-8827-ec44e23d2f6b" -resourcegroupName "ParticipantIntegration-Settings" -storageAccountName "integrationsettings"
# .\AzureStoragebloblist.ps1 -subscriptionId "1ab2120c-947c-40e2-96c7-460d3e9659de" -resourcegroupName "sa-backups" -storageAccountName "archivecommvault"
# .\AzureStoragebloblist.ps1 -subscriptionId "1ab2120c-947c-40e2-96c7-460d3e9659de" -resourcegroupName "sa-backups" -storageAccountName "backupcommvault"
# .\AzureStoragebloblist.ps1 -subscriptionId "2a07dfa7-69ee-4608-b2d5-14124fcccc31" -resourcegroupName "questionnaire-server-weu" -storageAccountName "questionnairestoreweu"
#
# .\AzureStoragebloblist.ps1 -subscriptionId "f9ab522b-4895-492d-b8a8-ca6e1f60c2a8" -resourcegroupName "participant-exchange" -storageAccountName "participantexchangev2" -excludedContainers "leases","insights-metrics-pt1m","insights-logs-partitionkeystatistics","insights-logs-dataplanerequests","insights-logs-controlplanerequests","event-attachments","command-handlers","aggregates-streaming","aggregates","`$logs","`$blobchangefeed"
# .\AzureStoragebloblist.ps1 -subscriptionId "14c2354d-45a9-4e0f-98ff-be58cdbcddc7" -resourcegroupName "ec-measurement" -storageAccountName "stecmeasurementprod"
# .\AzureStoragebloblist.ps1 -subscriptionId "2a07dfa7-69ee-4608-b2d5-14124fcccc31" -resourcegroupName "questionnaire-server-weu" -storageAccountName "questionnairedataweu"
# .\AzureStoragebloblist.ps1 -subscriptionId "54794e27-b714-4346-81bc-05eae7ccb5a5" -resourcegroupName "question-management-api-weu" -storageAccountName "qmprojectionsweu" -excludedContainers "`$logs","`$blobchangefeed"
# .\AzureStoragebloblist.ps1 -subscriptionId "7feeb150-9ee0-4aea-992a-5f3a89d933e6" -resourcegroupName "Results" -storageAccountName "myeffectoryresults" -excludedContainers "`$logs","`$blobchangefeed", "attachments", "azure-webjobs-hosts", "azure-webjobs-secrets", "azure-webjobs-dashboard", "azure-webjobs-hosts", "azure-webjobs-secrets", "hierarchydatesettings-leases", "projectcalculations-leases","resultscleanup-applease","resultscleanup-leases","resultsgroupscorecalculator-leases","testhubname-leases"
# .\AzureStoragebloblist.ps1 -subscriptionId "7feeb150-9ee0-4aea-992a-5f3a89d933e6" -resourcegroupName "results-calculation" -storageAccountName "resultscalculation" -excludedContainers "`$logs","`$blobchangefeed", "attachments", "azure-webjobs-hosts", "azure-webjobs-secrets", "local-leases", "local-applease", "calculations", "calculations-test"
# .\AzureStoragebloblist.ps1 -subscriptionId "54794e27-b714-4346-81bc-05eae7ccb5a5" -resourcegroupName "question-management-internaldata_api-weu" -storageAccountName "qmidapiweustore"
# .\AzureStoragebloblist.ps1 -subscriptionId "54794e27-b714-4346-81bc-05eae7ccb5a5" -resourcegroupName "question-management-library-weu" -storageAccountName "qmlibraryweu"
# .\AzureStoragebloblist.ps1 -subscriptionId "54794e27-b714-4346-81bc-05eae7ccb5a5" -resourcegroupName "question-management-media-api-weu" -storageAccountName "qmmediaweu"
# .\AzureStoragebloblist.ps1 -subscriptionId "54794e27-b714-4346-81bc-05eae7ccb5a5" -resourcegroupName "question-management-api-weu" -storageAccountName "qmprojectionsweu"
# .\AzureStoragebloblist.ps1 -subscriptionId "2a07dfa7-69ee-4608-b2d5-14124fcccc31" -resourcegroupName "questionnaire-data-collector-api-weu" -storageAccountName "quedatacolstoreweu"
# .\AzureStoragebloblist.ps1 -subscriptionId "34c83aa8-6a8f-4c5e-9c27-0f1730d233bb" -resourcegroupName "start-a-survey" -storageAccountName "startasurvey" -excludedContainers "active-projects","attachments","attachments-logs","azure-webjobs-hosts","azure-webjobs-secrets","durablefunctionshub-largemessages","durablefunctionshub-leases","event-documents","locales","locales-theme-names","pdf-temp","portal","public","schemas"
# .\AzureStoragebloblist.ps1 -subscriptionId "7feeb150-9ee0-4aea-992a-5f3a89d933e6" -resourcegroupName "rg-yourfeedback-001" -storageAccountName "yourfeedback" -excludedContainers "`$logs","`$blobchangefeed"
Write-Host "Configuration:"
Write-Host " Subscription ID: $subscriptionId"
Write-Host " Resource Group: $resourcegroupName"
Write-Host " Storage Account: $storageAccountName"
Write-Host " Containers Only Mode: $containersOnly"
Write-Host " Excluded Containers: $($excludedContainers -join ', ')"
Write-Host " Blob Prefix Filter: $($blobPrefix -eq '' ? 'None' : $blobPrefix)"
Write-Host ""
# Class definition for structured blob inventory data
class BlobCheck {
[string] $SubscriptionId = ""
[string] $SubscriptionName = ""
[string] $ResourcegroupName = ""
[string] $StorageAccountName = ""
[string] $ContainerName = ""
[string] $BlobName = ""
[string] $LastModifiedDate = ""
[string] $SubscriptionId = "" # Azure subscription GUID
[string] $SubscriptionName = "" # Subscription display name
[string] $ResourcegroupName = "" # Resource group containing the storage account
[string] $StorageAccountName = "" # Storage account name
[string] $ContainerName = "" # Blob container name
[string] $BlobName = "" # Individual blob name (empty for container-only mode)
[string] $LastModifiedDate = "" # Last modified timestamp for container or blob
}
[int] $maxCount = 100000
$containerToken = $null
$blobToken = $null
# Configuration constants for large dataset handling
[int] $maxCount = 100000 # Maximum items per batch to manage memory usage
$containerToken = $null # Continuation token for container enumeration
$blobToken = $null # Continuation token for blob enumeration
# Generate timestamped output filename
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date - $storageAccountName - bloblist.csv"
Write-Host "Output file: $fileName"
$subscription = Set-AzContext -SubscriptionId $subscriptionId
$storageAccount = Get-AzStorageAccount -ResourceGroupName $resourcegroupName -Name $storageAccountName
try {
# Set Azure context and retrieve storage account
Write-Host "Setting Azure context and retrieving storage account..."
$subscription = Set-AzContext -SubscriptionId $subscriptionId
$storageAccount = Get-AzStorageAccount -ResourceGroupName $resourcegroupName -Name $storageAccountName
Write-Host "Successfully connected to storage account: $($storageAccount.StorageAccountName)"
Write-Host "Storage account location: $($storageAccount.Location)"
Write-Host "Storage account SKU: $($storageAccount.Sku.Name)"
Write-Host ""
}
catch {
Write-Error "Failed to connect to storage account: $($_.Exception.Message)"
exit 1
}
# Execute inventory based on mode selection
if ($containersOnly -eq $true) {
Write-Host "======================================================================================================================================================================"
Write-Host "CONTAINERS ONLY MODE: Inventorying container information without blob details"
Write-Host "======================================================================================================================================================================"
$totalContainers = 0
# Container-only enumeration loop with continuation token support
do {
Write-Host "Processing container batch (max $maxCount containers)..."
[BlobCheck[]]$Result = @()
# Retrieve containers with continuation token support
$containers = Get-AzStorageContainer -Context $storageAccount.Context -MaxCount $maxCount -ContinuationToken $containerToken
# Apply container exclusion filters if specified
if ($excludedContainers.Length -gt 0) {
$originalCount = $containers.Count
$containers = $containers | Where-Object { $excludedContainers -notcontains $_.Name }
$filteredCount = $originalCount - $containers.Count
if ($filteredCount -gt 0) {
Write-Host " Filtered out $filteredCount excluded container(s)"
}
}
# Process each container and create inventory records
foreach ($container in $containers) {
[BlobCheck] $blobCheck = [BlobCheck]::new()
$blobCheck.SubscriptionId = $subscription.Subscription.Id
@@ -88,44 +217,90 @@ if ($containersOnly -eq $true) {
$blobCheck.ResourcegroupName = $resourcegroupName
$blobCheck.StorageAccountName = $storageAccountName
$blobCheck.ContainerName = $container.Name
$blobCheck.BlobName = ""
$blobCheck.BlobName = "" # Empty for container-only mode
$blobCheck.LastModifiedDate = $container.LastModified
$Result += $blobCheck
}
# Export current batch to CSV if results exist
if ($Result.Length -gt 0) {
$Result | Export-Csv -Path $fileName -NoTypeInformation -Append
$totalContainers += $Result.Length
Write-Host " Exported $($Result.Length) container(s) to CSV (Total: $totalContainers)"
}
# Check for continuation and prepare next iteration
if ($containers.Length -le 0) {
Break;
Write-Host " No more containers to process"
Break
}
$containerToken = $containers[$containers.Count - 1].ContinuationToken;
$containerToken = $containers[$containers.Count - 1].ContinuationToken
}
while ($null -ne $containerToken)
Write-Host ""
Write-Host "Container inventory completed. Total containers processed: $totalContainers"
}
elseif ($containersOnly -eq $false) {
Write-Host "======================================================================================================================================================================"
Write-Host "FULL BLOB ENUMERATION MODE: Inventorying all blobs across all containers"
Write-Host "======================================================================================================================================================================"
$totalContainers = 0
$totalBlobs = 0
# Full blob enumeration with nested container/blob loops
do {
Write-Host "Processing container batch (max $maxCount containers)..."
# Retrieve containers with continuation token support
$containers = Get-AzStorageContainer -Context $storageAccount.Context -MaxCount $maxCount -ContinuationToken $containerToken
# Apply container exclusion filters if specified
if ($excludedContainers.Length -gt 0) {
$originalCount = $containers.Count
$containers = $containers | Where-Object { $excludedContainers -notcontains $_.Name }
$filteredCount = $originalCount - $containers.Count
if ($filteredCount -gt 0) {
Write-Host " Filtered out $filteredCount excluded container(s)"
}
}
# Process each container for blob enumeration
foreach ($container in $containers) {
Write-Host " Processing container: $($container.Name)"
$containerBlobCount = 0
# Reset blob continuation token for each container
$blobToken = $null
# Blob enumeration loop with continuation token support
do {
[BlobCheck[]]$Result = @()
if ("" -ne $blobPrefix) {
$blobList = Get-AzStorageBlob -Container $Container.Name -Context $storageAccount.Context -MaxCount $maxCount -ContinuationToken $blobToken -Prefix $blobPrefix
# Retrieve blobs with optional prefix filtering
try {
if ("" -ne $blobPrefix) {
Write-Host " Retrieving blobs with prefix '$blobPrefix' (max $maxCount blobs)..."
$blobList = Get-AzStorageBlob -Container $Container.Name -Context $storageAccount.Context -MaxCount $maxCount -ContinuationToken $blobToken -Prefix $blobPrefix
}
else {
Write-Host " Retrieving all blobs (max $maxCount blobs)..."
$blobList = Get-AzStorageBlob -Container $Container.Name -Context $storageAccount.Context -MaxCount $maxCount -ContinuationToken $blobToken
}
}
else {
$blobList = Get-AzStorageBlob -Container $Container.Name -Context $storageAccount.Context -MaxCount $maxCount -ContinuationToken $blobToken
catch {
Write-Warning " Failed to retrieve blobs from container '$($container.Name)': $($_.Exception.Message)"
break
}
# Exit loop if no blobs found
if ($blobList.Length -le 0) {
Break;
Write-Host " No more blobs in container"
Break
}
# Process each blob and create inventory records
foreach ($blob in $blobList) {
[BlobCheck] $blobCheck = [BlobCheck]::new()
$blobCheck.SubscriptionId = $subscription.Subscription.Id
@@ -137,16 +312,66 @@ elseif ($containersOnly -eq $false) {
$blobCheck.LastModifiedDate = $blob.LastModified
$Result += $blobCheck
}
# Export current batch to CSV
$Result | Export-Csv -Path $fileName -NoTypeInformation -Append
$blobToken = $blobList[$blobList.Count - 1].ContinuationToken;
$containerBlobCount += $Result.Length
$totalBlobs += $Result.Length
Write-Host " Exported $($Result.Length) blob(s) to CSV (Container total: $containerBlobCount, Overall total: $totalBlobs)"
# Prepare continuation token for next iteration
$blobToken = $blobList[$blobList.Count - 1].ContinuationToken
}
while ($null -ne $blobToken)
Write-Host " Container '$($container.Name)' completed. Total blobs: $containerBlobCount"
$totalContainers++
}
# Check for continuation and prepare next container batch
if ($containers.Length -le 0) {
Break;
Write-Host "No more containers to process"
Break
}
$containerToken = $containers[$containers.Count - 1].ContinuationToken;
$containerToken = $containers[$containers.Count - 1].ContinuationToken
}
while ($null -ne $containerToken)
}
Write-Host ""
Write-Host "Full blob inventory completed."
Write-Host " Total containers processed: $totalContainers"
Write-Host " Total blobs exported: $totalBlobs"
}
# Script completion summary
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "Azure Storage blob inventory completed successfully."
Write-Host "======================================================================================================================================================================"
Write-Host "Results exported to: $fileName"
Write-Host ""
# Display final summary based on mode
if ($containersOnly) {
Write-Host "Summary (Containers Only Mode):"
Write-Host " Storage Account: $storageAccountName"
Write-Host " Containers inventoried (after exclusions)"
if ($excludedContainers.Length -gt 0) {
Write-Host " Excluded containers: $($excludedContainers.Length)"
}
} else {
Write-Host "Summary (Full Blob Enumeration Mode):"
Write-Host " Storage Account: $storageAccountName"
Write-Host " Total containers processed: $totalContainers"
Write-Host " Total blobs inventoried: $totalBlobs"
if ($excludedContainers.Length -gt 0) {
Write-Host " Excluded containers: $($excludedContainers.Length)"
}
if ($blobPrefix -ne "") {
Write-Host " Blob prefix filter applied: $blobPrefix"
}
}
Write-Host ""
Write-Host "For large storage accounts, consider using container exclusions or blob prefix filters"
Write-Host "to optimize processing time and focus on relevant data."
Write-Host "======================================================================================================================================================================"

View File

@@ -1,35 +1,236 @@
<#
.SYNOPSIS
Exports all entities from a specified Azure Storage Table to a CSV file.
.DESCRIPTION
This script connects to an Azure Storage Account and exports all entities (rows) from a specified
table to a CSV file. It's designed for data extraction, backup, and analysis purposes from Azure
Table Storage.
The script retrieves all entities from the specified table without filtering and exports them
with all their properties to a timestamped CSV file. This is useful for:
- Data backup and archival
- Data analysis and reporting
- Debugging and troubleshooting table contents
- Data migration between environments
The exported CSV includes all entity properties with their values, making it suitable for
further processing in Excel, PowerBI, or other data analysis tools.
.PARAMETER subscriptionId
The Azure subscription ID containing the storage account.
This parameter is mandatory and must be a valid GUID format.
Example: "86945e42-fa5a-4bbc-948f-3f5407f15d3e"
.PARAMETER resourcegroupName
The name of the resource group containing the storage account.
This parameter is mandatory and is case-sensitive.
Example: "hierarchy"
.PARAMETER storageAccountName
The name of the Azure Storage Account containing the table.
This parameter is mandatory and must be a valid storage account name (3-24 characters,
lowercase letters and numbers only).
Example: "hierarchyeff"
.PARAMETER tableName
The name of the table from which to export entities.
This parameter is mandatory and is case-sensitive. Table names must follow Azure
Table naming conventions (3-63 characters, alphanumeric and hyphens, no consecutive hyphens).
Example: "auditlog"
.EXAMPLE
.\AzureStorageTableListEntities.ps1 -subscriptionId "86945e42-fa5a-4bbc-948f-3f5407f15d3e" -resourcegroupName "hierarchy" -storageAccountName "hierarchyeff" -tableName "auditlog"
Exports all entities from the 'auditlog' table in the 'hierarchyeff' storage account to a CSV file.
.NOTES
Author: Cloud Engineering Team
Version: 1.0
Prerequisites:
- Azure PowerShell module (Az) must be installed
- AzTable PowerShell module must be available for installation
- User must be authenticated to Azure (Connect-AzAccount)
- User must have at least 'Storage Table Data Reader' permissions on the storage account
Required Permissions:
- Reader access to the subscription and resource group
- Storage Table Data Reader or Storage Account Contributor on the storage account
Output File:
- Format: "YYYY-MM-DD HHMM - {StorageAccountName} - tablecheck.csv"
- Location: Current directory
- Content: All table entities with their properties
Performance Considerations:
- Large tables may take significant time to export
- Consider the table size and available memory when running against large datasets
- Network bandwidth may impact export speed for tables with many entities
Security Notes:
- Exported CSV files may contain sensitive data - handle appropriately
- Ensure proper access controls on the output directory
- Consider encryption for sensitive table data exports
.LINK
https://docs.microsoft.com/en-us/azure/storage/tables/
https://docs.microsoft.com/en-us/powershell/module/az.storage/
#>
param (
[string] $subscriptionId = "",
[string] $resourcegroupName = "",
[string] $storageAccountName = "",
[string] $tableName = ""
[Parameter(Mandatory = $true, HelpMessage = "Azure subscription ID (GUID format)")]
[ValidateNotNullOrEmpty()]
[ValidateScript({
if ($_ -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') {
$true
} else {
throw "Subscription ID must be a valid GUID format"
}
})]
[string] $subscriptionId,
[Parameter(Mandatory = $true, HelpMessage = "Resource group name containing the storage account")]
[ValidateNotNullOrEmpty()]
[ValidateLength(1, 90)]
[string] $resourcegroupName,
[Parameter(Mandatory = $true, HelpMessage = "Storage account name (3-24 characters, lowercase alphanumeric)")]
[ValidateNotNullOrEmpty()]
[ValidateLength(3, 24)]
[ValidatePattern('^[a-z0-9]+$')]
[string] $storageAccountName,
[Parameter(Mandatory = $true, HelpMessage = "Table name to export entities from")]
[ValidateNotNullOrEmpty()]
[ValidateLength(3, 63)]
[ValidatePattern('^[A-Za-z][A-Za-z0-9]*$')]
[string] $tableName
)
if (("" -eq $subscriptionId) -or ("" -eq $resourcegroupName) -or ("" -eq $storageAccountName) -or ("" -eq $tableName)) {
throw "Parameter(s) missing."
}
else {
Import-Module AzTable
# Display configuration for user verification
Write-Host "======================================================================================================================================================================"
Write-Host "Azure Storage Table Entity Export Configuration"
Write-Host "======================================================================================================================================================================"
Write-Host "Subscription ID: $subscriptionId"
Write-Host "Resource Group: $resourcegroupName"
Write-Host "Storage Account: $storageAccountName"
Write-Host "Table Name: $tableName"
Write-Host "======================================================================================================================================================================"
Write-Host ""
# Import required module for Azure Table operations
Write-Host "Importing AzTable module for table operations..."
try {
Import-Module AzTable -ErrorAction Stop
Write-Host "✓ AzTable module imported successfully"
} catch {
Write-Error "Failed to import AzTable module. Please install it using: Install-Module -Name AzTable"
throw "Required module AzTable is not available"
}
# Generate timestamped filename for export
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date - $storageAccountName - tablecheck.csv"
$fileName = ".\$date - $storageAccountName - tablecheck.csv"
Write-Host "Export file: $fileName"
Write-Host ""
# .\AzureStorageTableListEntities.ps1 -subscriptionId "86945e42-fa5a-4bbc-948f-3f5407f15d3e" -resourcegroupName "hierarchy" -storageAccountName "hierarchyeff" -tableName "auditlog"
# Set Azure context to the specified subscription
Write-Host "Setting Azure context..."
try {
$subscription = Set-AzContext -SubscriptionId $subscriptionId -ErrorAction Stop
Write-Host "✓ Successfully connected to subscription: $($subscription.Subscription.Name)"
} catch {
Write-Error "Failed to set Azure context to subscription: $subscriptionId"
Write-Error "Please ensure you are authenticated (Connect-AzAccount) and have access to this subscription"
throw $_
}
# Get the storage account reference
Write-Host "Retrieving storage account information..."
try {
$storageAccount = Get-AzStorageAccount -ResourceGroupName $resourcegroupName -Name $storageAccountName -ErrorAction Stop
Write-Host "✓ Successfully connected to storage account: $storageAccountName"
} catch {
Write-Error "Failed to retrieve storage account '$storageAccountName' from resource group '$resourcegroupName'"
Write-Error "Please verify the storage account name and resource group name are correct"
throw $_
}
$subscription = Set-AzContext -SubscriptionId $subscriptionId
$storageAccount = Get-AzStorageAccount -ResourceGroupName $resourcegroupName -Name $storageAccountName
# Get the specified table reference
Write-Host "Accessing table '$tableName'..."
try {
$tables = Get-AzStorageTable -Context $storageAccount.Context -Name $tableName -ErrorAction Stop
Write-Host "✓ Successfully accessed table: $tableName"
} catch {
Write-Error "Failed to access table '$tableName' in storage account '$storageAccountName'"
Write-Error "Please verify the table name is correct and you have appropriate permissions"
throw $_
}
$tables = Get-AzStorageTable -Context $storageAccount.Context -Name $tableName
# Initialize counters for statistics
$totalEntities = 0
$tablesProcessed = 0
# Process each table (typically just one with specific name)
foreach ($table in $tables) {
$rows = Get-AzTableRow -table $table.CloudTable
if (($null -ne $rows) -and ($rows.Length -gt 0)) {
Write-Host "Processing subscription [$subscriptionId], resource group [$resourcegroupName], storage account [$storageAccountName] -> table [$($table.Name)]"
$rows | Export-Csv -Path $fileName -NoTypeInformation -Append
Write-Host ""
Write-Host "Processing table: $($table.Name)"
Write-Host "Retrieving all entities from the table..."
try {
# Get all rows/entities from the table
$rows = Get-AzTableRow -table $table.CloudTable -ErrorAction Stop
# Check if table contains any entities
if (($null -ne $rows) -and ($rows.Length -gt 0)) {
Write-Host "✓ Found $($rows.Length) entities in table '$($table.Name)'"
Write-Host "Exporting entities to CSV file..."
# Export entities to CSV file
$rows | Export-Csv -Path $fileName -NoTypeInformation -Append
# Update statistics
$totalEntities += $rows.Length
$tablesProcessed++
Write-Host "✓ Successfully exported $($rows.Length) entities to $fileName"
} else {
Write-Host "⚠ Table '$($table.Name)' contains no entities to export"
$tablesProcessed++
}
} catch {
Write-Error "Failed to retrieve entities from table '$($table.Name)'"
Write-Error $_.Exception.Message
throw $_
}
}
}
# Display completion summary
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "Azure Storage Table entity export completed successfully."
Write-Host "======================================================================================================================================================================"
Write-Host "Export Summary:"
Write-Host " Storage Account: $storageAccountName"
Write-Host " Table Name: $tableName"
Write-Host " Tables Processed: $tablesProcessed"
Write-Host " Total Entities Exported: $totalEntities"
Write-Host " Output File: $fileName"
Write-Host ""
if ($totalEntities -gt 0) {
Write-Host "✓ Export completed successfully. All table entities have been saved to the CSV file."
Write-Host "The CSV file can be opened in Excel, PowerBI, or processed with other data analysis tools."
} else {
Write-Host "⚠ No entities were found in the specified table to export."
}
Write-Host ""
Write-Host "Note: Large tables may contain sensitive data. Please handle the exported file appropriately"
Write-Host "and ensure proper access controls are in place."
Write-Host "======================================================================================================================================================================"

View File

@@ -1,78 +1,357 @@
#Connect-AzAccount
<#
.SYNOPSIS
Inventories and monitors Azure App Service certificates across all enabled subscriptions.
$fileName = ".\2020-12-23 azure_appservice_certificates (3).csv"
.DESCRIPTION
This script performs a comprehensive audit of all Azure App Service certificates across all
enabled subscriptions in your Azure tenant. It extracts certificate details including expiration
dates, thumbprints, subject names, and calculates the remaining days until expiration.
The script is designed for:
- Certificate lifecycle management and monitoring
- Proactive identification of expiring certificates
- Compliance auditing and reporting
- Security assessments of certificate inventory
- Planning certificate renewal activities
The script processes all enabled subscriptions automatically and exports results to a timestamped
CSV file, making it suitable for automated monitoring and reporting workflows.
Key features:
- Multi-subscription certificate discovery
- Expiration date calculation with days remaining
- Error handling for inaccessible or invalid certificates
- Detailed logging and progress reporting
- CSV export for further analysis and alerting
$subscriptions = Get-AzSubscription | Where-Object State -eq "Enabled"
.PARAMETER None
This script does not accept parameters and processes all enabled subscriptions automatically.
.EXAMPLE
.\Certificates.ps1
Runs the certificate inventory across all enabled subscriptions and exports results to a
timestamped CSV file in the current directory.
.EXAMPLE
# Schedule for automated monitoring
$scriptPath = "C:\Scripts\Certificates.ps1"
& $scriptPath
Executes the script from a scheduled task or automation workflow for regular certificate monitoring.
.EXAMPLE
# Run and immediately view results
.\Certificates.ps1
Get-Content ".\$(Get-Date -Format 'yyyy-MM-dd HHmm') azure_appservice_certificates.csv"
Runs the script and displays the generated CSV content for immediate review.
.NOTES
Author: Cloud Engineering Team
Version: 1.0
Prerequisites:
- Azure PowerShell module (Az) must be installed
- User must be authenticated to Azure (Connect-AzAccount)
- User must have at least 'Reader' permissions across target subscriptions
- Access to Microsoft.Web/certificates resources
Required Permissions:
- Reader access to subscriptions containing App Service certificates
- Web App Certificate Reader or App Service Certificate Reader permissions
- Resource Group Reader permissions for certificate resource groups
Output File:
- Format: "YYYY-MM-DD HHMM azure_appservice_certificates.csv"
- Location: Current directory
- Content: Certificate inventory with expiration analysis
Certificate Status Analysis:
- TotalDays > 30: Certificate is healthy
- TotalDays 7-30: Certificate expires soon (warning)
- TotalDays < 7: Certificate expires very soon (critical)
- TotalDays < 0: Certificate has already expired (urgent action required)
Performance Considerations:
- Processing time depends on the number of subscriptions and certificates
- Large tenants with many certificates may require extended execution time
- Network latency affects certificate detail retrieval
Security and Compliance:
- Certificate thumbprints and subject names are included in output
- Ensure proper access controls on generated CSV files
- Consider encryption for sensitive certificate inventory data
- Regular execution recommended for proactive certificate management
Common Use Cases:
- Monthly certificate expiration reports
- Pre-renewal planning and notifications
- Compliance audits requiring certificate inventory
- Security assessments of certificate lifecycle management
.LINK
https://docs.microsoft.com/en-us/azure/app-service/configure-ssl-certificate
https://docs.microsoft.com/en-us/powershell/module/az.websites/
#>
# Ensure user is authenticated to Azure
# Uncomment the following line if authentication is needed:
# Connect-AzAccount
# Display script header and configuration
Write-Host "======================================================================================================================================================================"
Write-Host "Azure App Service Certificate Inventory and Monitoring"
Write-Host "======================================================================================================================================================================"
Write-Host "Starting certificate discovery across all enabled subscriptions..."
Write-Host "Script execution started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host ""
# Generate timestamped filename for export
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_appservice_certificates.csv"
Write-Host "Export file: $fileName"
Write-Host ""
# Get all enabled subscriptions for processing
Write-Host "Retrieving enabled subscriptions..."
try {
$subscriptions = Get-AzSubscription | Where-Object State -eq "Enabled"
Write-Host "✓ Found $($subscriptions.Count) enabled subscription(s) to process:"
foreach ($sub in $subscriptions) {
Write-Host " - $($sub.Name) ($($sub.Id))"
}
} catch {
Write-Error "Failed to retrieve Azure subscriptions. Please ensure you are authenticated (Connect-AzAccount)"
throw $_
}
Write-Host ""
# Define certificate information class for structured data collection
class CertificateCheck {
# Azure subscription identifier containing the certificate
[string] $SubscriptionId = ""
# Full Azure resource ID of the certificate
[string] $CertificateId = ""
# Resource group name where the certificate is deployed
[string] $ResourceGroupName = ""
# Certificate subject name (Common Name and additional fields)
[string] $SubjectName = ""
# Certificate thumbprint (SHA-1 hash identifier)
[string] $ThumbPrint = ""
# Certificate expiration date and time
[DateTime] $ExpirationDate
# Number of days remaining until expiration (negative if expired)
[double] $TotalDays
# Certificate health status (Expired, Critical, Warning, Healthy, Error)
[string] $Health = ""
# Error messages or status comments for problematic certificates
[string] $Comment = ""
}
# Initialize result collection and processing variables
[CertificateCheck[]]$Result = @()
$StartDate = (Get-Date)
$totalCertificates = 0
$processedSubscriptions = 0
$certificatesWithIssues = 0
$StartDate=(GET-DATE)
[CertificateCheck[]]$Result = @()
foreach ($subscription in $subscriptions)
{
Set-AzContext -SubscriptionId $subscription.Id
Write-Host "======================================================================================================================================================================"
Write-Host "Processing Certificates by Subscription"
Write-Host "======================================================================================================================================================================"
$certs = Get-AzResource -ResourceType Microsoft.Web/certificates -ExpandProperties -ApiVersion 2018-02-01 | Select * -Expand Properties
foreach ($cert in $certs)
{
$id = $cert.Id
# Process each enabled subscription
foreach ($subscription in $subscriptions) {
Write-Host ""
Write-Host "Processing subscription: $($subscription.Name) ($($subscription.Id))"
try {
# Set Azure context to current subscription
Set-AzContext -SubscriptionId $subscription.Id -ErrorAction Stop | Out-Null
Write-Host "✓ Successfully connected to subscription"
[CertificateCheck] $certificateCheck = [CertificateCheck]::new()
$certificateCheck.SubscriptionId = $subscription.Id
$certificateCheck.CertificateId = $id
$certificateCheck.ThumbPrint = $cert.Properties.thumbprint
$certificateCheck.ResourceGroupName = $cert.ResourceGroupName
try
{
$thumbprint = $certificateCheck.ThumbPrint
$certificate = Get-AzWebAppCertificate -ResourceGroupName $certificateCheck.ResourceGroupName -Thumbprint $thumbprint -debug -verbose
if ($null -eq $certificate)
{
$certificateCheck.Comment = "Could not find certificate"
}
else
{
try
{
$subjectname = $certificate.SubjectName
$certificateCheck.SubjectName = $subjectname
Write-Host "Subject name: $subjectname"
$EndDate=[datetime]$certificate.ExpirationDate
$certificateCheck.ExpirationDate = $EndDate
$span = NEW-TIMESPAN Start $StartDate End $EndDate
$certificateCheck.TotalDays = $span.TotalDays
}
catch {
$certificateCheck.Comment = "Could not find expiry for certificate"
# Retrieve all App Service certificates in the subscription
Write-Host "Discovering App Service certificates..."
$certs = Get-AzResource -ResourceType Microsoft.Web/certificates -ApiVersion "2018-02-01" -ExpandProperties | Select-Object * -ExpandProperty Properties
if ($certs) {
Write-Host "✓ Found $($certs.Count) certificate(s) in subscription"
$subscriptionCertCount = 0
# Process each certificate found
foreach ($cert in $certs) {
$id = $cert.Id
Write-Host " Processing certificate: $($cert.Name)"
# Create new certificate check instance
[CertificateCheck] $certificateCheck = [CertificateCheck]::new()
# Populate basic certificate information
$certificateCheck.SubscriptionId = $subscription.Id
$certificateCheck.CertificateId = $id
$certificateCheck.ThumbPrint = $cert.Properties.thumbprint
$certificateCheck.ResourceGroupName = $cert.ResourceGroupName
try {
$thumbprint = $certificateCheck.ThumbPrint
# Retrieve detailed certificate information
$certificate = Get-AzWebAppCertificate -ResourceGroupName $certificateCheck.ResourceGroupName -Thumbprint $thumbprint -ErrorAction Stop
if ($null -eq $certificate) {
$certificateCheck.Health = "Error"
$certificateCheck.Comment = "Could not find certificate details"
$certificatesWithIssues++
Write-Host " ⚠ Warning: Certificate details not accessible"
} else {
try {
# Extract certificate subject name and expiration details
$subjectname = $certificate.SubjectName
$certificateCheck.SubjectName = $subjectname
Write-Host " ✓ Subject: $subjectname"
# Calculate expiration and days remaining
$EndDate = [datetime]$certificate.ExpirationDate
$certificateCheck.ExpirationDate = $EndDate
$span = New-TimeSpan -Start $StartDate -End $EndDate
$certificateCheck.TotalDays = [Math]::Round($span.TotalDays, 1)
# Determine and assign health status based on expiration
if ($certificateCheck.TotalDays -lt 0) {
$certificateCheck.Health = "Expired"
Write-Host " 🔴 EXPIRED: $([Math]::Abs($certificateCheck.TotalDays)) days ago" -ForegroundColor Red
$certificatesWithIssues++
} elseif ($certificateCheck.TotalDays -lt 7) {
$certificateCheck.Health = "Critical"
Write-Host " 🟠 CRITICAL: Expires in $($certificateCheck.TotalDays) days" -ForegroundColor Yellow
$certificatesWithIssues++
} elseif ($certificateCheck.TotalDays -lt 30) {
$certificateCheck.Health = "Warning"
Write-Host " 🟡 WARNING: Expires in $($certificateCheck.TotalDays) days" -ForegroundColor Yellow
} else {
$certificateCheck.Health = "Healthy"
Write-Host " ✓ Healthy: Expires in $($certificateCheck.TotalDays) days"
}
} catch {
$certificateCheck.Health = "Error"
$certificateCheck.Comment = "Could not determine expiration date"
$certificatesWithIssues++
Write-Host " ⚠ Warning: Could not determine expiration date"
}
}
} catch {
$certificateCheck.Health = "Error"
$certificateCheck.Comment = "Could not load certificate details"
$certificatesWithIssues++
Write-Host " ❌ Error: Could not load certificate details"
}
# Add certificate to results collection
$Result += $certificateCheck
$totalCertificates++
$subscriptionCertCount++
}
Write-Host " ✓ Processed $subscriptionCertCount certificate(s) in subscription"
} else {
Write-Host " No App Service certificates found in this subscription"
}
catch
{
$certificateCheck.Comment = "Could not load certificate"
}
$Result += $certificateCheck
$processedSubscriptions++
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Please verify permissions and subscription access"
}
}
$Result | Export-Csv -Path $fileName -NoTypeInformation -Force
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "Exporting Results and Analysis"
Write-Host "======================================================================================================================================================================"
$Result | ft
# Export results to CSV file
Write-Host "Exporting certificate inventory to CSV file..."
try {
$Result | Export-Csv -Path $fileName -NoTypeInformation -Force
Write-Host "✓ Successfully exported $($Result.Count) certificate records to: $fileName"
} catch {
Write-Error "Failed to export results to CSV file: $($_.Exception.Message)"
throw $_
}
# Display results summary table
Write-Host ""
Write-Host "Certificate Inventory Summary:"
Write-Host "======================================================================================================================================================================"
$Result | Format-Table -AutoSize
# Generate detailed analysis and statistics
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "Certificate Analysis Summary"
Write-Host "======================================================================================================================================================================"
Write-Host "Execution completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host ""
Write-Host "Processing Statistics:"
Write-Host " Subscriptions processed: $processedSubscriptions"
Write-Host " Total certificates discovered: $totalCertificates"
Write-Host " Certificates with issues: $certificatesWithIssues"
Write-Host ""
# Analyze certificate expiration status using Health property
if ($Result.Count -gt 0) {
$expiredCerts = $Result | Where-Object { $_.Health -eq "Expired" }
$criticalCerts = $Result | Where-Object { $_.Health -eq "Critical" }
$warnCerts = $Result | Where-Object { $_.Health -eq "Warning" }
$healthyCerts = $Result | Where-Object { $_.Health -eq "Healthy" }
$errorCerts = $Result | Where-Object { $_.Health -eq "Error" }
Write-Host "Certificate Status Analysis:"
Write-Host " 🔴 Expired certificates: $($expiredCerts.Count)"
Write-Host " 🟠 Critical (< 7 days): $($criticalCerts.Count)"
Write-Host " 🟡 Warning (7-30 days): $($warnCerts.Count)"
Write-Host " ✓ Healthy (> 30 days): $($healthyCerts.Count)"
Write-Host " ❌ Error/Inaccessible: $($errorCerts.Count)"
Write-Host ""
# Display urgent action items
if ($expiredCerts.Count -gt 0 -or $criticalCerts.Count -gt 0) {
Write-Host "🚨 URGENT ACTION REQUIRED:"
if ($expiredCerts.Count -gt 0) {
Write-Host " - $($expiredCerts.Count) certificate(s) have already expired"
}
if ($criticalCerts.Count -gt 0) {
Write-Host " - $($criticalCerts.Count) certificate(s) expire within 7 days"
}
Write-Host " Review the CSV file for detailed certificate information"
}
if ($warnCerts.Count -gt 0) {
Write-Host "⚠ RENEWAL PLANNING NEEDED:"
Write-Host " - $($warnCerts.Count) certificate(s) expire within 30 days"
}
} else {
Write-Host " No certificates found across all processed subscriptions"
}
Write-Host ""
Write-Host "Output File Information:"
Write-Host " File Path: $fileName"
Write-Host " File Size: $([Math]::Round((Get-Item $fileName).Length / 1KB, 2)) KB"
Write-Host ""
Write-Host "Recommendations:"
Write-Host " - Schedule regular execution for proactive certificate monitoring"
Write-Host " - Set up alerts for certificates expiring within 30 days"
Write-Host " - Implement automated renewal processes where possible"
Write-Host " - Review and resolve any certificates with error status"
Write-Host "======================================================================================================================================================================"

View File

@@ -1,77 +1,461 @@
# .\FrontDoorRoutes.ps1 -SubscriptionId "4820b5d8-cc1d-49bd-93e5-0c7a656371b7" -ResourceGroupName "my-effectory-global" -FrontDoorName "my-effectory-frontDoor"
<#
.SYNOPSIS
Exports Azure Front Door route configuration and origin mappings to a CSV file.
.DESCRIPTION
This script retrieves and documents the complete routing configuration for an Azure Front Door
(Standard/Premium) CDN profile. It extracts detailed information about endpoints, routes,
route patterns, origin groups, and individual origins, providing a comprehensive view of
traffic routing and backend configurations.
The script is designed for:
- Front Door configuration documentation and auditing
- Traffic routing analysis and optimization
- Origin backend inventory and health monitoring
- Troubleshooting routing issues and misconfigurations
- Migration planning and configuration validation
- Compliance documentation for CDN configurations
Key features:
- Complete route topology mapping from endpoints to backends
- Pattern matching rules documentation
- Origin health and enabled state tracking
- Route enablement status monitoring
- Structured CSV export for analysis and reporting
The exported data includes full URL construction for both front-end endpoints and
backend origins, making it easy to understand the complete request flow through
the Front Door configuration.
.PARAMETER SubscriptionId
The Azure subscription ID containing the Front Door profile.
This parameter is optional - if not provided, the script will use the current
subscription context. Must be a valid GUID format.
Example: "4820b5d8-cc1d-49bd-93e5-0c7a656371b7"
.PARAMETER ResourceGroupName
The name of the resource group containing the Front Door profile.
This parameter is mandatory and is case-sensitive.
Example: "my-effectory-global"
.PARAMETER FrontDoorName
The name of the Azure Front Door (Standard/Premium) profile to analyze.
This parameter is mandatory and is case-sensitive. Must be a valid Front Door
profile name (not the legacy Front Door Classic).
Example: "my-effectory-frontDoor"
.EXAMPLE
.\FrontDoorRoutes.ps1 -SubscriptionId "4820b5d8-cc1d-49bd-93e5-0c7a656371b7" -ResourceGroupName "my-effectory-global" -FrontDoorName "my-effectory-frontDoor"
Exports all route configurations for the specified Front Door profile with explicit subscription targeting.
.EXAMPLE
.\FrontDoorRoutes.ps1 -ResourceGroupName "production-rg" -FrontDoorName "prod-frontdoor"
Exports route configurations using the current subscription context.
.EXAMPLE
# Analyze multiple Front Door profiles
$frontDoors = @("frontdoor1", "frontdoor2", "frontdoor3")
foreach ($fd in $frontDoors) {
.\FrontDoorRoutes.ps1 -ResourceGroupName "global-rg" -FrontDoorName $fd
}
Batch processes multiple Front Door profiles for comprehensive documentation.
.EXAMPLE
# Export and immediately analyze results
.\FrontDoorRoutes.ps1 -ResourceGroupName "my-rg" -FrontDoorName "my-frontdoor"
$results = Import-Csv ".\$(Get-Date -Format 'yyyy-MM-dd HHmm') Front Door Routes (my-frontdoor).csv"
$results | Where-Object RouteEnabled -eq "Disabled" | Format-Table
Exports configuration and immediately identifies disabled routes for analysis.
.NOTES
Author: Cloud Engineering Team
Version: 1.0
Prerequisites:
- Azure PowerShell module (Az) must be installed
- Az.Cdn module specifically required for Front Door operations
- User must be authenticated to Azure (Connect-AzAccount)
- User must have at least 'Reader' permissions on the Front Door profile
Required Permissions:
- Reader access to the subscription and resource group
- CDN Profile Reader or Contributor permissions on the Front Door profile
- Access to Front Door endpoints, routes, and origin groups
Front Door Compatibility:
- Supports Azure Front Door Standard and Premium profiles
- Does NOT support legacy Azure Front Door Classic (different API)
- Requires Front Door profile to be in Standard or Premium tier
Output File:
- Format: "YYYY-MM-DD HHMM Front Door Routes ({FrontDoorName}).csv"
- Location: Current directory
- Content: Complete route topology with origins and patterns
CSV Structure:
- FrontDoorName: Front Door profile name
- EndpointName: Front Door endpoint name
- RouteName: Individual route configuration name
- RoutePatterns: URL patterns matched by this route (semicolon-separated)
- RouteUrl: Complete front-end URL for the endpoint
- OriginGroupName: Backend origin group name
- OriginName: Individual origin/backend name
- OriginUrl: Backend origin hostname/URL
- OriginEnabled: Origin availability status
- RouteEnabled: Route activation status
Performance Considerations:
- Processing time depends on the number of endpoints and routes
- Large Front Door configurations may require extended execution time
- Network latency affects configuration retrieval speed
Troubleshooting Notes:
- Ensure Front Door is Standard/Premium (not Classic)
- Verify resource group and Front Door names are correct
- Check that all required Az modules are installed and updated
- Confirm appropriate permissions on the Front Door resource
.LINK
https://docs.microsoft.com/en-us/azure/frontdoor/
https://docs.microsoft.com/en-us/powershell/module/az.cdn/
#>
param(
[Parameter(Mandatory=$false)]
[Parameter(Mandatory = $false, HelpMessage = "Azure subscription ID (optional - uses current context if not specified)")]
[ValidateScript({
if ([string]::IsNullOrEmpty($_) -or ($_ -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')) {
$true
} else {
throw "Subscription ID must be a valid GUID format"
}
})]
[string]$SubscriptionId,
[Parameter(Mandatory=$true)]
[Parameter(Mandatory = $true, HelpMessage = "Resource group name containing the Front Door profile")]
[ValidateNotNullOrEmpty()]
[ValidateLength(1, 90)]
[string]$ResourceGroupName,
[Parameter(Mandatory=$true)]
[Parameter(Mandatory = $true, HelpMessage = "Front Door profile name (Standard/Premium)")]
[ValidateNotNullOrEmpty()]
[ValidateLength(1, 260)]
[string]$FrontDoorName
)
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date Front Door Routes ($FrontDoorName).csv"
# Connect to Azure if not already connected
if (-not (Get-AzContext)) {
Connect-AzAccount
}
# Select subscription if provided
# Display script header and configuration
Write-Host "======================================================================================================================================================================"
Write-Host "Azure Front Door Route Configuration Export"
Write-Host "======================================================================================================================================================================"
Write-Host "Front Door Profile: $FrontDoorName"
Write-Host "Resource Group: $ResourceGroupName"
if ($SubscriptionId) {
Select-AzSubscription -SubscriptionId $SubscriptionId
Write-Host "Selected subscription: $SubscriptionId" -ForegroundColor Yellow
Write-Host "Target Subscription: $SubscriptionId"
} else {
Write-Host "Using current subscription context"
}
Write-Host "Script execution started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "======================================================================================================================================================================"
Write-Host ""
# Generate timestamped filename for export
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date Front Door Routes ($FrontDoorName).csv"
Write-Host "Export file: $fileName"
Write-Host ""
# Ensure Azure authentication
Write-Host "Verifying Azure authentication..."
if (-not (Get-AzContext)) {
Write-Host "No Azure context found. Initiating authentication..."
try {
Connect-AzAccount -ErrorAction Stop
Write-Host "✓ Successfully authenticated to Azure"
} catch {
Write-Error "Failed to authenticate to Azure. Please run Connect-AzAccount manually."
throw $_
}
} else {
Write-Host "✓ Azure authentication verified"
}
# Set target subscription if provided
if ($SubscriptionId) {
Write-Host "Setting subscription context..."
try {
$context = Select-AzSubscription -SubscriptionId $SubscriptionId -ErrorAction Stop
Write-Host "✓ Successfully connected to subscription: $($context.Subscription.Name)" -ForegroundColor Green
} catch {
Write-Error "Failed to select subscription: $SubscriptionId"
Write-Error "Please verify the subscription ID and ensure you have access"
throw $_
}
} else {
$currentContext = Get-AzContext
Write-Host "✓ Using current subscription: $($currentContext.Subscription.Name)" -ForegroundColor Green
}
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "Retrieving Front Door Configuration"
Write-Host "======================================================================================================================================================================"
try {
# Get Front Door profile
$frontDoor = Get-AzFrontDoorCdnProfile -ResourceGroupName $ResourceGroupName -Name $FrontDoorName
# Get Front Door profile and validate existence
Write-Host "Accessing Front Door profile '$FrontDoorName'..."
$frontDoor = Get-AzFrontDoorCdnProfile -ResourceGroupName $ResourceGroupName -Name $FrontDoorName -ErrorAction Stop
if (-not $frontDoor) {
Write-Error "Front Door '$FrontDoorName' not found in resource group '$ResourceGroupName'"
Write-Error "Front Door profile '$FrontDoorName' not found in resource group '$ResourceGroupName'"
Write-Error "Please verify the Front Door name and resource group are correct"
return
}
Write-Host "✓ Successfully accessed Front Door profile: $($frontDoor.Name)"
Write-Host " Profile Type: $($frontDoor.Sku.Name)"
Write-Host " Profile State: $($frontDoor.FrontDoorId)"
Write-Host ""
# Get all endpoints
$endpoints = Get-AzFrontDoorCdnEndpoint -ResourceGroupName $ResourceGroupName -ProfileName $FrontDoorName
# Get all endpoints for the Front Door profile
Write-Host "Discovering Front Door endpoints..."
$endpoints = Get-AzFrontDoorCdnEndpoint -ResourceGroupName $ResourceGroupName -ProfileName $FrontDoorName -ErrorAction Stop
$routeData = @()
foreach ($endpoint in $endpoints) {
# Get routes for each endpoint
$routes = Get-AzFrontDoorCdnRoute -ResourceGroupName $ResourceGroupName -ProfileName $FrontDoorName -EndpointName $endpoint.Name
foreach ($route in $routes) {
# Get origin group details
$originGroupId = $route.OriginGroupId
$originGroupName = ($originGroupId -split '/')[-1]
$origins = Get-AzFrontDoorCdnOrigin -ResourceGroupName $ResourceGroupName -ProfileName $FrontDoorName -OriginGroupName $originGroupName
foreach ($origin in $origins) {
$routeData += [PSCustomObject]@{
FrontDoorName = $FrontDoorName
EndpointName = $endpoint.Name
RouteName = $route.Name
RoutePatterns = ($route.PatternsToMatch -join '; ')
RouteUrl = "https://$($endpoint.HostName)"
OriginGroupName = $originGroupName
OriginName = $origin.Name
OriginUrl = $origin.HostName
OriginEnabled = $origin.EnabledState
RouteEnabled = $route.EnabledState
}
}
}
if (-not $endpoints -or $endpoints.Count -eq 0) {
Write-Warning "No endpoints found for Front Door profile '$FrontDoorName'"
Write-Host "This Front Door profile may not have any configured endpoints."
return
}
# Export to CSV
Write-Host "Exporting Front Door routes to: $fileName" -ForegroundColor Green
$routeData | Export-Csv -Path $fileName -NoTypeInformation -Force
Write-Host "✓ Found $($endpoints.Count) endpoint(s):"
foreach ($endpoint in $endpoints) {
Write-Host " - $($endpoint.Name) (https://$($endpoint.HostName))"
}
Write-Host ""
# Initialize collection for route data and processing counters
$routeData = @()
$totalRoutes = 0
$totalOrigins = 0
$enabledRoutes = 0
$disabledRoutes = 0
$enabledOrigins = 0
$disabledOrigins = 0
Write-Host "Processing endpoints and route configurations..."
Write-Host ""
# Process each endpoint to extract route and origin information
foreach ($endpoint in $endpoints) {
Write-Host "Processing endpoint: $($endpoint.Name)"
Write-Host " Endpoint URL: https://$($endpoint.HostName)"
try {
# Get all routes configured for this endpoint
$routes = Get-AzFrontDoorCdnRoute -ResourceGroupName $ResourceGroupName -ProfileName $FrontDoorName -EndpointName $endpoint.Name -ErrorAction Stop
if (-not $routes -or $routes.Count -eq 0) {
Write-Host " ⚠ No routes found for this endpoint" -ForegroundColor Yellow
continue
}
Write-Host " ✓ Found $($routes.Count) route(s) for endpoint"
# Process each route in the endpoint
foreach ($route in $routes) {
Write-Host " Processing route: $($route.Name)"
Write-Host " Patterns: $($route.PatternsToMatch -join ', ')"
Write-Host " Status: $($route.EnabledState)"
# Track route statistics
$totalRoutes++
if ($route.EnabledState -eq "Enabled") {
$enabledRoutes++
} else {
$disabledRoutes++
}
try {
# Extract origin group information from route
$originGroupId = $route.OriginGroupId
$originGroupName = ($originGroupId -split '/')[-1]
Write-Host " Origin Group: $originGroupName"
# Get all origins in the origin group
$origins = Get-AzFrontDoorCdnOrigin -ResourceGroupName $ResourceGroupName -ProfileName $FrontDoorName -OriginGroupName $originGroupName -ErrorAction Stop
if (-not $origins -or $origins.Count -eq 0) {
Write-Host " ⚠ No origins found in origin group '$originGroupName'" -ForegroundColor Yellow
# Create entry even if no origins found
$routeData += [PSCustomObject]@{
FrontDoorName = $FrontDoorName
EndpointName = $endpoint.Name
RouteName = $route.Name
RoutePatterns = ($route.PatternsToMatch -join '; ')
RouteUrl = "https://$($endpoint.HostName)"
OriginGroupName = $originGroupName
OriginName = "No origins found"
OriginUrl = "N/A"
OriginEnabled = "N/A"
RouteEnabled = $route.EnabledState
}
continue
}
Write-Host " ✓ Found $($origins.Count) origin(s) in group"
# Process each origin in the origin group
foreach ($origin in $origins) {
Write-Host " Origin: $($origin.Name) -> $($origin.HostName) ($($origin.EnabledState))"
# Track origin statistics
$totalOrigins++
if ($origin.EnabledState -eq "Enabled") {
$enabledOrigins++
} else {
$disabledOrigins++
}
# Create structured data entry for CSV export
$routeData += [PSCustomObject]@{
FrontDoorName = $FrontDoorName
EndpointName = $endpoint.Name
RouteName = $route.Name
RoutePatterns = ($route.PatternsToMatch -join '; ')
RouteUrl = "https://$($endpoint.HostName)"
OriginGroupName = $originGroupName
OriginName = $origin.Name
OriginUrl = $origin.HostName
OriginEnabled = $origin.EnabledState
RouteEnabled = $route.EnabledState
}
}
} catch {
Write-Host " ❌ Error processing origin group: $($_.Exception.Message)" -ForegroundColor Red
# Create error entry for troubleshooting
$routeData += [PSCustomObject]@{
FrontDoorName = $FrontDoorName
EndpointName = $endpoint.Name
RouteName = $route.Name
RoutePatterns = ($route.PatternsToMatch -join '; ')
RouteUrl = "https://$($endpoint.HostName)"
OriginGroupName = "Error retrieving"
OriginName = "Error"
OriginUrl = $_.Exception.Message
OriginEnabled = "Error"
RouteEnabled = $route.EnabledState
}
}
}
} catch {
Write-Host " ❌ Error processing routes for endpoint '$($endpoint.Name)': $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host ""
}
Write-Host "======================================================================================================================================================================"
Write-Host "Exporting Results and Analysis"
Write-Host "======================================================================================================================================================================"
# Export route configuration to CSV file
Write-Host "Exporting Front Door route configuration to CSV..."
try {
$routeData | Export-Csv -Path $fileName -NoTypeInformation -Force
Write-Host "✓ Successfully exported $($routeData.Count) route entries to: $fileName" -ForegroundColor Green
} catch {
Write-Error "Failed to export route data to CSV: $($_.Exception.Message)"
throw $_
}
# Display configuration summary table
Write-Host ""
Write-Host "Front Door Route Configuration Summary:"
Write-Host "======================================================================================================================================================================"
$routeData | Format-Table -Property FrontDoorName, EndpointName, RouteName, RoutePatterns, OriginGroupName, OriginName, RouteEnabled, OriginEnabled -AutoSize
# Generate detailed analysis and statistics
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "Configuration Analysis Summary"
Write-Host "======================================================================================================================================================================"
Write-Host "Export completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host ""
Write-Host "Front Door Statistics:"
Write-Host " Profile Name: $FrontDoorName"
Write-Host " Profile Type: $($frontDoor.Sku.Name)"
Write-Host " Total Endpoints: $($endpoints.Count)"
Write-Host " Total Routes: $totalRoutes"
Write-Host " Total Origins: $totalOrigins"
Write-Host ""
# Analyze route and origin health status
Write-Host "Route Status Analysis:"
Write-Host " ✓ Enabled Routes: $enabledRoutes"
Write-Host " ❌ Disabled Routes: $disabledRoutes"
Write-Host ""
Write-Host "Origin Status Analysis:"
Write-Host " ✓ Enabled Origins: $enabledOrigins"
Write-Host " ❌ Disabled Origins: $disabledOrigins"
Write-Host ""
# Identify potential issues
$issuesFound = @()
if ($disabledRoutes -gt 0) {
$issuesFound += "Some routes are disabled"
}
if ($disabledOrigins -gt 0) {
$issuesFound += "Some origins are disabled"
}
$errorEntries = $routeData | Where-Object { $_.OriginName -eq "Error" }
if ($errorEntries.Count -gt 0) {
$issuesFound += "Configuration retrieval errors detected"
}
if ($issuesFound.Count -gt 0) {
Write-Host "⚠ ATTENTION REQUIRED:"
foreach ($issue in $issuesFound) {
Write-Host " - $issue" -ForegroundColor Yellow
}
Write-Host " Review the CSV file for detailed information on affected routes/origins"
} else {
Write-Host "✓ All routes and origins are enabled and accessible"
}
Write-Host ""
Write-Host "Output File Information:"
Write-Host " File Path: $fileName"
Write-Host " File Size: $([Math]::Round((Get-Item $fileName).Length / 1KB, 2)) KB"
Write-Host " Records Exported: $($routeData.Count)"
Write-Host ""
Write-Host "Recommendations:"
Write-Host " - Review disabled routes and origins for intentional configuration"
Write-Host " - Validate routing patterns match expected traffic flow"
Write-Host " - Monitor origin health and performance regularly"
Write-Host " - Document configuration changes for audit trails"
Write-Host "======================================================================================================================================================================"
} catch {
Write-Error "Error retrieving Front Door routes: $($_.Exception.Message)"
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "❌ ERROR OCCURRED DURING PROCESSING"
Write-Host "======================================================================================================================================================================"
Write-Error "Error retrieving Front Door route configuration: $($_.Exception.Message)"
Write-Host ""
Write-Host "Troubleshooting Steps:"
Write-Host " 1. Verify Front Door profile name and resource group are correct"
Write-Host " 2. ensure the Front Door is Standard or Premium (not Classic)"
Write-Host " 3. Check that you have appropriate permissions on the Front Door resource"
Write-Host " 4. Confirm the Az.Cdn PowerShell module is installed and up to date"
Write-Host " 5. Verify network connectivity to Azure services"
Write-Host "======================================================================================================================================================================"
throw $_
}

View File

@@ -1,106 +1,444 @@
#Connect-AzAccount
<#
.SYNOPSIS
Exports Azure Key Vault access policies across all management groups and subscriptions for security auditing and compliance.
.DESCRIPTION
This script performs a comprehensive audit of Azure Key Vault access policies across an entire
Azure tenant, scanning all management groups, subscriptions, and resource groups. It identifies
Key Vaults using legacy access policy-based authentication (not RBAC) and exports detailed
permission information for security analysis and compliance reporting.
The script is designed for:
- Security auditing and access review processes
- Compliance reporting for Key Vault permissions
- Access policy governance and standardization
- Migration planning from access policies to RBAC
- Risk assessment of Key Vault permissions
- Regular security reviews and attestation processes
Key features:
- Multi-tenant hierarchical scanning (Management Groups → Subscriptions → Key Vaults)
- Access policy-based Key Vault filtering (excludes RBAC-only vaults)
- Comprehensive permission breakdown (Keys, Secrets, Certificates, Storage)
- Identity resolution with display names and application details
- Resource tagging extraction for governance analysis
- Structured CSV export for security team analysis
The script specifically targets Key Vaults using traditional access policies and skips
those configured for RBAC-only access, providing focused analysis on legacy permission models.
.PARAMETER None
This script does not accept parameters and processes all accessible management groups and subscriptions automatically.
.EXAMPLE
.\KeyVaultAccessPolicies.ps1
Runs the complete Key Vault access policy audit across all management groups and subscriptions.
.EXAMPLE
# Connect with specific account first
Connect-AzAccount -Tenant "your-tenant-id"
.\KeyVaultAccessPolicies.ps1
Authenticates with specific tenant context before running the comprehensive audit.
.EXAMPLE
# Schedule for automated security reviews
$scriptPath = "C:\SecurityScripts\KeyVaultAccessPolicies.ps1"
& $scriptPath
Executes the script from a scheduled security review process for regular compliance reporting.
.NOTES
Author: Cloud Engineering Team
Version: 1.0
Prerequisites:
- Azure PowerShell module (Az) must be installed
- User must be authenticated to Azure (Connect-AzAccount)
- User must have at least 'Reader' permissions across all target subscriptions
- Access to Management Group hierarchy and Key Vault resources
Required Permissions:
- Management Group Reader permissions at the tenant root or target management groups
- Reader access to all subscriptions containing Key Vaults
- Key Vault Reader or Key Vault Contributor permissions on Key Vault resources
- Microsoft Graph permissions may be needed for identity display name resolution
Security Context:
- This script reads access policy configurations but does not modify them
- Exported data contains sensitive permission information - handle appropriately
- Consider running from secure, controlled environments only
- Ensure proper access controls on output files
Output File:
- Format: "YYYY-MM-DD HHMM azure_key_vault_access_policies.csv"
- Location: Current directory
- Content: Comprehensive access policy inventory with identity and permission details
CSV Structure:
- Management Group information (ID, Name)
- Subscription details (ID, Name)
- Key Vault resource information (ID, Name, Location, Resource Group)
- Access Policy details (Object ID, Display Name, Application ID, Application Name)
- Permission breakdowns (Keys, Secrets, Certificates, Storage permissions)
- Resource tags (Team, Product, Environment, Data classification, Deployment, Creation date)
Performance Considerations:
- Processing time depends on the number of management groups, subscriptions, and Key Vaults
- Large Azure tenants may require extended execution time
- Network latency affects resource enumeration and permission retrieval
- Consider running during off-peak hours for large environments
Security and Compliance Notes:
- Key Vault access policies are being deprecated in favor of RBAC
- This script helps identify vaults still using legacy access policies
- Use results to plan migration from access policies to Azure RBAC
- Regular execution recommended for continuous security monitoring
- Exported data should be classified and protected appropriately
Filtering Logic:
- Only processes Key Vaults with EnableRbacAuthorization = FALSE
- Skips RBAC-only Key Vaults (EnableRbacAuthorization = TRUE)
- Focuses analysis on legacy access policy configurations
Common Use Cases:
- Quarterly security reviews and access attestation
- Pre-migration analysis for RBAC conversion projects
- Compliance audits requiring Key Vault permission documentation
- Risk assessments of privileged access to cryptographic resources
- Governance reviews of Key Vault access patterns
.LINK
https://docs.microsoft.com/en-us/azure/key-vault/general/security-features
https://docs.microsoft.com/en-us/azure/key-vault/general/rbac-guide
#>
# Ensure user is authenticated to Azure
# Uncomment the following line if authentication is needed:
# Connect-AzAccount
# Define comprehensive resource information class for Key Vault access policy analysis
class ResourceCheck {
[string] $ManagementGroupId = ""
[string] $ManagementGroupName = ""
[string] $SubscriptionId = ""
[string] $SubscriptionName = ""
[string] $ResourceGroup = ""
[string] $ResourceId = ""
[string] $Location = ""
[string] $ResourceName = ""
[string] $AccessPolicy_ObjectId = ""
[string] $AccessPolicy_DisplayName = ""
[string] $AccessPolicy_ApplicationId = ""
[string] $AccessPolicy_ApplicationDisplayName = ""
[string] $AccessPolicy_Keys = ""
[string] $AccessPolicy_Secrets = ""
[string] $AccessPolicy_Certificates = ""
[string] $AccessPolicy_Storage = ""
[string] $Tag_Team = ""
[string] $Tag_Product = ""
[string] $Tag_Environment = ""
[string] $Tag_Data = ""
[string] $Tag_Deployment = ""
[string] $Tag_CreatedOnDate = ""
# Management Group hierarchy information
[string] $ManagementGroupId = "" # Azure Management Group ID
[string] $ManagementGroupName = "" # Management Group display name
# Subscription context information
[string] $SubscriptionId = "" # Azure subscription ID
[string] $SubscriptionName = "" # Subscription display name
# Resource location and identification
[string] $ResourceGroup = "" # Resource group containing the Key Vault
[string] $ResourceId = "" # Full Azure resource ID of the Key Vault
[string] $Location = "" # Azure region where Key Vault is deployed
[string] $ResourceName = "" # Key Vault name
# Access policy identity information
[string] $AccessPolicy_ObjectId = "" # Azure AD Object ID with access
[string] $AccessPolicy_DisplayName = "" # Display name of the identity (user/service principal/group)
[string] $AccessPolicy_ApplicationId = "" # Application ID (for service principals)
[string] $AccessPolicy_ApplicationDisplayName = "" # Application display name
# Permission details by Key Vault resource type
[string] $AccessPolicy_Keys = "" # Permissions granted to cryptographic keys
[string] $AccessPolicy_Secrets = "" # Permissions granted to secrets
[string] $AccessPolicy_Certificates = "" # Permissions granted to certificates
[string] $AccessPolicy_Storage = "" # Permissions granted to storage account keys
# Resource governance tags for compliance and organization
[string] $Tag_Team = "" # Team responsible for the Key Vault
[string] $Tag_Product = "" # Product or service associated with the Key Vault
[string] $Tag_Environment = "" # Environment classification (dev/test/prod)
[string] $Tag_Data = "" # Data classification level
[string] $Tag_Deployment = "" # Deployment method or automation tag
[string] $Tag_CreatedOnDate = "" # Resource creation timestamp
}
Write-Host "======================================================================================================================================================================"
Write-Host "Creating key vault access policy resource overview."
Write-Host "Azure Key Vault Access Policy Security Audit"
Write-Host "======================================================================================================================================================================"
Write-Host "Starting comprehensive Key Vault access policy analysis across Azure tenant..."
Write-Host "Script execution started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host ""
Write-Host "Scope: All Management Groups → All Active Subscriptions → All Key Vaults (Access Policy-based only)"
Write-Host "Target: Key Vaults using legacy access policies (excludes RBAC-only vaults)"
Write-Host "======================================================================================================================================================================"
Write-Host ""
# Generate timestamped filename for export
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_key_vault_access_policies.csv"
$fileName = ".\$date azure_key_vault_access_policies.csv"
Write-Host "Export file: $fileName"
Write-Host ""
$managementGroups = Get-AzManagementGroup
# Initialize processing counters for statistics
$totalManagementGroups = 0
$totalSubscriptions = 0
$totalKeyVaults = 0
$totalAccessPolicies = 0
$rbacOnlyVaults = 0
$processedResourceGroups = 0
foreach ($managementGroup in $managementGroups)
{
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "Management group [$($managementGroup.Name)]"
Write-Host "Discovering Management Group hierarchy..."
try {
$managementGroups = Get-AzManagementGroup -ErrorAction Stop
$totalManagementGroups = $managementGroups.Count
Write-Host "✓ Found $totalManagementGroups Management Group(s) to process:"
foreach ($mg in $managementGroups) {
Write-Host " - $($mg.DisplayName) ($($mg.Name))"
}
} catch {
Write-Error "Failed to retrieve Management Groups. Please ensure you have appropriate permissions."
Write-Error "Required permissions: Management Group Reader at tenant root or target management groups"
throw $_
}
Write-Host ""
$subscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name | Where-Object State -eq "Active"
foreach ($subscription in $subscriptions)
{
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
Write-Host "Subscription [$($subscription.DisplayName) - $subscriptionId]"
Set-AzContext -SubscriptionId $subscriptionId | Out-Null
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
$allResourceGroups = Get-AzResourceGroup
[ResourceCheck[]]$Result = @()
foreach ($group in $allResourceGroups) {
$allVaults = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName
# Process each Management Group in the hierarchy
foreach ($managementGroup in $managementGroups) {
Write-Host "======================================================================================================================================================================"
Write-Host "Processing Management Group: $($managementGroup.DisplayName) ($($managementGroup.Name))"
Write-Host "======================================================================================================================================================================"
try {
# Get all active subscriptions within this management group
Write-Host "Discovering active subscriptions in management group..."
$subscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name -ErrorAction Stop | Where-Object State -eq "Active"
if (-not $subscriptions -or $subscriptions.Count -eq 0) {
Write-Host " No active subscriptions found in management group '$($managementGroup.DisplayName)'"
continue
}
Write-Host "✓ Found $($subscriptions.Count) active subscription(s):"
foreach ($sub in $subscriptions) {
Write-Host " - $($sub.DisplayName)"
}
Write-Host ""
# Process each active subscription
foreach ($subscription in $subscriptions) {
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
# Extract subscription ID from the full resource path
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
foreach ($vault in $allVaults) {
Write-Host "Processing Subscription: $($subscription.DisplayName) ($subscriptionId)"
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
try {
# Set Azure context to the current subscription
Set-AzContext -SubscriptionId $subscriptionId -ErrorAction Stop | Out-Null
Write-Host "✓ Successfully connected to subscription context"
$totalSubscriptions++
$vaultWithAllProps = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName -Name $vault.VaultName
if ($vaultWithAllProps.EnableRbacAuthorization -ne "TRUE") {
Write-Host $vaultWithAllProps.ResourceId
foreach($accessPolicy in $vaultWithAllProps.AccessPolicies) {
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.ResourceGroup = $vaultWithAllProps.ResourceGroupName
$resourceCheck.ResourceId = $vaultWithAllProps.ResourceId
$resourceCheck.Location = $vaultWithAllProps.Location
$resourceCheck.ResourceName = $vaultWithAllProps.VaultName
$resourceCheck.AccessPolicy_ObjectId = $accessPolicy.ObjectId
$resourceCheck.AccessPolicy_DisplayName = $accessPolicy.DisplayName
$resourceCheck.AccessPolicy_ApplicationId = $accessPolicy.ApplicationId
$resourceCheck.AccessPolicy_ApplicationDisplayName = $accessPolicy.ApplicationIdDisplayName
$resourceCheck.AccessPolicy_Keys = $accessPolicy.PermissionsToKeysStr
$resourceCheck.AccessPolicy_Secrets = $accessPolicy.PermissionsToSecretsStr
$resourceCheck.AccessPolicy_Certificates = $accessPolicy.PermissionsToCertificatesStr
$resourceCheck.AccessPolicy_Storage = $accessPolicy.PermissionsToStorageStr
$resourceCheck.Tag_Team = $vaultWithAllProps.Tags.team
$resourceCheck.Tag_Product = $vaultWithAllProps.Tags.product
$resourceCheck.Tag_Environment = $vaultWithAllProps.Tags.environment
$resourceCheck.Tag_Data = $vaultWithAllProps.Tags.data
$resourceCheck.Tag_CreatedOnDate = $vaultWithAllProps.Tags.CreatedOnDate
$resourceCheck.Tag_Deployment = $vaultWithAllProps.Tags.drp_deployment
$Result += $resourceCheck
# Get all resource groups in the current subscription
Write-Host "Discovering resource groups..."
$allResourceGroups = Get-AzResourceGroup -ErrorAction Stop
Write-Host "✓ Found $($allResourceGroups.Count) resource group(s) to scan"
# Initialize result collection for this subscription
[ResourceCheck[]]$Result = @()
$subscriptionKeyVaults = 0
$subscriptionAccessPolicies = 0
$subscriptionRbacVaults = 0
# Process each resource group to find Key Vaults
foreach ($group in $allResourceGroups) {
Write-Host " Scanning resource group: $($group.ResourceGroupName)"
$processedResourceGroups++
try {
# Get all Key Vaults in the current resource group
$allVaults = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName -ErrorAction Stop
if (-not $allVaults -or $allVaults.Count -eq 0) {
Write-Host " No Key Vaults found in resource group"
continue
}
Write-Host " ✓ Found $($allVaults.Count) Key Vault(s)"
# Process each Key Vault found
foreach ($vault in $allVaults) {
Write-Host " Processing Key Vault: $($vault.VaultName)"
try {
# Get detailed Key Vault properties including access policies
$vaultWithAllProps = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName -Name $vault.VaultName -ErrorAction Stop
$totalKeyVaults++
$subscriptionKeyVaults++
# Check if vault uses traditional access policies (not RBAC-only)
if ($vaultWithAllProps.EnableRbacAuthorization -ne $true) {
Write-Host " 📋 Access Policy-based vault: $($vaultWithAllProps.ResourceId)"
# Check if vault has any access policies configured
if (-not $vaultWithAllProps.AccessPolicies -or $vaultWithAllProps.AccessPolicies.Count -eq 0) {
Write-Host " ⚠ Warning: No access policies found on this vault" -ForegroundColor Yellow
continue
}
Write-Host " ✓ Found $($vaultWithAllProps.AccessPolicies.Count) access policy/policies"
# Process each access policy in the vault
foreach ($accessPolicy in $vaultWithAllProps.AccessPolicies) {
Write-Host " Identity: $($accessPolicy.DisplayName) ($($accessPolicy.ObjectId))"
# Create comprehensive resource check entry
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
# Populate management group and subscription information
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
# Populate Key Vault resource information
$resourceCheck.ResourceGroup = $vaultWithAllProps.ResourceGroupName
$resourceCheck.ResourceId = $vaultWithAllProps.ResourceId
$resourceCheck.Location = $vaultWithAllProps.Location
$resourceCheck.ResourceName = $vaultWithAllProps.VaultName
# Populate access policy identity information
$resourceCheck.AccessPolicy_ObjectId = $accessPolicy.ObjectId
$resourceCheck.AccessPolicy_DisplayName = $accessPolicy.DisplayName
$resourceCheck.AccessPolicy_ApplicationId = $accessPolicy.ApplicationId
$resourceCheck.AccessPolicy_ApplicationDisplayName = $accessPolicy.ApplicationIdDisplayName
# Populate permission details for each resource type
$resourceCheck.AccessPolicy_Keys = $accessPolicy.PermissionsToKeysStr
$resourceCheck.AccessPolicy_Secrets = $accessPolicy.PermissionsToSecretsStr
$resourceCheck.AccessPolicy_Certificates = $accessPolicy.PermissionsToCertificatesStr
$resourceCheck.AccessPolicy_Storage = $accessPolicy.PermissionsToStorageStr
# Extract resource tags for governance analysis
$resourceCheck.Tag_Team = $vaultWithAllProps.Tags.team
$resourceCheck.Tag_Product = $vaultWithAllProps.Tags.product
$resourceCheck.Tag_Environment = $vaultWithAllProps.Tags.environment
$resourceCheck.Tag_Data = $vaultWithAllProps.Tags.data
$resourceCheck.Tag_CreatedOnDate = $vaultWithAllProps.Tags.CreatedOnDate
$resourceCheck.Tag_Deployment = $vaultWithAllProps.Tags.drp_deployment
# Add to results collection
$Result += $resourceCheck
$totalAccessPolicies++
$subscriptionAccessPolicies++
}
} else {
Write-Host " 🔐 RBAC-only vault (skipped): $($vault.VaultName)" -ForegroundColor Blue
$rbacOnlyVaults++
$subscriptionRbacVaults++
}
} catch {
Write-Host " ❌ Error processing vault '$($vault.VaultName)': $($_.Exception.Message)" -ForegroundColor Red
}
}
} catch {
Write-Host " ❌ Error scanning resource group '$($group.ResourceGroupName)': $($_.Exception.Message)" -ForegroundColor Red
}
}
}
}
Write-Host ""
Write-Host "Subscription Summary:"
Write-Host " Key Vaults found: $subscriptionKeyVaults"
Write-Host " Access policies extracted: $subscriptionAccessPolicies"
Write-Host " RBAC-only vaults (skipped): $subscriptionRbacVaults"
Write-Host ""
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
# Export subscription results to CSV file
if ($Result.Count -gt 0) {
Write-Host "Exporting $($Result.Count) access policy entries to CSV..."
try {
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation -ErrorAction Stop
Write-Host "✓ Successfully exported subscription data"
} catch {
Write-Error "Failed to export data for subscription '$($subscription.DisplayName)': $($_.Exception.Message)"
}
} else {
Write-Host " No access policy data to export from this subscription"
}
} catch {
Write-Host "❌ Error processing subscription '$($subscription.DisplayName)': $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Please verify subscription access and permissions" -ForegroundColor Red
}
Write-Host ""
}
} catch {
Write-Host "❌ Error processing management group '$($managementGroup.DisplayName)': $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Please verify management group access and permissions" -ForegroundColor Red
}
}
Write-Host "======================================================================================================================================================================"
Write-Host "Done."
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "Key Vault Access Policy Audit Completed"
Write-Host "======================================================================================================================================================================"
Write-Host "Execution completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host ""
# Display comprehensive execution statistics
Write-Host "Processing Statistics:"
Write-Host " Management Groups processed: $totalManagementGroups"
Write-Host " Subscriptions processed: $totalSubscriptions"
Write-Host " Resource Groups scanned: $processedResourceGroups"
Write-Host " Total Key Vaults found: $totalKeyVaults"
Write-Host " Access policies extracted: $totalAccessPolicies"
Write-Host " RBAC-only vaults (skipped): $rbacOnlyVaults"
Write-Host ""
# Analyze and display key findings
if ($totalAccessPolicies -gt 0) {
Write-Host "✓ Successfully exported Key Vault access policy data to: $fileName"
Write-Host ""
Write-Host "Security Analysis Insights:"
if ($rbacOnlyVaults -gt 0) {
Write-Host "$rbacOnlyVaults Key Vault(s) using modern RBAC authentication"
}
$legacyVaults = $totalKeyVaults - $rbacOnlyVaults
if ($legacyVaults -gt 0) {
Write-Host "$legacyVaults Key Vault(s) still using legacy access policies" -ForegroundColor Yellow
Write-Host " Consider migrating to Azure RBAC for improved security and management"
}
Write-Host ""
Write-Host "Output File Information:"
if (Test-Path $fileName) {
Write-Host " File Path: $fileName"
Write-Host " File Size: $([Math]::Round((Get-Item $fileName).Length / 1KB, 2)) KB"
Write-Host " Records Exported: $totalAccessPolicies"
}
} else {
Write-Host " No Key Vault access policies found across all processed subscriptions"
Write-Host " This could indicate:"
Write-Host " - All Key Vaults are using RBAC-only authentication"
Write-Host " - No Key Vaults exist in the scanned subscriptions"
Write-Host " - Permission issues preventing access to Key Vault configurations"
}
Write-Host ""
Write-Host "Security Recommendations:"
Write-Host " - Review exported access policies for excessive permissions"
Write-Host " - Validate that all identities with access are still required"
Write-Host " - Consider implementing Azure RBAC for new Key Vaults"
Write-Host " - Plan migration from access policies to RBAC for existing vaults"
Write-Host " - Implement regular access reviews and permission audits"
Write-Host " - Ensure proper governance tags are applied to all Key Vaults"
Write-Host ""
Write-Host "Compliance Notes:"
Write-Host " - Exported data contains sensitive security information"
Write-Host " - Handle output file with appropriate data classification controls"
Write-Host " - Consider encryption for long-term storage of audit results"
Write-Host " - Schedule regular execution for continuous security monitoring"
Write-Host "======================================================================================================================================================================"

View File

@@ -1,101 +1,565 @@
#Connect-AzAccount
<#
.SYNOPSIS
Inventories secrets in Azure Key Vaults using legacy access policies across all management groups and subscriptions.
[string] $userObjectId = "c6025a2e-416c-42da-96ef-dd507382793a" #Should be interactive user (this one is Jurjen)
.DESCRIPTION
This script performs a comprehensive inventory of secrets stored in Azure Key Vaults that use
traditional access policy-based authentication (not RBAC). It temporarily grants list permissions
to a specified user account, enumerates all secret names, and then removes the temporary access.
⚠️ SECURITY WARNING: This script temporarily modifies Key Vault access policies during execution.
It grants temporary secret list permissions to the specified user account and removes them afterwards.
The script is designed for:
- Security auditing and secret inventory management
- Compliance reporting for secret governance
- Migration planning from access policies to RBAC
- Secret lifecycle management and cleanup identification
- Risk assessment of stored secrets across the organization
- Regular security reviews and secret attestation processes
Key features:
- Multi-tenant hierarchical scanning (Management Groups → Subscriptions → Key Vaults)
- Access policy-based Key Vault filtering (excludes RBAC-only vaults)
- Temporary access policy modification for secret enumeration
- Automatic cleanup of temporary permissions
- Comprehensive secret name inventory (does not retrieve secret values)
- Resource tagging extraction for governance analysis
- Structured CSV export for security team analysis
IMPORTANT SECURITY CONSIDERATIONS:
- Script only retrieves secret names, not secret values
- Temporary access policies are automatically cleaned up
- Requires privileged permissions to modify Key Vault access policies
- Should be run from secure, controlled environments only
- All activities are logged in Azure Activity Log for audit trails
.PARAMETER None
This script does not accept command-line parameters. The user Object ID must be configured
within the script before execution.
.EXAMPLE
# Update the userObjectId variable with your Object ID first
$userObjectId = "your-user-object-id-here"
.\KeyVaultNonRBACSecrets.ps1
Runs the complete Key Vault secret inventory after configuring the user Object ID.
.EXAMPLE
# Get your current user Object ID
$currentUser = Get-AzADUser -Mail (Get-AzContext).Account.Id
Write-Host "Your Object ID: $($currentUser.Id)"
# Then update the script and run
.\KeyVaultNonRBACSecrets.ps1
Retrieves your Object ID for configuration and runs the inventory.
.EXAMPLE
# Connect with specific account first for security
Connect-AzAccount -Tenant "your-tenant-id"
.\KeyVaultNonRBACSecrets.ps1
Authenticates with specific tenant context before running the sensitive inventory operation.
.NOTES
Author: Cloud Engineering Team
Version: 1.0
⚠️ SECURITY NOTICE: This script requires and uses highly privileged permissions to temporarily
modify Key Vault access policies. Use with extreme caution and only in authorized security
audit scenarios.
Prerequisites:
- Azure PowerShell module (Az) must be installed
- User must be authenticated to Azure (Connect-AzAccount)
- User must have Key Vault Access Policy management permissions across target vaults
- User Object ID must be configured in the script before execution
Required Permissions:
- Management Group Reader permissions at the tenant root or target management groups
- Key Vault Contributor or Key Vault Access Policy Administrator on all target Key Vaults
- Reader access to all subscriptions containing Key Vaults
- Sufficient privileges to modify and remove Key Vault access policies
Security Context:
- Script temporarily grants 'List' permissions on secrets to the specified user
- Access policies are automatically removed after secret enumeration
- Only secret names are collected, not secret values
- All access policy modifications are logged in Azure Activity Log
- Failed cleanup operations may leave temporary permissions (manual removal required)
Output File:
- Format: "YYYY-MM-DD HHMM azure_key_vault_secrets.csv"
- Location: Current directory
- Content: Secret inventory with Key Vault context and governance tags
CSV Structure:
- Management Group information (ID, Name)
- Subscription details (ID, Name)
- Key Vault resource information (ID, Name, Location, Resource Group)
- Secret name (Secret_Key field)
- Resource tags (Team, Product, Environment, Data classification, Deployment, Creation date)
Performance Considerations:
- Processing time depends on the number of Key Vaults and secrets
- Access policy modifications add latency to each Key Vault operation
- Large Azure tenants may require extended execution time
- Network latency affects both enumeration and policy modification operations
Risk Mitigation:
- Script implements automatic cleanup of temporary permissions
- Only grants minimal required permissions (List secrets only)
- Does not retrieve or expose secret values
- Focuses only on access policy-based vaults (skips RBAC vaults)
- All operations are auditable through Azure Activity Log
Failure Scenarios:
- If script fails during execution, temporary access policies may remain
- Manual cleanup may be required using Remove-AzKeyVaultAccessPolicy
- Network interruptions may prevent proper cleanup
- Insufficient permissions may cause partial processing
Compliance and Governance:
- Use only for authorized security audits and compliance activities
- Document all executions for audit trail purposes
- Ensure proper approval for access policy modification activities
- Handle exported secret names with appropriate data classification
- Consider encryption for long-term storage of inventory results
.LINK
https://docs.microsoft.com/en-us/azure/key-vault/general/security-features
https://docs.microsoft.com/en-us/azure/key-vault/general/rbac-guide
https://docs.microsoft.com/en-us/azure/key-vault/general/logging
#>
# Ensure user is authenticated to Azure
# Uncomment the following line if authentication is needed:
# Connect-AzAccount
# ⚠️ SECURITY CONFIGURATION: Dynamically retrieve current user's Object ID for temporary access policy grants
# This user will receive temporary 'List' permissions on secrets during processing
Write-Host "Retrieving current user's Object ID for temporary access policy grants..."
try {
$currentContext = Get-AzContext -ErrorAction Stop
if (-not $currentContext) {
throw "No Azure context found. Please run Connect-AzAccount first."
}
# Get the current user's Object ID from Azure AD
$currentUser = Get-AzADUser -Mail $currentContext.Account.Id -ErrorAction Stop
[string] $userObjectId = $currentUser.Id
if ([string]::IsNullOrEmpty($userObjectId)) {
throw "Could not retrieve Object ID for current user: $($currentContext.Account.Id)"
}
Write-Host "✓ Successfully retrieved Object ID for user: $($currentContext.Account.Id)"
Write-Host " Object ID: $userObjectId"
Write-Host " Display Name: $($currentUser.DisplayName)"
} catch {
Write-Error "Failed to retrieve current user's Object ID: $($_.Exception.Message)"
Write-Error "Please ensure you are authenticated with Connect-AzAccount and have a valid user account"
Write-Error "Note: Service Principal authentication is not supported for this operation"
throw $_
}
# Define comprehensive resource information class for Key Vault secret inventory
class ResourceCheck {
[string] $ManagementGroupId = ""
[string] $ManagementGroupName = ""
[string] $SubscriptionId = ""
[string] $SubscriptionName = ""
[string] $ResourceGroup = ""
[string] $ResourceId = ""
[string] $Location = ""
[string] $ResourceName = ""
[string] $Secret_Key = ""
[string] $Tag_Team = ""
[string] $Tag_Product = ""
[string] $Tag_Environment = ""
[string] $Tag_Data = ""
[string] $Tag_Deployment = ""
[string] $Tag_CreatedOnDate = ""
# Management Group hierarchy information
[string] $ManagementGroupId = "" # Azure Management Group ID
[string] $ManagementGroupName = "" # Management Group display name
# Subscription context information
[string] $SubscriptionId = "" # Azure subscription ID
[string] $SubscriptionName = "" # Subscription display name
# Resource location and identification
[string] $ResourceGroup = "" # Resource group containing the Key Vault
[string] $ResourceId = "" # Full Azure resource ID of the Key Vault
[string] $Location = "" # Azure region where Key Vault is deployed
[string] $ResourceName = "" # Key Vault name
# Secret inventory information
[string] $Secret_Key = "" # Name of the secret (not the secret value)
# Resource governance tags for compliance and organization
[string] $Tag_Team = "" # Team responsible for the Key Vault
[string] $Tag_Product = "" # Product or service associated with the Key Vault
[string] $Tag_Environment = "" # Environment classification (dev/test/prod)
[string] $Tag_Data = "" # Data classification level
[string] $Tag_Deployment = "" # Deployment method or automation tag
[string] $Tag_CreatedOnDate = "" # Resource creation timestamp
}
Write-Host "======================================================================================================================================================================"
Write-Host "Creating key vault secrets overview for key vaults with access policies."
Write-Host "🔐 Azure Key Vault Secret Inventory (Access Policy-Based Vaults Only)"
Write-Host "======================================================================================================================================================================"
Write-Host "⚠️ SECURITY WARNING: This script temporarily modifies Key Vault access policies during execution"
Write-Host "Script execution started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host ""
Write-Host "Security Configuration:"
Write-Host " User Object ID for temporary access: $userObjectId"
Write-Host " Permissions granted: List secrets only (temporary)"
Write-Host " Scope: Access policy-based Key Vaults only (RBAC vaults excluded)"
Write-Host " Data collected: Secret names only (values are NOT retrieved)"
Write-Host "======================================================================================================================================================================"
Write-Host ""
# Validate that we successfully retrieved a user Object ID
if ([string]::IsNullOrEmpty($userObjectId)) {
Write-Host "❌ CRITICAL ERROR: Could not retrieve current user's Object ID" -ForegroundColor Red
Write-Host ""
Write-Host "This could indicate:"
Write-Host " - You are not authenticated to Azure (run Connect-AzAccount)"
Write-Host " - You are authenticated with a Service Principal (user account required)"
Write-Host " - Your account is not found in Azure AD"
Write-Host " - Insufficient permissions to query Azure AD user information"
Write-Host ""
Write-Host "Please ensure you are authenticated with a valid user account and try again."
Write-Host "======================================================================================================================================================================"
throw "User Object ID retrieval failed"
}
# Generate timestamped filename for export
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_key_vault_secrets.csv"
$fileName = ".\$date azure_key_vault_secrets.csv"
Write-Host "Export file: $fileName"
Write-Host ""
$managementGroups = Get-AzManagementGroup
# Initialize processing counters and security tracking
$totalManagementGroups = 0
$totalSubscriptions = 0
$totalKeyVaults = 0
$totalSecrets = 0
$rbacOnlyVaults = 0
$processedResourceGroups = 0
$accessPolicyModifications = 0
$cleanupFailures = @()
foreach ($managementGroup in $managementGroups)
{
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "Management group [$($managementGroup.Name)]"
Write-Host "Discovering Management Group hierarchy..."
try {
$managementGroups = Get-AzManagementGroup -ErrorAction Stop
$totalManagementGroups = $managementGroups.Count
Write-Host "✓ Found $totalManagementGroups Management Group(s) to process:"
foreach ($mg in $managementGroups) {
Write-Host " - $($mg.DisplayName) ($($mg.Name))"
}
} catch {
Write-Error "Failed to retrieve Management Groups. Please ensure you have appropriate permissions."
Write-Error "Required permissions: Management Group Reader at tenant root or target management groups"
throw $_
}
Write-Host ""
$subscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name | Where-Object State -eq "Active"
foreach ($subscription in $subscriptions)
{
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
Write-Host "Subscription [$($subscription.DisplayName) - $subscriptionId]"
Set-AzContext -SubscriptionId $subscriptionId | Out-Null
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
$allResourceGroups = Get-AzResourceGroup
[ResourceCheck[]]$Result = @()
foreach ($group in $allResourceGroups) {
$allVaults = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName
foreach ($vault in $allVaults) {
Write-Host $vault.VaultName
$vaultWithAllProps = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName -Name $vault.VaultName
if ($vaultWithAllProps.EnableRbacAuthorization -ne "TRUE") {
Write-Host " -- processing..."
Set-AzKeyVaultAccessPolicy -VaultName $vault.VaultName -ObjectId $userObjectId -PermissionsToSecrets "List"
$secrets = Get-AzKeyVaultSecret -VaultName $vault.VaultName
foreach($secret in $secrets)
{
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.Name
$resourceCheck.ResourceGroup = $vaultWithAllProps.ResourceGroupName
$resourceCheck.ResourceId = $vaultWithAllProps.ResourceId
$resourceCheck.Location = $vaultWithAllProps.Location
$resourceCheck.ResourceName = $vaultWithAllProps.VaultName
$resourceCheck.Secret_Key = $secret.Name
$resourceCheck.Tag_Team = $vaultWithAllProps.Tags.team
$resourceCheck.Tag_Product = $vaultWithAllProps.Tags.product
$resourceCheck.Tag_Environment = $vaultWithAllProps.Tags.environment
$resourceCheck.Tag_Data = $vaultWithAllProps.Tags.data
$resourceCheck.Tag_CreatedOnDate = $vaultWithAllProps.Tags.CreatedOnDate
$resourceCheck.Tag_Deployment = $vaultWithAllProps.Tags.drp_deployment
$Result += $resourceCheck
}
Remove-AzKeyVaultAccessPolicy -VaultName $vault.VaultName -ObjectId $userObjectId
}
}
# Process each Management Group in the hierarchy
foreach ($managementGroup in $managementGroups) {
Write-Host "======================================================================================================================================================================"
Write-Host "Processing Management Group: $($managementGroup.DisplayName) ($($managementGroup.Name))"
Write-Host "======================================================================================================================================================================"
try {
# Get all active subscriptions within this management group
Write-Host "Discovering active subscriptions in management group..."
$subscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name -ErrorAction Stop | Where-Object State -eq "Active"
if (-not $subscriptions -or $subscriptions.Count -eq 0) {
Write-Host " No active subscriptions found in management group '$($managementGroup.DisplayName)'"
continue
}
Write-Host "✓ Found $($subscriptions.Count) active subscription(s):"
foreach ($sub in $subscriptions) {
Write-Host " - $($sub.DisplayName)"
}
Write-Host ""
# Process each active subscription
foreach ($subscription in $subscriptions) {
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
# Extract subscription ID from the full resource path
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
Write-Host "Processing Subscription: $($subscription.DisplayName) ($subscriptionId)"
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
try {
# Set Azure context to the current subscription
Set-AzContext -SubscriptionId $subscriptionId -ErrorAction Stop | Out-Null
Write-Host "✓ Successfully connected to subscription context"
$totalSubscriptions++
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
# Get all resource groups in the current subscription
Write-Host "Discovering resource groups..."
$allResourceGroups = Get-AzResourceGroup -ErrorAction Stop
Write-Host "✓ Found $($allResourceGroups.Count) resource group(s) to scan"
# Initialize result collection for this subscription
[ResourceCheck[]]$Result = @()
$subscriptionKeyVaults = 0
$subscriptionSecrets = 0
$subscriptionRbacVaults = 0
$subscriptionAccessPolicyMods = 0
# Process each resource group to find Key Vaults
foreach ($group in $allResourceGroups) {
Write-Host " Scanning resource group: $($group.ResourceGroupName)"
$processedResourceGroups++
try {
# Get all Key Vaults in the current resource group
$allVaults = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName -ErrorAction Stop
if (-not $allVaults -or $allVaults.Count -eq 0) {
Write-Host " No Key Vaults found in resource group"
continue
}
Write-Host " ✓ Found $($allVaults.Count) Key Vault(s)"
# Process each Key Vault found
foreach ($vault in $allVaults) {
Write-Host " 🔐 Processing Key Vault: $($vault.VaultName)"
try {
# Get detailed Key Vault properties
$vaultWithAllProps = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName -Name $vault.VaultName -ErrorAction Stop
$totalKeyVaults++
$subscriptionKeyVaults++
# Check if vault uses traditional access policies (not RBAC-only)
if ($vaultWithAllProps.EnableRbacAuthorization -ne $true) {
Write-Host " 📋 Access Policy-based vault - processing secrets..."
# ⚠️ SECURITY CRITICAL: Temporarily grant List permissions to enumerate secrets
try {
Write-Host " 🔑 Granting temporary List permissions to user: $userObjectId"
Set-AzKeyVaultAccessPolicy -VaultName $vault.VaultName -ObjectId $userObjectId -PermissionsToSecrets "List" -ErrorAction Stop
$accessPolicyModifications++
$subscriptionAccessPolicyMods++
# Enumerate all secrets in the vault
Write-Host " 📝 Enumerating secrets..."
$secrets = Get-AzKeyVaultSecret -VaultName $vault.VaultName -ErrorAction Stop
if (-not $secrets -or $secrets.Count -eq 0) {
Write-Host " No secrets found in this vault"
} else {
Write-Host " ✓ Found $($secrets.Count) secret(s)"
# Process each secret found
foreach ($secret in $secrets) {
Write-Host " Secret: $($secret.Name)"
# Create comprehensive resource check entry
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
# Populate management group and subscription information
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
# Populate Key Vault resource information
$resourceCheck.ResourceGroup = $vaultWithAllProps.ResourceGroupName
$resourceCheck.ResourceId = $vaultWithAllProps.ResourceId
$resourceCheck.Location = $vaultWithAllProps.Location
$resourceCheck.ResourceName = $vaultWithAllProps.VaultName
# Populate secret information (name only, not value)
$resourceCheck.Secret_Key = $secret.Name
# Extract resource tags for governance analysis
$resourceCheck.Tag_Team = $vaultWithAllProps.Tags.team
$resourceCheck.Tag_Product = $vaultWithAllProps.Tags.product
$resourceCheck.Tag_Environment = $vaultWithAllProps.Tags.environment
$resourceCheck.Tag_Data = $vaultWithAllProps.Tags.data
$resourceCheck.Tag_CreatedOnDate = $vaultWithAllProps.Tags.CreatedOnDate
$resourceCheck.Tag_Deployment = $vaultWithAllProps.Tags.drp_deployment
# Add to results collection
$Result += $resourceCheck
$totalSecrets++
$subscriptionSecrets++
}
}
# ⚠️ SECURITY CRITICAL: Remove temporary permissions immediately
Write-Host " 🧹 Removing temporary permissions..."
try {
Remove-AzKeyVaultAccessPolicy -VaultName $vault.VaultName -ObjectId $userObjectId -ErrorAction Stop
Write-Host " ✓ Successfully removed temporary permissions"
} catch {
Write-Host " ❌ CLEANUP FAILURE: Could not remove temporary permissions!" -ForegroundColor Red
$cleanupFailures += @{
VaultName = $vault.VaultName
ResourceGroup = $group.ResourceGroupName
SubscriptionId = $subscriptionId
Error = $_.Exception.Message
}
Write-Host " ⚠️ Manual cleanup required: Remove-AzKeyVaultAccessPolicy -VaultName '$($vault.VaultName)' -ObjectId '$userObjectId'" -ForegroundColor Yellow
}
} catch {
Write-Host " ❌ Error accessing vault secrets: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " This may indicate insufficient permissions or vault access restrictions"
# Ensure cleanup attempt even on failure
try {
Remove-AzKeyVaultAccessPolicy -VaultName $vault.VaultName -ObjectId $userObjectId -ErrorAction SilentlyContinue
} catch {
# Silent cleanup attempt
}
}
} else {
Write-Host " 🔐 RBAC-only vault (skipped): $($vault.VaultName)" -ForegroundColor Blue
$rbacOnlyVaults++
$subscriptionRbacVaults++
}
} catch {
Write-Host " ❌ Error processing vault '$($vault.VaultName)': $($_.Exception.Message)" -ForegroundColor Red
}
}
} catch {
Write-Host " ❌ Error scanning resource group '$($group.ResourceGroupName)': $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host ""
Write-Host "Subscription Summary:"
Write-Host " Key Vaults found: $subscriptionKeyVaults"
Write-Host " Secrets inventoried: $subscriptionSecrets"
Write-Host " Access policy modifications: $subscriptionAccessPolicyMods"
Write-Host " RBAC-only vaults (skipped): $subscriptionRbacVaults"
if ($cleanupFailures.Count -gt 0) {
Write-Host " ⚠️ Cleanup failures: $($cleanupFailures.Count)" -ForegroundColor Yellow
}
Write-Host ""
# Export subscription results to CSV file
if ($Result.Count -gt 0) {
Write-Host "Exporting $($Result.Count) secret entries to CSV..."
try {
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation -ErrorAction Stop
Write-Host "✓ Successfully exported subscription data"
} catch {
Write-Error "Failed to export data for subscription '$($subscription.DisplayName)': $($_.Exception.Message)"
}
} else {
Write-Host " No secret data to export from this subscription"
}
} catch {
Write-Host "❌ Error processing subscription '$($subscription.DisplayName)': $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Please verify subscription access and permissions" -ForegroundColor Red
}
Write-Host ""
}
} catch {
Write-Host "❌ Error processing management group '$($managementGroup.DisplayName)': $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Please verify management group access and permissions" -ForegroundColor Red
}
}
Write-Host "======================================================================================================================================================================"
Write-Host "Done."
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "🔐 Key Vault Secret Inventory Completed"
Write-Host "======================================================================================================================================================================"
Write-Host "Execution completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host ""
# Display comprehensive execution statistics
Write-Host "Processing Statistics:"
Write-Host " Management Groups processed: $totalManagementGroups"
Write-Host " Subscriptions processed: $totalSubscriptions"
Write-Host " Resource Groups scanned: $processedResourceGroups"
Write-Host " Total Key Vaults found: $totalKeyVaults"
Write-Host " Secrets inventoried: $totalSecrets"
Write-Host " Access policy modifications: $accessPolicyModifications"
Write-Host " RBAC-only vaults (skipped): $rbacOnlyVaults"
Write-Host ""
# Critical security status check
if ($cleanupFailures.Count -gt 0) {
Write-Host "🚨 CRITICAL SECURITY ALERT: MANUAL CLEANUP REQUIRED" -ForegroundColor Red
Write-Host "======================================================================================================================================================================"
Write-Host "The following Key Vaults still have temporary permissions that need manual removal:" -ForegroundColor Red
Write-Host ""
foreach ($failure in $cleanupFailures) {
Write-Host " Vault: $($failure.VaultName)" -ForegroundColor Red
Write-Host " Resource Group: $($failure.ResourceGroup)" -ForegroundColor Red
Write-Host " Subscription: $($failure.SubscriptionId)" -ForegroundColor Red
Write-Host " Cleanup Command: Remove-AzKeyVaultAccessPolicy -VaultName '$($failure.VaultName)' -ObjectId '$userObjectId'" -ForegroundColor Yellow
Write-Host " Error: $($failure.Error)" -ForegroundColor Red
Write-Host ""
}
Write-Host "⚠️ Please run the cleanup commands above to remove temporary permissions immediately!" -ForegroundColor Yellow
Write-Host "======================================================================================================================================================================"
} else {
Write-Host "✅ Security Status: All temporary access policies were successfully removed"
}
Write-Host ""
# Analyze and display key findings
if ($totalSecrets -gt 0) {
Write-Host "✓ Successfully exported Key Vault secret inventory to: $fileName"
Write-Host ""
Write-Host "Security Analysis Insights:"
if ($rbacOnlyVaults -gt 0) {
Write-Host "$rbacOnlyVaults Key Vault(s) using modern RBAC authentication"
}
$legacyVaults = $totalKeyVaults - $rbacOnlyVaults
if ($legacyVaults -gt 0) {
Write-Host "$legacyVaults Key Vault(s) still using legacy access policies" -ForegroundColor Yellow
Write-Host " Consider migrating to Azure RBAC for improved security and management"
}
Write-Host ""
Write-Host "Output File Information:"
if (Test-Path $fileName) {
Write-Host " File Path: $fileName"
Write-Host " File Size: $([Math]::Round((Get-Item $fileName).Length / 1KB, 2)) KB"
Write-Host " Records Exported: $totalSecrets"
}
} else {
Write-Host " No Key Vault secrets found across all processed subscriptions"
Write-Host " This could indicate:"
Write-Host " - All Key Vaults are using RBAC-only authentication"
Write-Host " - No secrets exist in the scanned Key Vaults"
Write-Host " - Permission issues preventing access to Key Vault contents"
}
Write-Host ""
Write-Host "Security Recommendations:"
Write-Host " - Review exported secret inventory for unused or expired secrets"
Write-Host " - Implement secret rotation policies for all identified secrets"
Write-Host " - Consider implementing Azure RBAC for new Key Vaults"
Write-Host " - Plan migration from access policies to RBAC for existing vaults"
Write-Host " - Implement regular secret lifecycle management and cleanup"
Write-Host " - Ensure proper governance tags are applied to all Key Vaults"
Write-Host ""
Write-Host "Compliance and Security Notes:"
Write-Host " - This inventory contains sensitive secret name information"
Write-Host " - Handle output file with appropriate data classification controls"
Write-Host " - All access policy modifications are logged in Azure Activity Log"
Write-Host " - Consider encryption for long-term storage of inventory results"
Write-Host " - Schedule regular execution for continuous secret governance"
Write-Host " - Ensure manual cleanup is performed if cleanup failures occurred"
Write-Host ""
Write-Host "Audit Trail:"
Write-Host " - All temporary access policy changes are logged in Azure Activity Log"
Write-Host " - Search for 'Microsoft.KeyVault/vaults/accessPolicies/write' operations"
Write-Host " - Filter by Object ID: $userObjectId"
Write-Host " - Execution timeframe: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "======================================================================================================================================================================"

View File

@@ -1,4 +1,74 @@
#Connect-AzAccount
<#
.SYNOPSIS
Generates a comprehensive inventory of Azure Key Vaults across all management groups and subscriptions.
.DESCRIPTION
This script enumerates all Azure Key Vaults within enabled subscriptions across the entire Azure tenant,
collecting detailed configuration properties, security settings, and governance tags. The results are
exported to a timestamped CSV file for analysis, compliance reporting, and security auditing.
Key capabilities:
- Multi-tenant Key Vault discovery across all management groups
- Configuration analysis including RBAC, purge protection, and soft delete settings
- Governance tag extraction for team ownership and compliance tracking
- Public network access configuration reporting
- Timestamped CSV export for audit trails and trend analysis
.PARAMETER None
This script does not accept parameters and will process all accessible Key Vaults.
.OUTPUTS
CSV File: "<date> azure_key_vaults.csv"
Contains columns for:
- Resource identification (ID, name, resource group, location)
- Management hierarchy (management group, subscription)
- Governance tags (team, product, environment, data classification)
- Security configuration (RBAC, purge protection, soft delete, network access)
.EXAMPLE
.\KeyVaults.ps1
Discovers all Key Vaults and generates: "2024-10-30 1435 azure_key_vaults.csv"
.NOTES
File Name : KeyVaults.ps1
Author : Cloud Engineering Team
Prerequisite : Azure PowerShell module (Az.KeyVault, Az.Resources, Az.Accounts)
Copyright : (c) 2024 Effectory. All rights reserved.
Version History:
1.0 - Initial release with comprehensive Key Vault inventory functionality
.LINK
https://docs.microsoft.com/en-us/azure/key-vault/
https://docs.microsoft.com/en-us/powershell/module/az.keyvault/
.COMPONENT
Requires Azure PowerShell modules:
- Az.KeyVault (for Key Vault enumeration and property retrieval)
- Az.Resources (for resource group and management group access)
- Az.Accounts (for authentication and subscription management)
.ROLE
Required Azure permissions:
- Key Vault Reader or higher on all subscriptions
- Management Group Reader for organizational hierarchy access
.FUNCTIONALITY
- Multi-subscription Key Vault discovery
- Security configuration analysis and reporting
- Governance tag extraction and compliance tracking
- CSV export with comprehensive audit trail
#>
#Requires -Modules Az.KeyVault, Az.Resources, Az.Accounts
#Requires -Version 5.1
[CmdletBinding()]
param()
# Uncomment the following line if authentication is required
#Connect-AzAccount
class ResourceCheck {
[string] $ResourceId = ""
@@ -22,77 +92,234 @@ class ResourceCheck {
}
# Initialize script execution
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$startTime = Get-Date
Write-Host "======================================================================================================================================================================"
Write-Host "Creating key vault resource overview."
Write-Host "🔐 AZURE KEY VAULT INVENTORY GENERATOR" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
Write-Host "⏰ Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Green
Write-Host ""
$subscriptions = Get-AzSubscription | Where-Object State -eq "Enabled"
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_key_vaults.csv"
$managementGroups = Get-AzManagementGroup
foreach ($managementGroup in $managementGroups)
{
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "Management group [$($managementGroup.Name)]"
$subscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name | Where-Object State -eq "Active"
foreach ($subscription in $subscriptions)
{
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
Write-Host "Subscription [$($subscription.DisplayName) - $subscriptionId]"
Set-AzContext -SubscriptionId $subscriptionId | Out-Null
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
$allResourceGroups = Get-AzResourceGroup
[ResourceCheck[]]$Result = @()
foreach ($group in $allResourceGroups) {
Write-Host $group.ResourceGroupName
$allVaults = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName
foreach ($vault in $allVaults) {
$vaultWithAllProps = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName -Name $vault.VaultName
$enabledRBAC = $vaultWithAllProps.EnableRbacAuthorization -eq "TRUE"
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.ResourceId = $vaultWithAllProps.ResourceId
$resourceCheck.Location = $vaultWithAllProps.Location
$resourceCheck.ResourceName = $vaultWithAllProps.VaultName
$resourceCheck.ResourceGroup = $vaultWithAllProps.ResourceGroupName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.Tag_Team = $vaultWithAllProps.Tags.team
$resourceCheck.Tag_Product = $vaultWithAllProps.Tags.product
$resourceCheck.Tag_Environment = $vaultWithAllProps.Tags.environment
$resourceCheck.Tag_Data = $vaultWithAllProps.Tags.data
$resourceCheck.Tag_CreatedOnDate = $vaultWithAllProps.Tags.CreatedOnDate
$resourceCheck.Tag_Deployment = $vaultWithAllProps.Tags.drp_deployment
$resourceCheck.Prop_EnablePurgeProtection = $vaultWithAllProps.EnablePurgeProtection
$resourceCheck.Prop_EnableRbacAuthorization = $enabledRBAC
$resourceCheck.Prop_EnableSoftDelete = $vaultWithAllProps.EnableSoftDelete
$resourceCheck.Prop_PublicNetworkAccess = $vaultWithAllProps.PublicNetworkAccess
$Result += $resourceCheck
}
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
try {
# Validate Azure authentication
$context = Get-AzContext
if (-not $context) {
throw "No Azure context found. Please run Connect-AzAccount first."
}
}
Write-Host "======================================================================================================================================================================"
Write-Host "Done."
Write-Host "🔐 Authenticated as: $($context.Account.Id)" -ForegroundColor Green
Write-Host "🏢 Tenant: $($context.Tenant.Id)" -ForegroundColor Green
Write-Host ""
# Initialize output file
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_key_vaults.csv"
Write-Host "📄 Output file: $fileName" -ForegroundColor Yellow
Write-Host ""
# 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 ""
# Initialize counters for progress tracking
$totalKeyVaults = 0
$processedManagementGroups = 0
$processedSubscriptions = 0
$securityIssues = @()
# 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 subscriptions in this management group
$subscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name | Where-Object State -eq "Active"
Write-Host " 📋 Found $($subscriptions.Count) active subscriptions" -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 = @()
$subscriptionKeyVaults = 0
foreach ($group in $allResourceGroups) {
Write-Host " 📁 Resource Group: $($group.ResourceGroupName)" -ForegroundColor DarkCyan -NoNewline
try {
# Get Key Vaults in this resource group
$allVaults = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName -ErrorAction SilentlyContinue
if ($allVaults.Count -gt 0) {
Write-Host " - Found $($allVaults.Count) Key Vaults" -ForegroundColor Green
$subscriptionKeyVaults += $allVaults.Count
} else {
Write-Host " - No Key Vaults" -ForegroundColor DarkGray
}
foreach ($vault in $allVaults) {
try {
# Get detailed Key Vault properties
$vaultWithAllProps = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName -Name $vault.VaultName
# Analyze security configuration
$enabledRBAC = $vaultWithAllProps.EnableRbacAuthorization -eq "TRUE"
# Check for security concerns
if (-not $vaultWithAllProps.EnablePurgeProtection) {
$securityIssues += "⚠️ Purge protection disabled: $($vaultWithAllProps.VaultName) in $($group.ResourceGroupName)"
}
if (-not $vaultWithAllProps.EnableSoftDelete) {
$securityIssues += "⚠️ Soft delete disabled: $($vaultWithAllProps.VaultName) in $($group.ResourceGroupName)"
}
if ($vaultWithAllProps.PublicNetworkAccess -eq "Enabled") {
$securityIssues += "🌐 Public network access enabled: $($vaultWithAllProps.VaultName) in $($group.ResourceGroupName)"
}
# Create resource check object
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.ResourceId = $vaultWithAllProps.ResourceId
$resourceCheck.Location = $vaultWithAllProps.Location
$resourceCheck.ResourceName = $vaultWithAllProps.VaultName
$resourceCheck.ResourceGroup = $vaultWithAllProps.ResourceGroupName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.Tag_Team = $vaultWithAllProps.Tags.team
$resourceCheck.Tag_Product = $vaultWithAllProps.Tags.product
$resourceCheck.Tag_Environment = $vaultWithAllProps.Tags.environment
$resourceCheck.Tag_Data = $vaultWithAllProps.Tags.data
$resourceCheck.Tag_CreatedOnDate = $vaultWithAllProps.Tags.CreatedOnDate
$resourceCheck.Tag_Deployment = $vaultWithAllProps.Tags.drp_deployment
$resourceCheck.Prop_EnablePurgeProtection = $vaultWithAllProps.EnablePurgeProtection
$resourceCheck.Prop_EnableRbacAuthorization = $enabledRBAC
$resourceCheck.Prop_EnableSoftDelete = $vaultWithAllProps.EnableSoftDelete
$resourceCheck.Prop_PublicNetworkAccess = $vaultWithAllProps.PublicNetworkAccess
$Result += $resourceCheck
$totalKeyVaults++
} catch {
Write-Host " ❌ Error processing vault $($vault.VaultName): $($_.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) Key Vaults from subscription" -ForegroundColor Green
} else {
Write-Host " No Key Vaults 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 summary report
$endTime = Get-Date
$executionTime = $endTime - $startTime
Write-Host "======================================================================================================================================================================"
Write-Host "📊 AZURE KEY VAULT 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 Key Vaults Discovered: $totalKeyVaults" -ForegroundColor Green
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 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 10) {
Write-Host " $issue" -ForegroundColor Yellow
}
if ($securityIssues.Count -gt 10) {
Write-Host " ... and $($securityIssues.Count - 10) more issues (see CSV for complete details)" -ForegroundColor DarkYellow
}
Write-Host ""
Write-Host "📋 Recommendations:" -ForegroundColor Cyan
Write-Host " • Enable purge protection on production Key Vaults" -ForegroundColor White
Write-Host " • Ensure soft delete is enabled for data recovery capabilities" -ForegroundColor White
Write-Host " • Consider disabling public network access where possible" -ForegroundColor White
Write-Host " • Implement RBAC authorization for enhanced security" -ForegroundColor White
} else {
Write-Host ""
Write-Host "✅ SECURITY ANALYSIS: No major security concerns detected" -ForegroundColor Green
}
Write-Host ""
Write-Host "📈 NEXT STEPS:" -ForegroundColor Cyan
Write-Host " 1. Review the generated CSV file for detailed Key Vault configurations" -ForegroundColor White
Write-Host " 2. Analyze governance tags for compliance with organizational standards" -ForegroundColor White
Write-Host " 3. Address any security recommendations identified above" -ForegroundColor White
Write-Host " 4. Consider implementing automated monitoring for Key Vault configurations" -ForegroundColor White
Write-Host ""
Write-Host "✅ Azure Key Vault 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 Key Vault Reader permissions on target subscriptions" -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. 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"
}

View File

@@ -1,4 +1,98 @@
#Connect-AzAccount
<#
.SYNOPSIS
Generates a hierarchical inventory of Azure Management Groups and Subscriptions.
.DESCRIPTION
This script creates a comprehensive mapping of Azure organizational structure by traversing
the management group hierarchy starting from a specified root management group. It discovers
and documents all subscriptions within a 3-level management group structure (Level 0-2).
The script provides detailed organizational visibility including:
- Hierarchical management group structure mapping
- Subscription placement and organizational context
- Subscription state tracking (Active, Disabled, etc.)
- Multi-level governance structure documentation
- CSV export for organizational analysis and compliance reporting
Note: The script is optimized for a maximum 3-level management group depth and starts
from a configurable root management group ID.
.PARAMETER RootManagementGroupId
The GUID of the root management group to start the hierarchy discovery from.
Defaults to the Effectory organization root management group.
Example: "12345678-1234-1234-1234-123456789012"
.OUTPUTS
CSV File: "<date> azure_managementgroups.csv"
Contains columns for:
- Subscription identification (ID, name, state)
- Level 0 Management Group (root level)
- Level 1 Management Group (department/division level)
- Level 2 Management Group (team/project level)
.EXAMPLE
.\ManagementGroups.ps1
Uses the default Effectory root management group and generates:
"2024-10-30 1435 azure_managementgroups.csv"
.EXAMPLE
.\ManagementGroups.ps1 -RootManagementGroupId "87654321-4321-4321-4321-210987654321"
Discovers management group hierarchy starting from a custom root management group.
.EXAMPLE
.\ManagementGroups.ps1 -RootManagementGroupId "tenant-root-mg" -Verbose
Runs with verbose output for detailed discovery logging.
.NOTES
File Name : ManagementGroups.ps1
Author : Cloud Engineering Team
Prerequisite : Azure PowerShell module (Az.Resources, Az.Accounts)
Copyright : (c) 2024 Effectory. All rights reserved.
Version History:
1.0 - Initial release with 3-level management group hierarchy discovery
1.1 - Added parameterized root management group for flexibility
.LINK
https://docs.microsoft.com/en-us/azure/governance/management-groups/
https://docs.microsoft.com/en-us/powershell/module/az.resources/
.COMPONENT
Requires Azure PowerShell modules:
- Az.Resources (for management group and subscription enumeration)
- Az.Accounts (for authentication and context management)
.ROLE
Required Azure permissions:
- Management Group Reader on the root management group and all child groups
- Reader access to view subscription details within management groups
.FUNCTIONALITY
- Hierarchical management group discovery (3-level maximum)
- Subscription placement mapping and state tracking
- Organizational structure documentation and CSV export
- Cross-tenant compatible with configurable root management group
#>
#Requires -Modules Az.Resources, Az.Accounts
#Requires -Version 5.1
# Uncomment the following line if authentication is required
#Connect-AzAccount
[CmdletBinding()]
param(
[Parameter(
Mandatory = $false,
HelpMessage = "The GUID of the root management group to start hierarchy discovery from"
)]
[ValidateNotNullOrEmpty()]
[string]$RootManagementGroupId = 'e9792fd7-4044-47e7-a40d-3fba46f1cd09'
)
class ResourceCheck {
[string] $SubscriptionId = ""
@@ -12,102 +106,286 @@ class ResourceCheck {
[string] $Level2_ManagementGroupName = ""
}
Write-Host "======================================================================================================================"
Write-Host "Creating list of Effectory Management Groups and subscriptions."
Write-Host "- Note: not very dynamic; Starts at hard coded root group and works up max 2 levels."
Write-Host "======================================================================================================================"
# Initialize script execution
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$startTime = Get-Date
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_managementgroups.csv"
[ResourceCheck[]]$Result = @()
Write-Host "======================================================================================================================================================================"
Write-Host "🏗️ AZURE MANAGEMENT GROUP STRUCTURE DISCOVERY" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
Write-Host "⏰ Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Green
Write-Host ""
$rootManagementGroup = (Get-AzManagementGroup -GroupId 'e9792fd7-4044-47e7-a40d-3fba46f1cd09' -Expand)[0]
#level 0
Write-Host "---------------------------------------------------------------------------------------------"
Write-Host "Level 0 Management group [$($rootManagementGroup.Name)]"
Write-Host "---------------------------------------------------------------------------------------------"
$subscriptions = $rootManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
foreach ($subscription in $subscriptions)
{
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
Write-Host "Subscription [$($subscription.DisplayName) - $subscriptionId]"
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level0_ManagementGroupId = $rootManagementGroup.Id
$resourceCheck.Level0_ManagementGroupName = $rootManagementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscriptionId
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
}
#level 1
foreach ($level1ManagementGroupLister in ($rootManagementGroup.Children | Where-Object Type -EQ 'Microsoft.Management/managementGroups'))
{
$level1ManagementGroup = (Get-AzManagementGroup -Group $level1ManagementGroupLister.Name -Expand)[0]
Write-Host " ---------------------------------------------------------------------------------------------"
Write-Host " Level 1 Management group [$($level1ManagementGroup.Name)]"
Write-Host " ---------------------------------------------------------------------------------------------"
$subscriptions = $level1ManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
foreach ($subscription in $subscriptions)
{
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
Write-Host " Subscription [$($subscription.DisplayName) - $subscriptionId]"
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level0_ManagementGroupId = $rootManagementGroup.Id
$resourceCheck.Level0_ManagementGroupName = $rootManagementGroup.DisplayName
$resourceCheck.Level1_ManagementGroupId = $level1ManagementGroup.Id
$resourceCheck.Level1_ManagementGroupName = $level1ManagementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscriptionId
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
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 "🎯 Root Management Group: $RootManagementGroupId" -ForegroundColor Yellow
Write-Host ""
# Initialize output file and tracking variables
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_managementgroups.csv"
Write-Host "📄 Output file: $fileName" -ForegroundColor Yellow
Write-Host ""
[ResourceCheck[]]$Result = @()
$totalSubscriptions = 0
$managementGroupCount = 0
# Get root management group with error handling
Write-Host "🔍 Discovering root management group structure..." -ForegroundColor Cyan
$rootManagementGroup = Get-AzManagementGroup -GroupId $RootManagementGroupId -Expand -ErrorAction Stop
if (-not $rootManagementGroup) {
throw "Root management group '$RootManagementGroupId' not found or not accessible."
}
Write-Host "✅ Root Management Group: $($rootManagementGroup.DisplayName)" -ForegroundColor Green
Write-Host " ID: $($rootManagementGroup.Id)" -ForegroundColor DarkGray
Write-Host ""
#level 2
foreach ($level2ManagementGroupLister in ($level1ManagementGroup.Children | Where-Object Type -EQ 'Microsoft.Management/managementGroups'))
{
$level2ManagementGroup = (Get-AzManagementGroup -Group $level2ManagementGroupLister.Name -Expand)[0]
# Process Level 0 (Root) subscriptions
$managementGroupCount++
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "📋 LEVEL 0 (Root): $($rootManagementGroup.DisplayName)" -ForegroundColor Cyan
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host " ---------------------------------------------------------------------------------------------"
Write-Host " Level 2 Management group [$($level2ManagementGroup.Name)]"
Write-Host " ---------------------------------------------------------------------------------------------"
$subscriptions = $level2ManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
$subscriptions = $rootManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
Write-Host " 📊 Direct subscriptions: $($subscriptions.Count)" -ForegroundColor Green
foreach ($subscription in $subscriptions)
{
foreach ($subscription in $subscriptions) {
try {
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
Write-Host " Subscription [$($subscription.DisplayName) - $subscriptionId]"
# Color code subscription state
$stateColor = switch ($subscription.State) {
"Enabled" { "Green" }
"Disabled" { "Red" }
"Warned" { "Yellow" }
default { "White" }
}
Write-Host " 📋 Subscription: $($subscription.DisplayName) [$subscriptionId] - $($subscription.State)" -ForegroundColor $stateColor
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level0_ManagementGroupId = $rootManagementGroup.Id
$resourceCheck.Level0_ManagementGroupName = $rootManagementGroup.DisplayName
$resourceCheck.Level1_ManagementGroupId = $level1ManagementGroup.Id
$resourceCheck.Level1_ManagementGroupName = $level1ManagementGroup.DisplayName
$resourceCheck.Level2_ManagementGroupId = $level2ManagementGroup.Id
$resourceCheck.Level2_ManagementGroupName = $level2ManagementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscriptionId
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
$totalSubscriptions++
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
}
# Process Level 1 management groups
$level1Groups = $rootManagementGroup.Children | Where-Object Type -EQ 'Microsoft.Management/managementGroups'
Write-Host ""
Write-Host "🔍 Found $($level1Groups.Count) Level 1 management groups" -ForegroundColor Green
Write-Host ""
foreach ($level1ManagementGroupLister in $level1Groups) {
try {
$managementGroupCount++
$level1ManagementGroup = Get-AzManagementGroup -Group $level1ManagementGroupLister.Name -Expand -ErrorAction Stop
Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
Write-Host " 📂 LEVEL 1: $($level1ManagementGroup.DisplayName)" -ForegroundColor Yellow
Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
$subscriptions = $level1ManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
Write-Host " 📊 Direct subscriptions: $($subscriptions.Count)" -ForegroundColor Green
foreach ($subscription in $subscriptions) {
try {
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
# Color code subscription state
$stateColor = switch ($subscription.State) {
"Enabled" { "Green" }
"Disabled" { "Red" }
"Warned" { "Yellow" }
default { "White" }
}
Write-Host " 📋 Subscription: $($subscription.DisplayName) [$subscriptionId] - $($subscription.State)" -ForegroundColor $stateColor
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level0_ManagementGroupId = $rootManagementGroup.Id
$resourceCheck.Level0_ManagementGroupName = $rootManagementGroup.DisplayName
$resourceCheck.Level1_ManagementGroupId = $level1ManagementGroup.Id
$resourceCheck.Level1_ManagementGroupName = $level1ManagementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscriptionId
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
$totalSubscriptions++
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
}
} catch {
Write-Host " ❌ Error accessing Level 1 management group '$($level1ManagementGroupLister.Name)': $($_.Exception.Message)" -ForegroundColor Red
}
# Process Level 2 management groups (nested within Level 1)
$level2Groups = $level1ManagementGroup.Children | Where-Object Type -EQ 'Microsoft.Management/managementGroups'
if ($level2Groups.Count -gt 0) {
Write-Host " 🔍 Found $($level2Groups.Count) Level 2 management groups" -ForegroundColor Green
foreach ($level2ManagementGroupLister in $level2Groups) {
try {
$managementGroupCount++
$level2ManagementGroup = Get-AzManagementGroup -Group $level2ManagementGroupLister.Name -Expand -ErrorAction Stop
Write-Host ""
Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
Write-Host " 📁 LEVEL 2: $($level2ManagementGroup.DisplayName)" -ForegroundColor Magenta
Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
$subscriptions = $level2ManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
Write-Host " 📊 Direct subscriptions: $($subscriptions.Count)" -ForegroundColor Green
foreach ($subscription in $subscriptions) {
try {
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
# Color code subscription state
$stateColor = switch ($subscription.State) {
"Enabled" { "Green" }
"Disabled" { "Red" }
"Warned" { "Yellow" }
default { "White" }
}
Write-Host " 📋 Subscription: $($subscription.DisplayName) [$subscriptionId] - $($subscription.State)" -ForegroundColor $stateColor
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level0_ManagementGroupId = $rootManagementGroup.Id
$resourceCheck.Level0_ManagementGroupName = $rootManagementGroup.DisplayName
$resourceCheck.Level1_ManagementGroupId = $level1ManagementGroup.Id
$resourceCheck.Level1_ManagementGroupName = $level1ManagementGroup.DisplayName
$resourceCheck.Level2_ManagementGroupId = $level2ManagementGroup.Id
$resourceCheck.Level2_ManagementGroupName = $level2ManagementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscriptionId
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
$totalSubscriptions++
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
}
} catch {
Write-Host " ❌ Error accessing Level 2 management group '$($level2ManagementGroupLister.Name)': $($_.Exception.Message)" -ForegroundColor Red
}
}
} else {
Write-Host " No Level 2 management groups found" -ForegroundColor DarkGray
}
}
# Export results to CSV
Write-Host ""
Write-Host "💾 Exporting results to CSV..." -ForegroundColor Cyan
$Result | Export-Csv -Path $fileName -NoTypeInformation
# Calculate execution time and generate summary report
$endTime = Get-Date
$executionTime = $endTime - $startTime
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "📊 AZURE MANAGEMENT GROUP DISCOVERY SUMMARY" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
Write-Host "⏰ Execution Time: $($executionTime.ToString('hh\:mm\:ss'))" -ForegroundColor Green
Write-Host "🏗️ Management Groups Discovered: $managementGroupCount" -ForegroundColor Green
Write-Host "📋 Total Subscriptions Found: $totalSubscriptions" -ForegroundColor Green
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
}
# Analyze subscription states
$subscriptionStates = $Result | Group-Object SubscriptionState
if ($subscriptionStates.Count -gt 0) {
Write-Host ""
Write-Host "📈 SUBSCRIPTION STATE ANALYSIS:" -ForegroundColor Cyan
foreach ($state in $subscriptionStates) {
$stateColor = switch ($state.Name) {
"Enabled" { "Green" }
"Disabled" { "Red" }
"Warned" { "Yellow" }
default { "White" }
}
Write-Host " $($state.Name): $($state.Count) subscriptions" -ForegroundColor $stateColor
}
}
# Provide organizational insights
$level0Subs = ($Result | Where-Object { [string]::IsNullOrEmpty($_.Level1_ManagementGroupId) }).Count
$level1Subs = ($Result | Where-Object { -not [string]::IsNullOrEmpty($_.Level1_ManagementGroupId) -and [string]::IsNullOrEmpty($_.Level2_ManagementGroupId) }).Count
$level2Subs = ($Result | Where-Object { -not [string]::IsNullOrEmpty($_.Level2_ManagementGroupId) }).Count
Write-Host ""
Write-Host "🏗️ ORGANIZATIONAL STRUCTURE:" -ForegroundColor Cyan
Write-Host " Root Level (Level 0): $level0Subs subscriptions" -ForegroundColor Green
Write-Host " Department/Division Level (Level 1): $level1Subs subscriptions" -ForegroundColor Yellow
Write-Host " Team/Project Level (Level 2): $level2Subs subscriptions" -ForegroundColor Magenta
Write-Host ""
Write-Host "📈 NEXT STEPS:" -ForegroundColor Cyan
Write-Host " 1. Review the generated CSV file for detailed organizational mapping" -ForegroundColor White
Write-Host " 2. Analyze subscription placement for governance compliance" -ForegroundColor White
Write-Host " 3. Consider moving orphaned subscriptions to appropriate management groups" -ForegroundColor White
Write-Host " 4. Use this data for policy assignment and resource organization planning" -ForegroundColor White
Write-Host ""
Write-Host "✅ Azure Management Group discovery 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 Management Group Reader permissions on the root management group" -ForegroundColor White
Write-Host " 3. Verify the root management group ID '$RootManagementGroupId' exists and is accessible" -ForegroundColor White
Write-Host " 4. Check that Azure PowerShell modules are installed and up to date" -ForegroundColor White
Write-Host " 5. Try running with a different root management group ID if the current one is invalid" -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"
}
$Result | Export-Csv -Path $fileName -NoTypeInformation
Write-Host "============================================================================================="
Write-Host "Done."

View File

@@ -1,42 +0,0 @@
#Connect-AzAccount
class ResourceCheck {
[string] $ResourceId = ""
[string] $Id = ""
[string] $Kind = ""
[string] $Location = ""
[string] $ResourceName = ""
[string] $ResourceGroupName = ""
[string] $ResourceType = ""
[string] $SubscriptionId = ""
[string] $SubscriptionName = ""
[string] $Tag_Team = ""
[string] $Tag_Product = ""
[string] $Tag_Environment = ""
[string] $Tag_Data = ""
[string] $Tag_Delete = ""
[string] $Tag_Split = ""
}
Write-Host "========================================================================================================================================================================"
Write-Host "Creating policy assignment overview."
Write-Host "========================================================================================================================================================================"
# $subscriptions = Get-AzSubscription
# $fileName = "c:\temp\2020-08-12 azure_policies.csv"
# rm $fileName
# foreach ($subscription in $subscriptions)
# {
# Set-AzContext -SubscriptionId $subscription.Id
# $allAssignments = Get-AzPolicyAssignment
# $allAssignments | Export-Csv -Path $fileName -Append -NoTypeInformation
# }
$fileName = "c:\temp\2020-08-14 azure_policiy_definitions.csv"
Get-AzPolicyDefinition -Builtin | Export-Csv -Path $fileName -NoTypeInformation
Write-Host "========================================================================================================================================================================"
Write-Host "Done."

View File

@@ -1,4 +1,81 @@
#Connect-AzAccount
<#
.SYNOPSIS
Generates a comprehensive inventory of all Azure resources across enabled subscriptions.
.DESCRIPTION
This script enumerates all Azure resources within enabled subscriptions across the entire Azure tenant,
collecting detailed resource properties, governance tags, and managed identity information. The results
are exported to a timestamped CSV file for analysis, compliance reporting, and resource management.
Key capabilities:
- Cross-subscription resource discovery and enumeration
- Comprehensive resource metadata collection (type, location, resource group)
- Governance tag extraction for team ownership and compliance tracking
- Managed identity discovery and principal ID mapping
- Resource lifecycle management tags (delete, split, deployment tracking)
- Timestamped CSV export with complete audit trail
The script processes all enabled subscriptions and captures essential resource information
including resource hierarchy, governance tags, and security identities for comprehensive
Azure estate management and reporting.
.PARAMETER None
This script does not accept parameters and will process all resources in all enabled subscriptions.
.OUTPUTS
CSV File: "<date> Resources.csv"
Contains columns for:
- Resource identification (ID, name, type, kind, location)
- Subscription and resource group context
- Governance tags (team, product, environment, data classification)
- Lifecycle management tags (delete, split, created date, deployment)
- Managed identity information (name, principal ID)
.EXAMPLE
.\Resources.ps1
Discovers all resources across enabled subscriptions and generates:
"2024-10-30 1435 Resources.csv"
.NOTES
File Name : Resources.ps1
Author : Cloud Engineering Team
Prerequisite : Azure PowerShell module (Az.Resources, Az.Accounts, Az.ManagedServiceIdentity)
Copyright : (c) 2024 Effectory. All rights reserved.
Version History:
1.0 - Initial release with comprehensive resource inventory functionality
.LINK
https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/overview
https://docs.microsoft.com/en-us/powershell/module/az.resources/
.COMPONENT
Requires Azure PowerShell modules:
- Az.Resources (for resource enumeration and property retrieval)
- Az.Accounts (for authentication and subscription management)
- Az.ManagedServiceIdentity (for managed identity discovery)
.ROLE
Required Azure permissions:
- Reader or higher on all target subscriptions
- Managed Identity Operator (for identity information retrieval)
.FUNCTIONALITY
- Multi-subscription resource discovery and enumeration
- Governance tag extraction and compliance tracking
- Managed identity mapping and security context analysis
- CSV export with comprehensive resource metadata
#>
#Requires -Modules Az.Resources, Az.Accounts, Az.ManagedServiceIdentity
#Requires -Version 5.1
[CmdletBinding()]
param()
# Uncomment the following line if authentication is required
#Connect-AzAccount
class ResourceCheck {
[string] $ResourceId = ""
@@ -22,59 +99,259 @@ class ResourceCheck {
[string] $ManagedIndentity_PrincipalId = ""
}
Write-Host "========================================================================================================================================================================"
Write-Host "Creating resource overview."
Write-Host "========================================================================================================================================================================"
# Initialize script execution
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$startTime = Get-Date
$subscriptions = Get-AzSubscription | Where-Object State -eq "Enabled"
Write-Host "======================================================================================================================================================================"
Write-Host "🔍 AZURE RESOURCE INVENTORY GENERATOR" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
Write-Host "⏰ Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Green
Write-Host ""
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date Resources.csv"
foreach ($subscription in $subscriptions)
{
Set-AzContext -SubscriptionId $subscription.Id
$allResources = Get-AzResource
[ResourceCheck[]]$Result = @()
try {
# Validate Azure authentication
$context = Get-AzContext
if (-not $context) {
throw "No Azure context found. Please run Connect-AzAccount first."
}
foreach ($resource in $allResources)
{
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ResourceId = $resource.ResourceId
$resourceCheck.Id = $resource.Id
$resourceCheck.Kind = $resource.Kind
$resourceCheck.Location = $resource.Location
$resourceCheck.ResourceName = $resource.ResourceName
$resourceCheck.ResourceGroupName = $resource.ResourceGroupName
$resourceCheck.ResourceType = $resource.ResourceType
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.Name
$resourceCheck.Tag_Team = $resource.Tags.team
$resourceCheck.Tag_Product = $resource.Tags.product
$resourceCheck.Tag_Environment = $resource.Tags.environment
$resourceCheck.Tag_Data = $resource.Tags.data
$resourceCheck.Tag_Delete = $resource.Tags.delete
$resourceCheck.Tag_Split = $resource.Tags.split
$resourceCheck.Tag_CreatedOnDate = $resource.Tags.CreatedOnDate
$resourceCheck.Tag_Deployment = $resource.Tags.drp_deployment
Write-Host "🔐 Authenticated as: $($context.Account.Id)" -ForegroundColor Green
Write-Host "🏢 Tenant: $($context.Tenant.Id)" -ForegroundColor Green
Write-Host ""
# Get enabled subscriptions
Write-Host "🔄 Discovering enabled subscriptions..." -ForegroundColor Cyan
$subscriptions = Get-AzSubscription | Where-Object State -eq "Enabled"
Write-Host "✅ Found $($subscriptions.Count) enabled subscriptions" -ForegroundColor Green
Write-Host ""
# Initialize output file and tracking variables
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date Resources.csv"
Write-Host "📄 Output file: $fileName" -ForegroundColor Yellow
Write-Host ""
$totalResources = 0
$processedSubscriptions = 0
$resourceTypes = @{}
$managedIdentityCount = 0
$governanceIssues = @()
# Process each subscription
foreach ($subscription in $subscriptions) {
$processedSubscriptions++
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "🔄 Processing Subscription [$($processedSubscriptions)/$($subscriptions.Count)]: $($subscription.Name)" -ForegroundColor Yellow
Write-Host " ID: $($subscription.Id)" -ForegroundColor DarkGray
try {
$managedIdentity = $null
$managedIdentity = Get-AzSystemAssignedIdentity -Scope $resource.ResourceId -erroraction 'silentlycontinue'
$resourceCheck.ManagedIndentity_Name = $managedIdentity.Name
$resourceCheck.ManagedIndentity_PrincipalId = $managedIdentity.PrincipalId
}
catch {
$resourceCheck.ManagedIndentity_Name = ""
$resourceCheck.ManagedIndentity_PrincipalId = ""
}
Set-AzContext -SubscriptionId $subscription.Id | Out-Null
$Result += $resourceCheck
# Get all resources in the subscription
Write-Host " 🔍 Discovering resources..." -ForegroundColor Cyan
$allResources = Get-AzResource -ErrorAction Stop
Write-Host " ✅ Found $($allResources.Count) resources" -ForegroundColor Green
[ResourceCheck[]]$Result = @()
$subscriptionResourceCount = 0
$subscriptionManagedIdentityCount = 0
foreach ($resource in $allResources) {
try {
$subscriptionResourceCount++
# Track resource types for analytics
if ($resourceTypes.ContainsKey($resource.ResourceType)) {
$resourceTypes[$resource.ResourceType]++
} else {
$resourceTypes[$resource.ResourceType] = 1
}
# Create resource check object
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ResourceId = $resource.ResourceId
$resourceCheck.Id = $resource.Id
$resourceCheck.Kind = $resource.Kind
$resourceCheck.Location = $resource.Location
$resourceCheck.ResourceName = $resource.ResourceName
$resourceCheck.ResourceGroupName = $resource.ResourceGroupName
$resourceCheck.ResourceType = $resource.ResourceType
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.Name
$resourceCheck.Tag_Team = $resource.Tags.team
$resourceCheck.Tag_Product = $resource.Tags.product
$resourceCheck.Tag_Environment = $resource.Tags.environment
$resourceCheck.Tag_Data = $resource.Tags.data
$resourceCheck.Tag_Delete = $resource.Tags.delete
$resourceCheck.Tag_Split = $resource.Tags.split
$resourceCheck.Tag_CreatedOnDate = $resource.Tags.CreatedOnDate
$resourceCheck.Tag_Deployment = $resource.Tags.drp_deployment
# Check for governance tag compliance
$missingTags = @()
if ([string]::IsNullOrEmpty($resource.Tags.team)) { $missingTags += "team" }
if ([string]::IsNullOrEmpty($resource.Tags.product)) { $missingTags += "product" }
if ([string]::IsNullOrEmpty($resource.Tags.environment)) { $missingTags += "environment" }
if ($missingTags.Count -gt 0) {
$governanceIssues += "⚠️ Missing tags [$($missingTags -join ', ')] on $($resource.ResourceType): $($resource.ResourceName) in $($resource.ResourceGroupName)"
}
# Attempt to get managed identity information
try {
$managedIdentity = Get-AzSystemAssignedIdentity -Scope $resource.ResourceId -ErrorAction SilentlyContinue
if ($managedIdentity) {
$resourceCheck.ManagedIndentity_Name = $managedIdentity.Name
$resourceCheck.ManagedIndentity_PrincipalId = $managedIdentity.PrincipalId
$subscriptionManagedIdentityCount++
$managedIdentityCount++
} else {
$resourceCheck.ManagedIndentity_Name = ""
$resourceCheck.ManagedIndentity_PrincipalId = ""
}
} catch {
# Silently handle managed identity lookup failures
$resourceCheck.ManagedIndentity_Name = ""
$resourceCheck.ManagedIndentity_PrincipalId = ""
}
$Result += $resourceCheck
$totalResources++
# Show progress for large subscriptions
if ($subscriptionResourceCount % 100 -eq 0) {
Write-Host " 📊 Processed $subscriptionResourceCount resources..." -ForegroundColor DarkCyan
}
} catch {
Write-Host " ❌ Error processing resource '$($resource.ResourceName)': $($_.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) resources" -ForegroundColor Green
if ($subscriptionManagedIdentityCount -gt 0) {
Write-Host " 🔐 Found $subscriptionManagedIdentityCount managed identities" -ForegroundColor Cyan
}
} else {
Write-Host " No resources found in subscription" -ForegroundColor DarkYellow
}
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host ""
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
}
# Calculate execution time and generate comprehensive summary report
$endTime = Get-Date
$executionTime = $endTime - $startTime
Write-Host "========================================================================================================================================================================"
Write-Host "Done."
Write-Host "======================================================================================================================================================================"
Write-Host "📊 AZURE RESOURCE INVENTORY SUMMARY" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
Write-Host "⏰ Execution Time: $($executionTime.ToString('hh\:mm\:ss'))" -ForegroundColor Green
Write-Host "📋 Subscriptions Processed: $processedSubscriptions" -ForegroundColor Green
Write-Host "🔍 Total Resources Discovered: $totalResources" -ForegroundColor Green
Write-Host "🔐 Managed Identities Found: $managedIdentityCount" -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 top resource types
if ($resourceTypes.Count -gt 0) {
Write-Host ""
Write-Host "📈 TOP RESOURCE TYPES:" -ForegroundColor Cyan
$topResourceTypes = $resourceTypes.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 10
foreach ($resourceType in $topResourceTypes) {
Write-Host " $($resourceType.Key): $($resourceType.Value) resources" -ForegroundColor White
}
if ($resourceTypes.Count -gt 10) {
$remainingTypes = $resourceTypes.Count - 10
$remainingResources = ($resourceTypes.GetEnumerator() | Sort-Object Value -Descending | Select-Object -Skip 10 | Measure-Object Value -Sum).Sum
Write-Host " ... and $remainingTypes more types ($remainingResources resources)" -ForegroundColor DarkGray
}
}
# Display governance analysis
if ($governanceIssues.Count -gt 0) {
Write-Host ""
Write-Host "🚨 GOVERNANCE ANALYSIS" -ForegroundColor Red
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "Found $($governanceIssues.Count) resources with missing governance tags:" -ForegroundColor Yellow
foreach ($issue in $governanceIssues | Select-Object -First 15) {
Write-Host " $issue" -ForegroundColor Yellow
}
if ($governanceIssues.Count -gt 15) {
Write-Host " ... and $($governanceIssues.Count - 15) more governance issues (see CSV for complete details)" -ForegroundColor DarkYellow
}
Write-Host ""
Write-Host "📋 Governance Recommendations:" -ForegroundColor Cyan
Write-Host " • Implement mandatory tagging policies for team, product, and environment" -ForegroundColor White
Write-Host " • Use Azure Policy to enforce governance tag compliance" -ForegroundColor White
Write-Host " • Establish resource naming conventions and tagging standards" -ForegroundColor White
Write-Host " • Regular governance audits using this resource inventory" -ForegroundColor White
} else {
Write-Host ""
Write-Host "✅ GOVERNANCE ANALYSIS: All resources have required governance tags" -ForegroundColor Green
}
# Security and identity insights
if ($managedIdentityCount -gt 0) {
$identityPercentage = [math]::Round(($managedIdentityCount / $totalResources) * 100, 1)
Write-Host ""
Write-Host "🔐 SECURITY ANALYSIS:" -ForegroundColor Cyan
Write-Host " Managed Identity Adoption: $identityPercentage% of resources ($managedIdentityCount/$totalResources)" -ForegroundColor Green
Write-Host " 💡 Consider expanding managed identity usage for enhanced security" -ForegroundColor White
}
Write-Host ""
Write-Host "📈 NEXT STEPS:" -ForegroundColor Cyan
Write-Host " 1. Review the generated CSV file for detailed resource analysis" -ForegroundColor White
Write-Host " 2. Address governance tag compliance issues identified above" -ForegroundColor White
Write-Host " 3. Analyze resource distribution across subscriptions and regions" -ForegroundColor White
Write-Host " 4. Consider resource optimization opportunities (unused resources, right-sizing)" -ForegroundColor White
Write-Host " 5. Implement automated resource monitoring and cost management" -ForegroundColor White
Write-Host " 6. Use managed identity information for security auditing" -ForegroundColor White
Write-Host ""
Write-Host "✅ Azure resource 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 Reader permissions on all target subscriptions" -ForegroundColor White
Write-Host " 3. Check that Azure PowerShell modules are installed and up to date" -ForegroundColor White
Write-Host " 4. Verify Managed Identity Operator role for identity information retrieval" -ForegroundColor White
Write-Host " 5. Try running the script with -Verbose for additional diagnostic information" -ForegroundColor White
Write-Host " 6. Consider processing subscriptions individually if encountering timeout issues" -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"
}

View File

@@ -1,10 +1,88 @@
<#
.SYNOPSIS
Generates a comprehensive inventory of Azure Service Bus resources across all management groups and subscriptions.
.DESCRIPTION
This script enumerates all Azure Service Bus namespaces, topics, topic subscriptions, and queues
within active subscriptions across the entire Azure tenant. It provides detailed hierarchical
mapping of Service Bus messaging infrastructure for monitoring, compliance, and governance reporting.
Key capabilities:
- Multi-tenant Service Bus discovery across all management groups
- Hierarchical messaging structure mapping (namespaces → topics → subscriptions, queues)
- Complete Service Bus topology documentation
- Management group and subscription context tracking
- Resource hierarchy analysis for messaging architecture
- Timestamped CSV export for audit trails and capacity planning
The script processes the complete Service Bus messaging estate including:
- Service Bus namespaces (messaging containers)
- Topics (pub/sub messaging endpoints)
- Topic subscriptions (message consumers)
- Queues (point-to-point messaging endpoints)
.PARAMETER None
This script does not accept parameters and will process all Service Bus resources across all accessible namespaces.
.OUTPUTS
CSV File: "<date> azure service bus.csv"
Contains columns for:
- Resource identification (ID, type, location)
- Management hierarchy (management group, subscription, resource group)
- Service Bus namespace information
- Messaging topology (topic names, subscription names, queue names)
.EXAMPLE
.\ServiceBus.ps1
Discovers all Service Bus resources and generates:
"2024-10-30 1435 azure service bus.csv"
.NOTES
File Name : ServiceBus.ps1
Author : Cloud Engineering Team
Prerequisite : Azure PowerShell modules (Az.ServiceBus, Az.Resources, Az.Accounts)
Copyright : (c) 2024 Effectory. All rights reserved.
Version History:
1.0 - Initial release with comprehensive Service Bus inventory functionality
.LINK
https://docs.microsoft.com/en-us/azure/service-bus-messaging/
https://docs.microsoft.com/en-us/powershell/module/az.servicebus/
.COMPONENT
Requires Azure PowerShell modules:
- Az.ServiceBus (for Service Bus namespace, topic, queue, and subscription enumeration)
- Az.Resources (for resource group and management group access)
- Az.Accounts (for authentication and subscription management)
- Az.Automation (for automation context support)
.ROLE
Required Azure permissions:
- Service Bus Data Reader or higher on all Service Bus namespaces
- Management Group Reader for organizational hierarchy access
- Reader access on target subscriptions
.FUNCTIONALITY
- Multi-subscription Service Bus discovery
- Messaging topology analysis and hierarchical mapping
- Complete Service Bus infrastructure documentation
- CSV export with comprehensive messaging architecture details
#>
#Requires -Modules Az.ServiceBus, Az.Resources, Az.Accounts, Az.Automation
#Requires -Version 5.1
[CmdletBinding()]
param()
# Import required modules
Import-Module Az.Accounts
Import-Module Az.Automation
Import-Module Az.ServiceBus
Import-Module Az.Resources
$subscriptions = Get-AzSubscription
class ResourceCheck {
[string] $ResourceId = ""
[string] $ManagementGroupId = ""
@@ -20,109 +98,309 @@ class ResourceCheck {
[string] $QueueName = ""
}
# Initialize script execution
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$startTime = Get-Date
Write-Host "======================================================================================================================================================================"
Write-Host "Creating service bus resource overview."
Write-Host "🚌 AZURE SERVICE BUS INVENTORY GENERATOR" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
Write-Host "⏰ Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Green
Write-Host ""
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure service bus.csv"
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 service bus.csv"
Write-Host "📄 Output file: $fileName" -ForegroundColor Yellow
Write-Host ""
# Initialize counters for progress tracking
$totalNamespaces = 0
$totalTopics = 0
$totalTopicSubscriptions = 0
$totalQueues = 0
$processedManagementGroups = 0
$processedSubscriptions = 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 ""
$managementGroups = Get-AzManagementGroup
foreach ($managementGroup in $managementGroups) {
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "Management group [$($managementGroup.Name)]"
$subscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name | Where-Object State -eq "Active"
foreach ($subscription in $subscriptions) {
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
Write-Host "Subscription [$($subscription.DisplayName) - $subscriptionId]"
Set-AzContext -SubscriptionId $subscriptionId | Out-Null
# 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
$servicebusses = Get-AzServiceBusNamespaceV2
foreach ($servicebus in $servicebusses) {
try {
# Get active subscriptions in this management group
$subscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name | Where-Object State -eq "Active"
Write-Host " 📋 Found $($subscriptions.Count) active subscriptions" -ForegroundColor Green
Write-Host "Getting info for service bus [$($servicebus.Name)]"
[ResourceCheck[]]$Result = @()
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ResourceId = $servicebus.Id
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.ResourceGroup = $servicebus.ResourceGroupName
$resourceCheck.RespourceType = $servicebus.Type
$resourceCheck.Location = $servicebus.Location
$resourceCheck.ServiceBusName = $servicebus.Name
$Result += $resourceCheck
#topics
$topics = Get-AzServiceBusTopic -NamespaceName $servicebus.Name -ResourceGroupName $servicebus.ResourceGroupName
foreach ($topic in $topics) {
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ResourceId = $servicebus.Id
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.ResourceGroup = $servicebus.ResourceGroupName
$resourceCheck.RespourceType = $topic.Type
$resourceCheck.Location = $servicebus.Location
$resourceCheck.ServiceBusName = $servicebus.Name
$resourceCheck.TopicName = $topic.Name
$Result += $resourceCheck
# topic subscriptions
$topicSubs = Get-AzServiceBusSubscription -NamespaceName $servicebus.Name -ResourceGroupName $servicebus.ResourceGroupName -TopicName $topic.Name
foreach ($topicSub in $topicSubs) {
foreach ($subscription in $subscriptions) {
$processedSubscriptions++
Write-Host ""
Write-Host " 🔄 Processing Subscription: $($subscription.DisplayName)" -ForegroundColor Yellow
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ResourceId = $servicebus.Id
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.ResourceGroup = $servicebus.ResourceGroupName
$resourceCheck.RespourceType = $topicSub.Type
$resourceCheck.Location = $servicebus.Location
$resourceCheck.ServiceBusName = $servicebus.Name
$resourceCheck.TopicName = $topic.Name
$resourceCheck.TopicSubscriptionName = $topicSub.Name
$Result += $resourceCheck
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 Service Bus namespaces in the subscription
Write-Host " 🔍 Discovering Service Bus namespaces..." -ForegroundColor Cyan
$servicebusses = Get-AzServiceBusNamespaceV2 -ErrorAction SilentlyContinue
if ($servicebusses.Count -gt 0) {
Write-Host " ✅ Found $($servicebusses.Count) Service Bus namespaces" -ForegroundColor Green
$totalNamespaces += $servicebusses.Count
} else {
Write-Host " No Service Bus namespaces found" -ForegroundColor DarkGray
}
foreach ($servicebus in $servicebusses) {
Write-Host " 📦 Processing namespace: $($servicebus.Name)" -ForegroundColor Yellow
try {
[ResourceCheck[]]$Result = @()
$namespaceTopics = 0
$namespaceTopicSubs = 0
$namespaceQueues = 0
# Add namespace entry
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ResourceId = $servicebus.Id
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.ResourceGroup = $servicebus.ResourceGroupName
$resourceCheck.RespourceType = $servicebus.Type
$resourceCheck.Location = $servicebus.Location
$resourceCheck.ServiceBusName = $servicebus.Name
$Result += $resourceCheck
# Process topics
Write-Host " 🔍 Discovering topics..." -ForegroundColor DarkCyan
try {
$topics = Get-AzServiceBusTopic -NamespaceName $servicebus.Name -ResourceGroupName $servicebus.ResourceGroupName -ErrorAction SilentlyContinue
if ($topics.Count -gt 0) {
Write-Host " 📊 Found $($topics.Count) topics" -ForegroundColor Green
$namespaceTopics = $topics.Count
$totalTopics += $topics.Count
} else {
Write-Host " No topics found" -ForegroundColor DarkGray
}
foreach ($topic in $topics) {
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ResourceId = $servicebus.Id
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.ResourceGroup = $servicebus.ResourceGroupName
$resourceCheck.RespourceType = $topic.Type
$resourceCheck.Location = $servicebus.Location
$resourceCheck.ServiceBusName = $servicebus.Name
$resourceCheck.TopicName = $topic.Name
$Result += $resourceCheck
# Process topic subscriptions
try {
$topicSubs = Get-AzServiceBusSubscription -NamespaceName $servicebus.Name -ResourceGroupName $servicebus.ResourceGroupName -TopicName $topic.Name -ErrorAction SilentlyContinue
foreach ($topicSub in $topicSubs) {
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ResourceId = $servicebus.Id
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.ResourceGroup = $servicebus.ResourceGroupName
$resourceCheck.RespourceType = $topicSub.Type
$resourceCheck.Location = $servicebus.Location
$resourceCheck.ServiceBusName = $servicebus.Name
$resourceCheck.TopicName = $topic.Name
$resourceCheck.TopicSubscriptionName = $topicSub.Name
$Result += $resourceCheck
$namespaceTopicSubs++
$totalTopicSubscriptions++
}
} catch {
Write-Host " ⚠️ Error getting subscriptions for topic '$($topic.Name)': $($_.Exception.Message)" -ForegroundColor Yellow
}
}
} catch {
Write-Host " ❌ Error getting topics: $($_.Exception.Message)" -ForegroundColor Red
}
# Process queues
Write-Host " 🔍 Discovering queues..." -ForegroundColor DarkCyan
try {
$queues = Get-AzServiceBusQueue -NamespaceName $servicebus.Name -ResourceGroupName $servicebus.ResourceGroupName -ErrorAction SilentlyContinue
if ($queues.Count -gt 0) {
Write-Host " 📊 Found $($queues.Count) queues" -ForegroundColor Green
$namespaceQueues = $queues.Count
$totalQueues += $queues.Count
} else {
Write-Host " No queues found" -ForegroundColor DarkGray
}
foreach ($queue in $queues) {
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ResourceId = $servicebus.Id
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.ResourceGroup = $servicebus.ResourceGroupName
$resourceCheck.RespourceType = $queue.Type
$resourceCheck.Location = $servicebus.Location
$resourceCheck.ServiceBusName = $servicebus.Name
$resourceCheck.QueueName = $queue.Name
$Result += $resourceCheck
}
} catch {
Write-Host " ❌ Error getting queues: $($_.Exception.Message)" -ForegroundColor Red
}
# Export results for this namespace
if ($Result.Count -gt 0) {
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
Write-Host " ✅ Exported $($Result.Count) Service Bus resources" -ForegroundColor Green
Write-Host " Topics: $namespaceTopics, Subscriptions: $namespaceTopicSubs, Queues: $namespaceQueues" -ForegroundColor DarkGray
}
} catch {
Write-Host " ❌ Error processing namespace '$($servicebus.Name)': $($_.Exception.Message)" -ForegroundColor Red
}
}
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
}
} catch {
Write-Host " ❌ Error accessing management group: $($_.Exception.Message)" -ForegroundColor Red
}
# queues
$queues = Get-AzServiceBusQueue -NamespaceName $servicebus.Name -ResourceGroupName $servicebus.ResourceGroupName
foreach ($queue in $queues) {
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ResourceId = $servicebus.Id
$resourceCheck.ManagementGroupId = $managementGroup.Id
$resourceCheck.ManagementGroupName = $managementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscription.Id
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.ResourceGroup = $servicebus.ResourceGroupName
$resourceCheck.RespourceType = $queue.Type
$resourceCheck.Location = $servicebus.Location
$resourceCheck.ServiceBusName = $servicebus.Name
$resourceCheck.QueueName = $queue.Name
$Result += $resourceCheck
}
Write-Host ""
}
# Calculate execution time and generate comprehensive summary report
$endTime = Get-Date
$executionTime = $endTime - $startTime
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
Write-Host "======================================================================================================================================================================"
Write-Host "📊 AZURE SERVICE BUS 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 "🚌 Service Bus Namespaces: $totalNamespaces" -ForegroundColor Green
Write-Host "📡 Topics Discovered: $totalTopics" -ForegroundColor Cyan
Write-Host "📨 Topic Subscriptions: $totalTopicSubscriptions" -ForegroundColor Yellow
Write-Host "📬 Queues Discovered: $totalQueues" -ForegroundColor Magenta
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
}
# Calculate messaging topology insights
$totalMessagingEndpoints = $totalTopics + $totalQueues
$averageTopicsPerNamespace = if ($totalNamespaces -gt 0) { [math]::Round($totalTopics / $totalNamespaces, 1) } else { 0 }
$averageQueuesPerNamespace = if ($totalNamespaces -gt 0) { [math]::Round($totalQueues / $totalNamespaces, 1) } else { 0 }
$averageSubsPerTopic = if ($totalTopics -gt 0) { [math]::Round($totalTopicSubscriptions / $totalTopics, 1) } else { 0 }
if ($totalNamespaces -gt 0) {
Write-Host ""
Write-Host "📈 MESSAGING TOPOLOGY ANALYSIS:" -ForegroundColor Cyan
Write-Host " Total Messaging Endpoints: $totalMessagingEndpoints (Topics + Queues)" -ForegroundColor White
Write-Host " Average Topics per Namespace: $averageTopicsPerNamespace" -ForegroundColor White
Write-Host " Average Queues per Namespace: $averageQueuesPerNamespace" -ForegroundColor White
if ($totalTopics -gt 0) {
Write-Host " Average Subscriptions per Topic: $averageSubsPerTopic" -ForegroundColor White
}
# Provide architecture insights
Write-Host ""
Write-Host "🏗️ ARCHITECTURE INSIGHTS:" -ForegroundColor Cyan
if ($totalTopics -gt $totalQueues) {
Write-Host " 📡 Pub/Sub Pattern Dominant: More topics ($totalTopics) than queues ($totalQueues)" -ForegroundColor Green
Write-Host " This indicates a preference for broadcast messaging patterns" -ForegroundColor White
} elseif ($totalQueues -gt $totalTopics) {
Write-Host " 📬 Point-to-Point Pattern Dominant: More queues ($totalQueues) than topics ($totalTopics)" -ForegroundColor Green
Write-Host " This indicates a preference for direct messaging patterns" -ForegroundColor White
} else {
Write-Host " ⚖️ Balanced Architecture: Equal distribution of topics and queues" -ForegroundColor Green
Write-Host " This indicates a mixed messaging architecture approach" -ForegroundColor White
}
if ($totalTopicSubscriptions -gt ($totalTopics * 2)) {
Write-Host " 🔄 High Fan-out: Multiple consumers per topic (avg: $averageSubsPerTopic subscribers)" -ForegroundColor Yellow
Write-Host " Consider monitoring subscription performance and message distribution" -ForegroundColor White
}
}
}
Write-Host "======================================================================================================================================================================"
Write-Host "Done."
Write-Host ""
Write-Host "📈 NEXT STEPS:" -ForegroundColor Cyan
Write-Host " 1. Review the generated CSV file for detailed Service Bus topology" -ForegroundColor White
Write-Host " 2. Analyze messaging patterns and identify optimization opportunities" -ForegroundColor White
Write-Host " 3. Monitor Service Bus performance metrics and throughput" -ForegroundColor White
Write-Host " 4. Consider namespace consolidation for cost optimization" -ForegroundColor White
Write-Host " 5. Implement message monitoring and alerting for critical endpoints" -ForegroundColor White
Write-Host " 6. Review security settings and access policies for each namespace" -ForegroundColor White
Write-Host ""
Write-Host "✅ Azure Service Bus 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 Service Bus Data Reader permissions on target namespaces" -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 Service Bus namespaces are accessible and not deleted" -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"
}

View File

@@ -1,35 +1,164 @@
#Connect-AzAccount
<#
.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: "<date> 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 = ""
)
$access_token = (Get-AzAccessToken -TenantId "e9792fd7-4044-47e7-a40d-3fba46f1cd09").Token
$url = ""
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"
}
# GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Web/sites/{name}/slots/{slot}/deploymentStatus/{deploymentStatusId}?api-version=2022-03-01
$head = @{ Authorization =" Bearer $access_token" }
$response = Invoke-RestMethod -Uri $url -Method GET -Headers $head
$response | ForEach-Object {
$responseValue = $_.value
if ($responseValue.Length -gt 0) {
return $responseValue[0].properties.last_success_end_time
}
else {
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 ""
}
}
@@ -61,119 +190,356 @@ class ResourceCheck {
[string] $LastDeployDate = ""
}
# Initialize script execution
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$startTime = Get-Date
Write-Host "======================================================================================================================================================================"
Write-Host "Creating webapp resource overview."
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 ""
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_webapps.csv"
$managementGroups = Get-AzManagementGroup
foreach ($managementGroup in $managementGroups)
{
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "Management group [$($managementGroup.Name)]"
$subscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name | Where-Object State -eq "Active" | Where-Object DisplayName -NotLike "Visual Studio*"
foreach ($subscription in $subscriptions)
{
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
Write-Host "Subscription [$($subscription.DisplayName) - $subscriptionId]"
Set-AzContext -SubscriptionId $subscriptionId | Out-Null
# 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
$allResourceGroups = Get-AzResourceGroup
[ResourceCheck[]]$Result = @()
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 ($group in $allResourceGroups) {
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
$allWebApps = Get-AzWebApp -ResourceGroupName $group.ResourceGroupName
foreach ($webApp in $allWebApps) {
# Get all resource groups in the subscription
$allResourceGroups = Get-AzResourceGroup
[ResourceCheck[]]$Result = @()
$subscriptionWebApps = 0
$subscriptionSlots = 0
Write-Host $webApp.Name
foreach ($group in $allResourceGroups) {
Write-Host " 📁 Resource Group: $($group.ResourceGroupName)" -ForegroundColor DarkCyan -NoNewline
[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
$resourceCheck.LastDeployDate = GetDeployment -siteName $webApp.Name -resourceGroupName $group.ResourceGroupName -subscriptionId $subscriptionId
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
$Result += $resourceCheck
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)"
}
$allSlots = Get-AzWebAppSlot -Name $webApp.Name -ResourceGroupName $webApp.ResourceGroup
# 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
foreach ($slotTemp in $allSlots) {
# Get deployment information with error handling
$deploymentDate = GetDeployment -siteName $webApp.Name -resourceGroupName $group.ResourceGroupName -subscriptionId $subscriptionId
if ([string]::IsNullOrEmpty($deploymentDate)) {
$deploymentTrackingErrors++
}
$resourceCheck.LastDeployDate = $deploymentDate
Write-Host $slotTemp.Name
[string] $slotName = $slotTemp.Name.Split("/")[1]
$slot = Get-AzWebAppSlot -Name $webApp.Name -ResourceGroupName $webApp.ResourceGroup -Slot $slotName
$Result += $resourceCheck
$totalWebApps++
[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
$resourceCheck.LastDeployDate = GetDeployment -siteName $webApp.Name -resourceGroupName $group.ResourceGroupName -subscriptionId $subscriptionId -slotName $slotName
# Process deployment slots
try {
$allSlots = Get-AzWebAppSlot -Name $webApp.Name -ResourceGroupName $webApp.ResourceGroup -ErrorAction SilentlyContinue
$Result += $resourceCheck
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
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
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"
}
Write-Host "======================================================================================================================================================================"
Write-Host "Done."