mirror of
https://dev.azure.com/effectory/Survey%20Software/_git/Cloud%20Engineering
synced 2026-02-27 18:52:18 +01:00
added documetation
This commit is contained in:
@@ -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
|
||||
@@ -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 "========================================================================================================================================================================"
|
||||
@@ -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."
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 "======================================================================================================================================================================"
|
||||
@@ -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 "======================================================================================================================================================================"
|
||||
@@ -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 "======================================================================================================================================================================"
|
||||
@@ -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 $_
|
||||
}
|
||||
@@ -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 "======================================================================================================================================================================"
|
||||
|
||||
|
||||
@@ -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 "======================================================================================================================================================================"
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
||||
@@ -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."
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user