added documetation

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

View File

@@ -1,27 +1,146 @@
<#
.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"
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"
$head = @{ Authorization =" Bearer $access_token" }
# 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 = $_
$alert.properties.actionGroups
| ForEach-Object {
# 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
@@ -36,70 +155,140 @@ function GetSmartDetectorActionGroupIds {
}
}
}
}
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"
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)"
if (($null -eq $scheduledQueryRule.ActionGroup) -or ($scheduledQueryRule.ActionGroup.Length -eq 0))
{
foreach ($scheduledQueryRule in $scheduledQueryRules) {
# Get corresponding resource for tag information
$resource = $scheduledQueryRulesResources | Where-Object { $_.id -eq $scheduledQueryRule.Id }
# 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,21 +375,27 @@ 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
@@ -196,32 +406,40 @@ foreach ($subscription in $subscriptions)
$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) {
Write-Host " Found $($activityLogAlerts.Count) Activity Log Alert Rule(s)"
if (($null -eq $activityLogAlert.ActionGroup) -or ($activityLogAlert.ActionGroup.Length -eq 0))
{
foreach ($activityLogAlert in $activityLogAlerts) {
# Handle Activity Log Alerts without Action Groups
if (($null -eq $activityLogAlert.ActionGroup) -or ($activityLogAlert.ActionGroup.Length -eq 0)) {
# Create alert rule entry without Action Group details
[AlertRule] $AlertRule = [AlertRule]::new()
$AlertRule.SubscriptionId = $subscription.Id
$AlertRule.SubscriptionName = $subscription.Name
@@ -293,6 +523,8 @@ foreach ($subscription in $subscriptions)
$AlertRule.ResourceGroupName = $activityLogAlert.ResourceGroupName
$AlertRule.Description = GetDecentDescription $activityLogAlert.Description
$AlertRule.State = $activityLogAlert.Enabled -eq $true ? "Enabled" : "Disabled"
# Extract governance tags
$AlertRule.Tag_Team = $activityLogAlert.Tags.team
$AlertRule.Tag_Product = $activityLogAlert.Tags.product
$AlertRule.Tag_Environment = $activityLogAlert.Tags.environment
@@ -303,11 +535,15 @@ foreach ($subscription in $subscriptions)
$Result += $AlertRule
}
else {
foreach($action in $activityLogAlert.ActionGroup) {
# Process Activity Log Alerts with Action Groups
foreach ($action in $activityLogAlert.ActionGroup) {
# Create alert rule entry with Action Group details
[AlertRule] $AlertRule = [AlertRule]::new()
$actionGroup = $actionGroups | where { $_.id -eq [uri]::UnescapeDataString($action.Id) }
# Find corresponding Action Group from pre-loaded collection
$actionGroup = $actionGroups | Where-Object { $_.id -eq [uri]::UnescapeDataString($action.Id) }
# Populate basic alert rule information
$AlertRule.SubscriptionId = $subscription.Id
$AlertRule.SubscriptionName = $subscription.Name
$AlertRule.Id = $activityLogAlert.Id
@@ -318,15 +554,19 @@ foreach ($subscription in $subscriptions)
$AlertRule.State = $activityLogAlert.Enabled -eq $true ? "Enabled" : "Disabled"
$AlertRule.ActionGroupId = $action.Id
# Populate Action Group details if found
if ($null -ne $actionGroup) {
$AlertRule.ActionGroupName = $actionGroup.Name
$AlertRule.ActionGroupResourceGroupName = $actionGroup.ResourceGroupName
$AlertRule.ActionGroupEnabled = $actionGroup.Enabled
# Extract receiver information (convert arrays to comma-separated strings)
$AlertRule.ActionGroupArmRoleReceivers = [string] ( $actionGroup.ArmRoleReceivers | ForEach-Object { $_.Name } )
$AlertRule.ActionGroupEmailReceivers = [string] ( $actionGroup.EmailReceivers | ForEach-Object { $_.EmailAddress } )
$AlertRule.AzureFunctionReceivers = [string] ($actionGroup.AzureFunctionReceivers | ForEach-Object { $_.FunctionName } )
}
# Extract governance tags
$AlertRule.Tag_Team = $activityLogAlert.Tags.team
$AlertRule.Tag_Product = $activityLogAlert.Tags.product
$AlertRule.Tag_Environment = $activityLogAlert.Tags.environment
@@ -338,8 +578,54 @@ foreach ($subscription in $subscriptions)
}
}
}
Write-Host "Completed processing subscription: $($subscription.Name)"
Write-Host ""
}
# Export results and display summary
Write-Host "======================================================================================================================================================================"
Write-Host "Exporting results and generating summary..."
Write-Host "======================================================================================================================================================================"
# Export comprehensive alert rules data to CSV
$Result | Export-Csv -Path $fileName -NoTypeInformation -Force
$Result | ft
# Generate summary statistics
$summaryStats = @{
TotalAlertRules = $Result.Count
SmartDetectorRules = ($Result | Where-Object { $_.Type -eq "microsoft.alertsmanagement/smartdetectoralertrules" }).Count
ScheduledQueryRules = ($Result | Where-Object { $_.Type -eq "microsoft.insights/scheduledqueryrules" }).Count
MetricAlerts = ($Result | Where-Object { $_.Type -eq "Microsoft.Insights/metricAlerts" }).Count
ActivityLogAlerts = ($Result | Where-Object { $_.Type -eq "Microsoft.Insights/ActivityLogAlerts" }).Count
EnabledRules = ($Result | Where-Object { $_.State -eq "Enabled" }).Count
DisabledRules = ($Result | Where-Object { $_.State -eq "Disabled" }).Count
RulesWithActionGroups = ($Result | Where-Object { $_.ActionGroupId -ne "" }).Count
RulesWithoutActionGroups = ($Result | Where-Object { $_.ActionGroupId -eq "" }).Count
}
Write-Host "Alert Rules Inventory Summary:"
Write-Host "==============================="
Write-Host "Total Alert Rules Found: $($summaryStats.TotalAlertRules)"
Write-Host ""
Write-Host "By Alert Type:"
Write-Host " Smart Detector Rules: $($summaryStats.SmartDetectorRules)"
Write-Host " Scheduled Query Rules (Log Analytics): $($summaryStats.ScheduledQueryRules)"
Write-Host " Metric Alert Rules: $($summaryStats.MetricAlerts)"
Write-Host " Activity Log Alert Rules: $($summaryStats.ActivityLogAlerts)"
Write-Host ""
Write-Host "By State:"
Write-Host " Enabled Rules: $($summaryStats.EnabledRules)"
Write-Host " Disabled Rules: $($summaryStats.DisabledRules)"
Write-Host ""
Write-Host "By Action Group Association:"
Write-Host " Rules with Action Groups: $($summaryStats.RulesWithActionGroups)"
Write-Host " Rules without Action Groups: $($summaryStats.RulesWithoutActionGroups)"
Write-Host ""
Write-Host "Results exported to: $fileName"
Write-Host "======================================================================================================================================================================"
# Display formatted table output to console
Write-Host ""
Write-Host "Detailed Alert Rules (displaying first 50 rows):"
$Result | Select-Object -First 50 | Format-Table -AutoSize

View File

@@ -1,36 +1,186 @@
#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
$subscriptions = Get-AzSubscription | Where-Object State -eq "Enabled"
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
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 SubscriptionFilter
Optional array of subscription IDs to analyze. If not specified, all enabled subscriptions are processed.
.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)
{
# 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
# 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
@@ -38,8 +188,18 @@ foreach ($subscription in $subscriptions)
$AppInsightsCheck.ResourceGroupName = $appinsights.ResourceGroupName
$AppInsightsCheck.WorkspaceResourceId = $appinsights.WorkspaceResourceId
$resource = Get-AzResource -ResourceId $appinsights.Id
# 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
@@ -47,10 +207,82 @@ foreach ($subscription in $subscriptions)
$AppInsightsCheck.Tag_CreatedOnDate = $resource.Tags.CreatedOnDate
$AppInsightsCheck.Tag_Deployment = $resource.Tags.drp_deployment
# Report on tag compliance
$tagCount = @($AppInsightsCheck.Tag_Team, $AppInsightsCheck.Tag_Product, $AppInsightsCheck.Tag_Environment) | Where-Object { $_ } | Measure-Object | Select-Object -ExpandProperty Count
if ($tagCount -eq 3) {
Write-Host " All required tags present" -ForegroundColor Green
} else {
Write-Host " Missing required tags (team, product, environment)" -ForegroundColor Yellow
}
# Add to results
$Result += $AppInsightsCheck
$totalResourcesProcessed++
} catch {
Write-Host " ERROR processing resource: $($_.Exception.Message)" -ForegroundColor Red
# Still add basic info even if tag retrieval fails
[AppInsightsCheck] $AppInsightsCheck = [AppInsightsCheck]::new()
$AppInsightsCheck.SubscriptionId = $subscription.Id
$AppInsightsCheck.SubscriptionName = $subscription.Name
$AppInsightsCheck.Id = $appinsights.Id
$AppInsightsCheck.Name = $appinsights.Name
$AppInsightsCheck.ResourceGroupName = $appinsights.ResourceGroupName
$AppInsightsCheck.WorkspaceResourceId = $appinsights.WorkspaceResourceId
$Result += $AppInsightsCheck
$totalResourcesProcessed++
}
}
} catch {
Write-Host " ERROR accessing subscription: $($_.Exception.Message)" -ForegroundColor Red
continue
}
}
$Result | Export-Csv -Path $fileName -NoTypeInformation -Force
# Export results to CSV file
Write-Host "`nExporting results to: $OutputPath" -ForegroundColor Yellow
$Result | Export-Csv -Path $OutputPath -NoTypeInformation -Force
$Result | ft
# Calculate and display summary statistics
$totalSubscriptions = $subscriptions.Count
$workspaceBasedCount = ($Result | Where-Object { $_.WorkspaceResourceId -ne "" }).Count
$legacyCount = ($Result | Where-Object { $_.WorkspaceResourceId -eq "" }).Count
$taggedResourcesCount = ($Result | Where-Object { $_.Tag_Team -ne "" -and $_.Tag_Product -ne "" -and $_.Tag_Environment -ne "" }).Count
# Display completion summary
Write-Host "`n========================================================================================================================================================================"
Write-Host "APPLICATION INSIGHTS ANALYSIS COMPLETED SUCCESSFULLY!" -ForegroundColor Green
Write-Host "========================================================================================================================================================================"
Write-Host ""
Write-Host "SUMMARY STATISTICS:" -ForegroundColor Cyan
Write-Host "Subscriptions analyzed: $totalSubscriptions" -ForegroundColor Yellow
Write-Host "Total Application Insights resources: $totalResourcesProcessed" -ForegroundColor Yellow
Write-Host "Workspace-based resources: $workspaceBasedCount" -ForegroundColor Yellow
Write-Host "Legacy standalone resources: $legacyCount" -ForegroundColor Yellow
Write-Host "Resources with complete tags (team, product, environment): $taggedResourcesCount" -ForegroundColor Yellow
# Highlight areas needing attention
if ($legacyCount -gt 0) {
Write-Host ""
Write-Host "RECOMMENDATIONS:" -ForegroundColor Cyan
Write-Host "- $legacyCount legacy Application Insights resources should be migrated to workspace-based mode" -ForegroundColor Yellow
}
if ($taggedResourcesCount -lt $totalResourcesProcessed) {
$untaggedCount = $totalResourcesProcessed - $taggedResourcesCount
Write-Host "- $untaggedCount resources are missing required governance tags" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Output file: $OutputPath" -ForegroundColor Yellow
# Display results table
Write-Host ""
Write-Host "DETAILED RESULTS:" -ForegroundColor Cyan
$Result | Format-Table -Property SubscriptionName, Name, ResourceGroupName, @{
Name = 'WorkspaceAssociated'
Expression = { if ($_.WorkspaceResourceId) { 'Yes' } else { 'No' } }
}, Tag_Team, Tag_Product, Tag_Environment -AutoSize
Write-Host "========================================================================================================================================================================"

View File

@@ -1,72 +1,192 @@
#Connect-AzAccount
<#
.SYNOPSIS
Exports Azure Privileged Identity Management (PIM) role eligible assignments across all management groups, subscriptions, resource groups, and resources.
.DESCRIPTION
This script comprehensively inventories all PIM eligible role assignments across the entire Azure environment hierarchy.
It traverses management groups, subscriptions, resource groups, and individual resources to collect detailed information
about role eligibility schedules. The script uses Azure REST API calls to retrieve PIM data and exports results to CSV.
The script performs a complete audit of:
- Management Group level PIM assignments
- Subscription level PIM assignments
- Resource Group level PIM assignments
- Individual Resource level PIM assignments
All data is consolidated into a single CSV file with detailed scope and principal information.
.PARAMETER None
This script does not accept parameters. It processes all accessible management groups and their child resources.
.OUTPUTS
CSV file named with timestamp pattern: "yyyy-MM-dd HHmm azure_pim_assignments.csv"
Contains columns for scope hierarchy, role definitions, principals, and assignment metadata.
.EXAMPLE
.\AzurePIM.ps1
Exports all PIM eligible assignments to a timestamped CSV file in the current directory.
.NOTES
Author: Cloud Engineering Team
Version: 1.0
Created: 2024
Prerequisites:
- Azure PowerShell module (Az) must be installed
- User must be authenticated (Connect-AzAccount)
- Requires appropriate permissions to read PIM assignments across all scopes
- Tenant ID is hardcoded and may need adjustment for different environments
Security Considerations:
- Script uses Azure access tokens for REST API authentication
- Requires elevated permissions to access PIM data across all scopes
- Output file contains sensitive role assignment information
Performance Notes:
- Processing time varies significantly based on environment size
- Script processes resources sequentially which may take considerable time
- Consider running during off-peak hours for large environments
.LINK
https://docs.microsoft.com/en-us/azure/active-directory/privileged-identity-management/
https://docs.microsoft.com/en-us/rest/api/authorization/roleeligibilityscheduleinstances
#>
#Requires -Modules Az
#Connect-AzAccount
# Class definition for structured PIM assignment data
class ResourceCheck {
[string] $Level = ""
[string] $ManagementGroupId = ""
[string] $ManagementGroupName = ""
[string] $SubscriptionId = ""
[string] $SubscriptionName = ""
[string] $ResourceId = ""
[string] $ResourceGroup = ""
[string] $ResourceName = ""
[string] $ResourceType = ""
[string] $RoleEligibilityScheduleId = ""
[string] $Scope = ""
[string] $RoleDefinitionId = ""
[string] $RoleDefinitionName = ""
[string] $RoleDefinitionType = ""
[string] $PrincipalId = ""
[string] $PrincipalName = ""
[string] $PrincipalType = ""
[string] $Status = ""
[string] $StartDateTime = ""
[string] $EndDateTime = ""
[string] $CreatedOn = ""
# Hierarchy level indicators
[string] $Level = "" # Management Group, Subscription, Resource Group, or Resource
[string] $ManagementGroupId = "" # Management group identifier
[string] $ManagementGroupName = "" # Management group display name
[string] $SubscriptionId = "" # Azure subscription GUID
[string] $SubscriptionName = "" # Subscription display name
# Resource identification
[string] $ResourceId = "" # Full Azure resource identifier
[string] $ResourceGroup = "" # Resource group name
[string] $ResourceName = "" # Individual resource name
[string] $ResourceType = "" # Azure resource type
# PIM assignment details
[string] $RoleEligibilityScheduleId = "" # Unique PIM schedule identifier
[string] $Scope = "" # Assignment scope path
[string] $RoleDefinitionId = "" # Azure RBAC role definition ID
[string] $RoleDefinitionName = "" # Human-readable role name
[string] $RoleDefinitionType = "" # Role definition type
# Principal (user/group/service principal) information
[string] $PrincipalId = "" # Principal object ID
[string] $PrincipalName = "" # Principal display name
[string] $PrincipalType = "" # User, Group, or ServicePrincipal
# Assignment metadata
[string] $Status = "" # Assignment status (Active, Eligible, etc.)
[string] $StartDateTime = "" # Assignment start date/time
[string] $EndDateTime = "" # Assignment expiration date/time
[string] $CreatedOn = "" # Assignment creation timestamp
}
function GetEligibleAssignments {
<#
.SYNOPSIS
Retrieves PIM eligible role assignments for a specified scope using Azure Management REST API.
.DESCRIPTION
This function queries the Azure Management REST API to retrieve role eligibility schedule instances
for a given scope. It handles authentication using Azure access tokens and filters results to
exclude inherited assignments.
.PARAMETER scope
The Azure resource scope path to query for PIM assignments. Can be management group, subscription,
resource group, or individual resource scope.
.OUTPUTS
Returns an array of PIM assignment objects with properties containing assignment details,
or empty string if no assignments found.
.EXAMPLE
GetEligibleAssignments -scope "subscriptions/12345678-1234-1234-1234-123456789012"
Retrieves all PIM eligible assignments for the specified subscription.
.NOTES
- Uses hardcoded tenant ID which may need adjustment for different environments
- REST API version 2020-10-01 is used for compatibility
- Filters out inherited assignments to show only direct assignments
#>
function GetEligibleAssignments {
param (
[Parameter(Mandatory = $true)]
[string] $scope
)
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))
# 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()"
$head = @{ Authorization =" Bearer $access_token" }
# 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 ""
}
}
}
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"
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
}
# 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
}
# Export subscription assignments to CSV
if ($Result.Count -gt 0) {
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
Write-Host " Exported $($Result.Count) subscription assignment(s)"
}
# Retrieve all resource groups in the current subscription
$allResourceGroups = Get-AzResourceGroup
Write-Host " Found $($allResourceGroups.Count) resource group(s) to process"
# Process each resource group for PIM assignments
foreach ($group in $allResourceGroups) {
Write-Host " Processing Resource Group: $($group.ResourceGroupName)"
Write-Host $group.ResourceGroupName
# Retrieve PIM assignments at resource group level
$assignments = GetEligibleAssignments -scope $group.ResourceId
# Process resource group level assignments
[ResourceCheck[]]$Result = @()
foreach ($assignment in $assignments) {
# Create structured object for resource group assignment
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level = "Resource Group"
$resourceCheck.ManagementGroupId = $managementGroup.Id
@@ -156,16 +300,26 @@ foreach ($managementGroup in $managementGroups)
$resourceCheck.CreatedOn = $assignment.createdOn
$Result += $resourceCheck
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
# Export resource group assignments to CSV
if ($Result.Count -gt 0) {
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
Write-Host " Exported $($Result.Count) resource group assignment(s)"
}
# Retrieve all resources within the current resource group
$allResources = Get-AzResource -ResourceGroupName $group.ResourceGroupName
# Process each individual resource for PIM assignments
foreach ($resource in $allResources)
{
# Retrieve PIM assignments at individual resource level
$assignments = GetEligibleAssignments -scope $resource.ResourceId
# Process individual resource level assignments
[ResourceCheck[]]$Result = @()
foreach ($assignment in $assignments) {
# Create structured object for resource assignment
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level = "Resource"
$resourceCheck.ManagementGroupId = $managementGroup.Id
@@ -190,13 +344,27 @@ foreach ($managementGroup in $managementGroups)
$resourceCheck.CreatedOn = $assignment.createdOn
$Result += $resourceCheck
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
# Export individual resource assignments to CSV
if ($Result.Count -gt 0) {
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
Write-Host " Exported $($Result.Count) assignment(s) for resource: $($resource.Name)"
}
}
}
}
}
# Script completion summary
Write-Host "======================================================================================================================================================================"
Write-Host "PIM assignments export completed successfully."
Write-Host "Results saved to: $fileName"
Write-Host ""
Write-Host "Summary:"
Write-Host "- Processed $($managementGroups.Count) management group(s)"
Write-Host "- Traversed all subscriptions, resource groups, and individual resources"
Write-Host "- Exported all PIM eligible role assignments to CSV format"
Write-Host ""
Write-Host "Note: Review the CSV file for comprehensive PIM assignment details across all Azure scopes."
Write-Host "======================================================================================================================================================================"
Write-Host "Done."

View File

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

View File

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

View File

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

View File

@@ -1,78 +1,357 @@
#Connect-AzAccount
<#
.SYNOPSIS
Inventories and monitors Azure App Service certificates across all enabled subscriptions.
$fileName = ".\2020-12-23 azure_appservice_certificates (3).csv"
.DESCRIPTION
This script performs a comprehensive audit of all Azure App Service certificates across all
enabled subscriptions in your Azure tenant. It extracts certificate details including expiration
dates, thumbprints, subject names, and calculates the remaining days until expiration.
$subscriptions = Get-AzSubscription | Where-Object State -eq "Enabled"
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
.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)
{
# 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"
# 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
{
try {
$thumbprint = $certificateCheck.ThumbPrint
$certificate = Get-AzWebAppCertificate -ResourceGroupName $certificateCheck.ResourceGroupName -Thumbprint $thumbprint -debug -verbose
# Retrieve detailed certificate information
$certificate = Get-AzWebAppCertificate -ResourceGroupName $certificateCheck.ResourceGroupName -Thumbprint $thumbprint -ErrorAction Stop
if ($null -eq $certificate)
{
$certificateCheck.Comment = "Could not find certificate"
}
else
{
try
{
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 name: $subjectname"
Write-Host "Subject: $subjectname"
$EndDate=[datetime]$certificate.ExpirationDate
# Calculate expiration and days remaining
$EndDate = [datetime]$certificate.ExpirationDate
$certificateCheck.ExpirationDate = $EndDate
$span = NEW-TIMESPAN Start $StartDate End $EndDate
$certificateCheck.TotalDays = $span.TotalDays
$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.Comment = "Could not find expiry for certificate"
} catch {
$certificateCheck.Health = "Error"
$certificateCheck.Comment = "Could not determine expiration date"
$certificatesWithIssues++
Write-Host " ⚠ Warning: Could not determine expiration date"
}
}
}
catch
{
$certificateCheck.Comment = "Could not load certificate"
} 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"
}
$processedSubscriptions++
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Please verify permissions and subscription access"
}
}
$Result | Export-Csv -Path $fileName -NoTypeInformation -Force
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "Exporting Results and Analysis"
Write-Host "======================================================================================================================================================================"
$Result | ft
# Export results to CSV file
Write-Host "Exporting certificate inventory to CSV file..."
try {
$Result | Export-Csv -Path $fileName -NoTypeInformation -Force
Write-Host "✓ Successfully exported $($Result.Count) certificate records to: $fileName"
} catch {
Write-Error "Failed to export results to CSV file: $($_.Exception.Message)"
throw $_
}
# Display results summary table
Write-Host ""
Write-Host "Certificate Inventory Summary:"
Write-Host "======================================================================================================================================================================"
$Result | Format-Table -AutoSize
# Generate detailed analysis and statistics
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "Certificate Analysis Summary"
Write-Host "======================================================================================================================================================================"
Write-Host "Execution completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host ""
Write-Host "Processing Statistics:"
Write-Host " Subscriptions processed: $processedSubscriptions"
Write-Host " Total certificates discovered: $totalCertificates"
Write-Host " Certificates with issues: $certificatesWithIssues"
Write-Host ""
# Analyze certificate expiration status using Health property
if ($Result.Count -gt 0) {
$expiredCerts = $Result | Where-Object { $_.Health -eq "Expired" }
$criticalCerts = $Result | Where-Object { $_.Health -eq "Critical" }
$warnCerts = $Result | Where-Object { $_.Health -eq "Warning" }
$healthyCerts = $Result | Where-Object { $_.Health -eq "Healthy" }
$errorCerts = $Result | Where-Object { $_.Health -eq "Error" }
Write-Host "Certificate Status Analysis:"
Write-Host " 🔴 Expired certificates: $($expiredCerts.Count)"
Write-Host " 🟠 Critical (< 7 days): $($criticalCerts.Count)"
Write-Host " 🟡 Warning (7-30 days): $($warnCerts.Count)"
Write-Host " ✓ Healthy (> 30 days): $($healthyCerts.Count)"
Write-Host " ❌ Error/Inaccessible: $($errorCerts.Count)"
Write-Host ""
# Display urgent action items
if ($expiredCerts.Count -gt 0 -or $criticalCerts.Count -gt 0) {
Write-Host "🚨 URGENT ACTION REQUIRED:"
if ($expiredCerts.Count -gt 0) {
Write-Host " - $($expiredCerts.Count) certificate(s) have already expired"
}
if ($criticalCerts.Count -gt 0) {
Write-Host " - $($criticalCerts.Count) certificate(s) expire within 7 days"
}
Write-Host " Review the CSV file for detailed certificate information"
}
if ($warnCerts.Count -gt 0) {
Write-Host "⚠ RENEWAL PLANNING NEEDED:"
Write-Host " - $($warnCerts.Count) certificate(s) expire within 30 days"
}
} else {
Write-Host " No certificates found across all processed subscriptions"
}
Write-Host ""
Write-Host "Output File Information:"
Write-Host " File Path: $fileName"
Write-Host " File Size: $([Math]::Round((Get-Item $fileName).Length / 1KB, 2)) KB"
Write-Host ""
Write-Host "Recommendations:"
Write-Host " - Schedule regular execution for proactive certificate monitoring"
Write-Host " - Set up alerts for certificates expiring within 30 days"
Write-Host " - Implement automated renewal processes where possible"
Write-Host " - Review and resolve any certificates with error status"
Write-Host "======================================================================================================================================================================"

View File

@@ -1,56 +1,328 @@
# .\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
)
# 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) {
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 ""
# Connect to Azure if not already connected
# Ensure Azure authentication
Write-Host "Verifying Azure authentication..."
if (-not (Get-AzContext)) {
Connect-AzAccount
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"
}
# Select subscription if provided
# Set target subscription if provided
if ($SubscriptionId) {
Select-AzSubscription -SubscriptionId $SubscriptionId
Write-Host "Selected subscription: $SubscriptionId" -ForegroundColor Yellow
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
}
# Get all endpoints
$endpoints = Get-AzFrontDoorCdnEndpoint -ResourceGroupName $ResourceGroupName -ProfileName $FrontDoorName
Write-Host "✓ Successfully accessed Front Door profile: $($frontDoor.Name)"
Write-Host " Profile Type: $($frontDoor.Sku.Name)"
Write-Host " Profile State: $($frontDoor.FrontDoorId)"
Write-Host ""
$routeData = @()
# Get all endpoints for the Front Door profile
Write-Host "Discovering Front Door endpoints..."
$endpoints = Get-AzFrontDoorCdnEndpoint -ResourceGroupName $ResourceGroupName -ProfileName $FrontDoorName -ErrorAction Stop
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
}
Write-Host "✓ Found $($endpoints.Count) endpoint(s):"
foreach ($endpoint in $endpoints) {
# Get routes for each endpoint
$routes = Get-AzFrontDoorCdnRoute -ResourceGroupName $ResourceGroupName -ProfileName $FrontDoorName -EndpointName $endpoint.Name
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) {
# Get origin group details
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]
$origins = Get-AzFrontDoorCdnOrigin -ResourceGroupName $ResourceGroupName -ProfileName $FrontDoorName -OriginGroupName $originGroupName
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
@@ -64,14 +336,126 @@ try {
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
}
# Export to CSV
Write-Host "Exporting Front Door routes to: $fileName" -ForegroundColor Green
Write-Host ""
}
Write-Host "======================================================================================================================================================================"
Write-Host "Exporting Results and Analysis"
Write-Host "======================================================================================================================================================================"
# Export route configuration to CSV file
Write-Host "Exporting Front Door route configuration to CSV..."
try {
$routeData | Export-Csv -Path $fileName -NoTypeInformation -Force
Write-Host "✓ Successfully exported $($routeData.Count) route entries to: $fileName" -ForegroundColor Green
} catch {
Write-Error "Failed to export route data to CSV: $($_.Exception.Message)"
throw $_
}
# Display configuration summary table
Write-Host ""
Write-Host "Front Door Route Configuration Summary:"
Write-Host "======================================================================================================================================================================"
$routeData | Format-Table -Property FrontDoorName, EndpointName, RouteName, RoutePatterns, OriginGroupName, OriginName, RouteEnabled, OriginEnabled -AutoSize
# Generate detailed analysis and statistics
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "Configuration Analysis Summary"
Write-Host "======================================================================================================================================================================"
Write-Host "Export completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host ""
Write-Host "Front Door Statistics:"
Write-Host " Profile Name: $FrontDoorName"
Write-Host " Profile Type: $($frontDoor.Sku.Name)"
Write-Host " Total Endpoints: $($endpoints.Count)"
Write-Host " Total Routes: $totalRoutes"
Write-Host " Total Origins: $totalOrigins"
Write-Host ""
# Analyze route and origin health status
Write-Host "Route Status Analysis:"
Write-Host " ✓ Enabled Routes: $enabledRoutes"
Write-Host " ❌ Disabled Routes: $disabledRoutes"
Write-Host ""
Write-Host "Origin Status Analysis:"
Write-Host " ✓ Enabled Origins: $enabledOrigins"
Write-Host " ❌ Disabled Origins: $disabledOrigins"
Write-Host ""
# Identify potential issues
$issuesFound = @()
if ($disabledRoutes -gt 0) {
$issuesFound += "Some routes are disabled"
}
if ($disabledOrigins -gt 0) {
$issuesFound += "Some origins are disabled"
}
$errorEntries = $routeData | Where-Object { $_.OriginName -eq "Error" }
if ($errorEntries.Count -gt 0) {
$issuesFound += "Configuration retrieval errors detected"
}
if ($issuesFound.Count -gt 0) {
Write-Host "⚠ ATTENTION REQUIRED:"
foreach ($issue in $issuesFound) {
Write-Host " - $issue" -ForegroundColor Yellow
}
Write-Host " Review the CSV file for detailed information on affected routes/origins"
} else {
Write-Host "✓ All routes and origins are enabled and accessible"
}
Write-Host ""
Write-Host "Output File Information:"
Write-Host " File Path: $fileName"
Write-Host " File Size: $([Math]::Round((Get-Item $fileName).Length / 1KB, 2)) KB"
Write-Host " Records Exported: $($routeData.Count)"
Write-Host ""
Write-Host "Recommendations:"
Write-Host " - Review disabled routes and origins for intentional configuration"
Write-Host " - Validate routing patterns match expected traffic flow"
Write-Host " - Monitor origin health and performance regularly"
Write-Host " - Document configuration changes for audit trails"
Write-Host "======================================================================================================================================================================"
} catch {
Write-Error "Error retrieving Front Door routes: $($_.Exception.Message)"
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "❌ ERROR OCCURRED DURING PROCESSING"
Write-Host "======================================================================================================================================================================"
Write-Error "Error retrieving Front Door route configuration: $($_.Exception.Message)"
Write-Host ""
Write-Host "Troubleshooting Steps:"
Write-Host " 1. Verify Front Door profile name and resource group are correct"
Write-Host " 2. ensure the Front Door is Standard or Premium (not Classic)"
Write-Host " 3. Check that you have appropriate permissions on the Front Door resource"
Write-Host " 4. Confirm the Az.Cdn PowerShell module is installed and up to date"
Write-Host " 5. Verify network connectivity to Azure services"
Write-Host "======================================================================================================================================================================"
throw $_
}

View File

@@ -1,90 +1,321 @@
#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"
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)]"
$subscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name | Where-Object State -eq "Active"
foreach ($subscription in $subscriptions)
{
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 ""
# 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 "Subscription [$($subscription.DisplayName) - $subscriptionId]"
Set-AzContext -SubscriptionId $subscriptionId | Out-Null
Write-Host "Processing Subscription: $($subscription.DisplayName) ($subscriptionId)"
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
$allResourceGroups = Get-AzResourceGroup
try {
# Set Azure context to the current subscription
Set-AzContext -SubscriptionId $subscriptionId -ErrorAction Stop | Out-Null
Write-Host "✓ Successfully connected to subscription context"
$totalSubscriptions++
# 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++
$allVaults = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName
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)"
$vaultWithAllProps = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName -Name $vault.VaultName
try {
# Get detailed Key Vault properties including access policies
$vaultWithAllProps = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName -Name $vault.VaultName -ErrorAction Stop
$totalKeyVaults++
$subscriptionKeyVaults++
if ($vaultWithAllProps.EnableRbacAuthorization -ne "TRUE") {
# Check if vault uses traditional access policies (not RBAC-only)
if ($vaultWithAllProps.EnableRbacAuthorization -ne $true) {
Write-Host " 📋 Access Policy-based vault: $($vaultWithAllProps.ResourceId)"
Write-Host $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
}
foreach($accessPolicy in $vaultWithAllProps.AccessPolicies) {
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
@@ -92,15 +323,122 @@ foreach ($managementGroup in $managementGroups)
$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
}
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
} 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 ""
# Export subscription results to CSV file
if ($Result.Count -gt 0) {
Write-Host "Exporting $($Result.Count) access policy entries to CSV..."
try {
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation -ErrorAction Stop
Write-Host "✓ Successfully exported subscription data"
} catch {
Write-Error "Failed to export data for subscription '$($subscription.DisplayName)': $($_.Exception.Message)"
}
} else {
Write-Host " No access policy data to export from this subscription"
}
} catch {
Write-Host "❌ Error processing subscription '$($subscription.DisplayName)': $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Please verify subscription access and permissions" -ForegroundColor Red
}
Write-Host ""
}
} catch {
Write-Host "❌ Error processing management group '$($managementGroup.DisplayName)': $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Please verify management group access and permissions" -ForegroundColor Red
}
}
Write-Host "======================================================================================================================================================================"
Write-Host "Done."
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "Key Vault Access Policy Audit Completed"
Write-Host "======================================================================================================================================================================"
Write-Host "Execution completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host ""
# Display comprehensive execution statistics
Write-Host "Processing Statistics:"
Write-Host " Management Groups processed: $totalManagementGroups"
Write-Host " Subscriptions processed: $totalSubscriptions"
Write-Host " Resource Groups scanned: $processedResourceGroups"
Write-Host " Total Key Vaults found: $totalKeyVaults"
Write-Host " Access policies extracted: $totalAccessPolicies"
Write-Host " RBAC-only vaults (skipped): $rbacOnlyVaults"
Write-Host ""
# Analyze and display key findings
if ($totalAccessPolicies -gt 0) {
Write-Host "✓ Successfully exported Key Vault access policy data to: $fileName"
Write-Host ""
Write-Host "Security Analysis Insights:"
if ($rbacOnlyVaults -gt 0) {
Write-Host "$rbacOnlyVaults Key Vault(s) using modern RBAC authentication"
}
$legacyVaults = $totalKeyVaults - $rbacOnlyVaults
if ($legacyVaults -gt 0) {
Write-Host "$legacyVaults Key Vault(s) still using legacy access policies" -ForegroundColor Yellow
Write-Host " Consider migrating to Azure RBAC for improved security and management"
}
Write-Host ""
Write-Host "Output File Information:"
if (Test-Path $fileName) {
Write-Host " File Path: $fileName"
Write-Host " File Size: $([Math]::Round((Get-Item $fileName).Length / 1KB, 2)) KB"
Write-Host " Records Exported: $totalAccessPolicies"
}
} else {
Write-Host " No Key Vault access policies found across all processed subscriptions"
Write-Host " This could indicate:"
Write-Host " - All Key Vaults are using RBAC-only authentication"
Write-Host " - No Key Vaults exist in the scanned subscriptions"
Write-Host " - Permission issues preventing access to Key Vault configurations"
}
Write-Host ""
Write-Host "Security Recommendations:"
Write-Host " - Review exported access policies for excessive permissions"
Write-Host " - Validate that all identities with access are still required"
Write-Host " - Consider implementing Azure RBAC for new Key Vaults"
Write-Host " - Plan migration from access policies to RBAC for existing vaults"
Write-Host " - Implement regular access reviews and permission audits"
Write-Host " - Ensure proper governance tags are applied to all Key Vaults"
Write-Host ""
Write-Host "Compliance Notes:"
Write-Host " - Exported data contains sensitive security information"
Write-Host " - Handle output file with appropriate data classification controls"
Write-Host " - Consider encryption for long-term storage of audit results"
Write-Host " - Schedule regular execution for continuous security monitoring"
Write-Host "======================================================================================================================================================================"

View File

@@ -1,83 +1,377 @@
#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"
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)]"
$subscriptions = Get-AzManagementGroupSubscription -Group $managementGroup.Name | Where-Object State -eq "Active"
foreach ($subscription in $subscriptions)
{
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 ""
# 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 "Subscription [$($subscription.DisplayName) - $subscriptionId]"
Set-AzContext -SubscriptionId $subscriptionId | Out-Null
Write-Host "Processing Subscription: $($subscription.DisplayName) ($subscriptionId)"
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
$allResourceGroups = Get-AzResourceGroup
try {
# Set Azure context to the current subscription
Set-AzContext -SubscriptionId $subscriptionId -ErrorAction Stop | Out-Null
Write-Host "✓ Successfully connected to subscription context"
$totalSubscriptions++
# 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++
$allVaults = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName
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)"
Write-Host $vault.VaultName
try {
# Get detailed Key Vault properties
$vaultWithAllProps = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName -Name $vault.VaultName -ErrorAction Stop
$totalKeyVaults++
$subscriptionKeyVaults++
$vaultWithAllProps = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName -Name $vault.VaultName
# Check if vault uses traditional access policies (not RBAC-only)
if ($vaultWithAllProps.EnableRbacAuthorization -ne $true) {
Write-Host " 📋 Access Policy-based vault - processing secrets..."
if ($vaultWithAllProps.EnableRbacAuthorization -ne "TRUE") {
# ⚠️ 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++
Write-Host " -- processing..."
# Enumerate all secrets in the vault
Write-Host " 📝 Enumerating secrets..."
$secrets = Get-AzKeyVaultSecret -VaultName $vault.VaultName -ErrorAction Stop
Set-AzKeyVaultAccessPolicy -VaultName $vault.VaultName -ObjectId $userObjectId -PermissionsToSecrets "List"
if (-not $secrets -or $secrets.Count -eq 0) {
Write-Host " No secrets found in this vault"
} else {
Write-Host " ✓ Found $($secrets.Count) secret(s)"
$secrets = Get-AzKeyVaultSecret -VaultName $vault.VaultName
# Process each secret found
foreach ($secret in $secrets) {
Write-Host " Secret: $($secret.Name)"
foreach($secret in $secrets)
{
# 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.Name
$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
@@ -85,17 +379,187 @@ foreach ($managementGroup in $managementGroups)
$resourceCheck.Tag_CreatedOnDate = $vaultWithAllProps.Tags.CreatedOnDate
$resourceCheck.Tag_Deployment = $vaultWithAllProps.Tags.drp_deployment
# Add to results collection
$Result += $resourceCheck
}
Remove-AzKeyVaultAccessPolicy -VaultName $vault.VaultName -ObjectId $userObjectId
}
$totalSecrets++
$subscriptionSecrets++
}
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
# ⚠️ SECURITY CRITICAL: Remove temporary permissions immediately
Write-Host " 🧹 Removing temporary permissions..."
try {
Remove-AzKeyVaultAccessPolicy -VaultName $vault.VaultName -ObjectId $userObjectId -ErrorAction Stop
Write-Host " ✓ Successfully removed temporary permissions"
} catch {
Write-Host " ❌ CLEANUP FAILURE: Could not remove temporary permissions!" -ForegroundColor Red
$cleanupFailures += @{
VaultName = $vault.VaultName
ResourceGroup = $group.ResourceGroupName
SubscriptionId = $subscriptionId
Error = $_.Exception.Message
}
Write-Host " ⚠️ Manual cleanup required: Remove-AzKeyVaultAccessPolicy -VaultName '$($vault.VaultName)' -ObjectId '$userObjectId'" -ForegroundColor Yellow
}
} catch {
Write-Host " ❌ Error accessing vault secrets: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " This may indicate insufficient permissions or vault access restrictions"
# Ensure cleanup attempt even on failure
try {
Remove-AzKeyVaultAccessPolicy -VaultName $vault.VaultName -ObjectId $userObjectId -ErrorAction SilentlyContinue
} catch {
# Silent cleanup attempt
}
}
} else {
Write-Host " 🔐 RBAC-only vault (skipped): $($vault.VaultName)" -ForegroundColor Blue
$rbacOnlyVaults++
$subscriptionRbacVaults++
}
} catch {
Write-Host " ❌ Error processing vault '$($vault.VaultName)': $($_.Exception.Message)" -ForegroundColor Red
}
}
} catch {
Write-Host " ❌ Error scanning resource group '$($group.ResourceGroupName)': $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host ""
Write-Host "Subscription Summary:"
Write-Host " Key Vaults found: $subscriptionKeyVaults"
Write-Host " Secrets inventoried: $subscriptionSecrets"
Write-Host " Access policy modifications: $subscriptionAccessPolicyMods"
Write-Host " RBAC-only vaults (skipped): $subscriptionRbacVaults"
if ($cleanupFailures.Count -gt 0) {
Write-Host " ⚠️ Cleanup failures: $($cleanupFailures.Count)" -ForegroundColor Yellow
}
Write-Host ""
# Export subscription results to CSV file
if ($Result.Count -gt 0) {
Write-Host "Exporting $($Result.Count) secret entries to CSV..."
try {
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation -ErrorAction Stop
Write-Host "✓ Successfully exported subscription data"
} catch {
Write-Error "Failed to export data for subscription '$($subscription.DisplayName)': $($_.Exception.Message)"
}
} else {
Write-Host " No secret data to export from this subscription"
}
} catch {
Write-Host "❌ Error processing subscription '$($subscription.DisplayName)': $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Please verify subscription access and permissions" -ForegroundColor Red
}
Write-Host ""
}
} catch {
Write-Host "❌ Error processing management group '$($managementGroup.DisplayName)': $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Please verify management group access and permissions" -ForegroundColor Red
}
}
Write-Host "======================================================================================================================================================================"
Write-Host "Done."
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "🔐 Key Vault Secret Inventory Completed"
Write-Host "======================================================================================================================================================================"
Write-Host "Execution completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host ""
# Display comprehensive execution statistics
Write-Host "Processing Statistics:"
Write-Host " Management Groups processed: $totalManagementGroups"
Write-Host " Subscriptions processed: $totalSubscriptions"
Write-Host " Resource Groups scanned: $processedResourceGroups"
Write-Host " Total Key Vaults found: $totalKeyVaults"
Write-Host " Secrets inventoried: $totalSecrets"
Write-Host " Access policy modifications: $accessPolicyModifications"
Write-Host " RBAC-only vaults (skipped): $rbacOnlyVaults"
Write-Host ""
# Critical security status check
if ($cleanupFailures.Count -gt 0) {
Write-Host "🚨 CRITICAL SECURITY ALERT: MANUAL CLEANUP REQUIRED" -ForegroundColor Red
Write-Host "======================================================================================================================================================================"
Write-Host "The following Key Vaults still have temporary permissions that need manual removal:" -ForegroundColor Red
Write-Host ""
foreach ($failure in $cleanupFailures) {
Write-Host " Vault: $($failure.VaultName)" -ForegroundColor Red
Write-Host " Resource Group: $($failure.ResourceGroup)" -ForegroundColor Red
Write-Host " Subscription: $($failure.SubscriptionId)" -ForegroundColor Red
Write-Host " Cleanup Command: Remove-AzKeyVaultAccessPolicy -VaultName '$($failure.VaultName)' -ObjectId '$userObjectId'" -ForegroundColor Yellow
Write-Host " Error: $($failure.Error)" -ForegroundColor Red
Write-Host ""
}
Write-Host "⚠️ Please run the cleanup commands above to remove temporary permissions immediately!" -ForegroundColor Yellow
Write-Host "======================================================================================================================================================================"
} else {
Write-Host "✅ Security Status: All temporary access policies were successfully removed"
}
Write-Host ""
# Analyze and display key findings
if ($totalSecrets -gt 0) {
Write-Host "✓ Successfully exported Key Vault secret inventory to: $fileName"
Write-Host ""
Write-Host "Security Analysis Insights:"
if ($rbacOnlyVaults -gt 0) {
Write-Host "$rbacOnlyVaults Key Vault(s) using modern RBAC authentication"
}
$legacyVaults = $totalKeyVaults - $rbacOnlyVaults
if ($legacyVaults -gt 0) {
Write-Host "$legacyVaults Key Vault(s) still using legacy access policies" -ForegroundColor Yellow
Write-Host " Consider migrating to Azure RBAC for improved security and management"
}
Write-Host ""
Write-Host "Output File Information:"
if (Test-Path $fileName) {
Write-Host " File Path: $fileName"
Write-Host " File Size: $([Math]::Round((Get-Item $fileName).Length / 1KB, 2)) KB"
Write-Host " Records Exported: $totalSecrets"
}
} else {
Write-Host " No Key Vault secrets found across all processed subscriptions"
Write-Host " This could indicate:"
Write-Host " - All Key Vaults are using RBAC-only authentication"
Write-Host " - No secrets exist in the scanned Key Vaults"
Write-Host " - Permission issues preventing access to Key Vault contents"
}
Write-Host ""
Write-Host "Security Recommendations:"
Write-Host " - Review exported secret inventory for unused or expired secrets"
Write-Host " - Implement secret rotation policies for all identified secrets"
Write-Host " - Consider implementing Azure RBAC for new Key Vaults"
Write-Host " - Plan migration from access policies to RBAC for existing vaults"
Write-Host " - Implement regular secret lifecycle management and cleanup"
Write-Host " - Ensure proper governance tags are applied to all Key Vaults"
Write-Host ""
Write-Host "Compliance and Security Notes:"
Write-Host " - This inventory contains sensitive secret name information"
Write-Host " - Handle output file with appropriate data classification controls"
Write-Host " - All access policy modifications are logged in Azure Activity Log"
Write-Host " - Consider encryption for long-term storage of inventory results"
Write-Host " - Schedule regular execution for continuous secret governance"
Write-Host " - Ensure manual cleanup is performed if cleanup failures occurred"
Write-Host ""
Write-Host "Audit Trail:"
Write-Host " - All temporary access policy changes are logged in Azure Activity Log"
Write-Host " - Search for 'Microsoft.KeyVault/vaults/accessPolicies/write' operations"
Write-Host " - Filter by Object ID: $userObjectId"
Write-Host " - Execution timeframe: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "======================================================================================================================================================================"

View File

@@ -1,4 +1,74 @@
#Connect-AzAccount
<#
.SYNOPSIS
Generates a comprehensive inventory of Azure Key Vaults across all management groups and subscriptions.
.DESCRIPTION
This script enumerates all Azure Key Vaults within enabled subscriptions across the entire Azure tenant,
collecting detailed configuration properties, security settings, and governance tags. The results are
exported to a timestamped CSV file for analysis, compliance reporting, and security auditing.
Key capabilities:
- Multi-tenant Key Vault discovery across all management groups
- Configuration analysis including RBAC, purge protection, and soft delete settings
- Governance tag extraction for team ownership and compliance tracking
- Public network access configuration reporting
- Timestamped CSV export for audit trails and trend analysis
.PARAMETER None
This script does not accept parameters and will process all accessible Key Vaults.
.OUTPUTS
CSV File: "<date> azure_key_vaults.csv"
Contains columns for:
- Resource identification (ID, name, resource group, location)
- Management hierarchy (management group, subscription)
- Governance tags (team, product, environment, data classification)
- Security configuration (RBAC, purge protection, soft delete, network access)
.EXAMPLE
.\KeyVaults.ps1
Discovers all Key Vaults and generates: "2024-10-30 1435 azure_key_vaults.csv"
.NOTES
File Name : KeyVaults.ps1
Author : Cloud Engineering Team
Prerequisite : Azure PowerShell module (Az.KeyVault, Az.Resources, Az.Accounts)
Copyright : (c) 2024 Effectory. All rights reserved.
Version History:
1.0 - Initial release with comprehensive Key Vault inventory functionality
.LINK
https://docs.microsoft.com/en-us/azure/key-vault/
https://docs.microsoft.com/en-us/powershell/module/az.keyvault/
.COMPONENT
Requires Azure PowerShell modules:
- Az.KeyVault (for Key Vault enumeration and property retrieval)
- Az.Resources (for resource group and management group access)
- Az.Accounts (for authentication and subscription management)
.ROLE
Required Azure permissions:
- Key Vault Reader or higher on all subscriptions
- Management Group Reader for organizational hierarchy access
.FUNCTIONALITY
- Multi-subscription Key Vault discovery
- Security configuration analysis and reporting
- Governance tag extraction and compliance tracking
- CSV export with comprehensive audit trail
#>
#Requires -Modules Az.KeyVault, Az.Resources, Az.Accounts
#Requires -Version 5.1
[CmdletBinding()]
param()
# Uncomment the following line if authentication is required
#Connect-AzAccount
class ResourceCheck {
[string] $ResourceId = ""
@@ -22,49 +92,110 @@ 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"
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 ""
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_key_vaults.csv"
# 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 ""
$managementGroups = Get-AzManagementGroup
# 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 ""
foreach ($managementGroup in $managementGroups)
{
# 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.Name)]"
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)
{
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
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 "Subscription [$($subscription.DisplayName) - $subscriptionId]"
Set-AzContext -SubscriptionId $subscriptionId | Out-Null
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
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
Write-Host $group.ResourceGroupName
try {
# Get Key Vaults in this resource group
$allVaults = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName -ErrorAction SilentlyContinue
$allVaults = Get-AzKeyVault -ResourceGroupName $group.ResourceGroupName
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
@@ -86,13 +217,109 @@ foreach ($managementGroup in $managementGroups)
$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
}
}
Write-Host "======================================================================================================================================================================"
Write-Host "Done."
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
}
} catch {
Write-Host " ❌ Error accessing management group: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host ""
}
# Calculate execution time and generate summary report
$endTime = Get-Date
$executionTime = $endTime - $startTime
Write-Host "======================================================================================================================================================================"
Write-Host "📊 AZURE KEY VAULT INVENTORY SUMMARY" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
Write-Host "⏰ Execution Time: $($executionTime.ToString('hh\:mm\:ss'))" -ForegroundColor Green
Write-Host "🏗️ Management Groups Processed: $processedManagementGroups" -ForegroundColor Green
Write-Host "📋 Subscriptions Processed: $processedSubscriptions" -ForegroundColor Green
Write-Host "🔐 Total Key Vaults Discovered: $totalKeyVaults" -ForegroundColor Green
Write-Host "📄 Results Exported To: $fileName" -ForegroundColor Yellow
if (Test-Path $fileName) {
$fileSize = (Get-Item $fileName).Length
Write-Host "💾 File Size: $([math]::Round($fileSize/1KB, 2)) KB" -ForegroundColor Green
}
# Display security analysis summary
if ($securityIssues.Count -gt 0) {
Write-Host ""
Write-Host "🚨 SECURITY ANALYSIS SUMMARY" -ForegroundColor Red
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "Found $($securityIssues.Count) potential security concerns:" -ForegroundColor Yellow
foreach ($issue in $securityIssues | Select-Object -First 10) {
Write-Host " $issue" -ForegroundColor Yellow
}
if ($securityIssues.Count -gt 10) {
Write-Host " ... and $($securityIssues.Count - 10) more issues (see CSV for complete details)" -ForegroundColor DarkYellow
}
Write-Host ""
Write-Host "📋 Recommendations:" -ForegroundColor Cyan
Write-Host " • Enable purge protection on production Key Vaults" -ForegroundColor White
Write-Host " • Ensure soft delete is enabled for data recovery capabilities" -ForegroundColor White
Write-Host " • Consider disabling public network access where possible" -ForegroundColor White
Write-Host " • Implement RBAC authorization for enhanced security" -ForegroundColor White
} else {
Write-Host ""
Write-Host "✅ SECURITY ANALYSIS: No major security concerns detected" -ForegroundColor Green
}
Write-Host ""
Write-Host "📈 NEXT STEPS:" -ForegroundColor Cyan
Write-Host " 1. Review the generated CSV file for detailed Key Vault configurations" -ForegroundColor White
Write-Host " 2. Analyze governance tags for compliance with organizational standards" -ForegroundColor White
Write-Host " 3. Address any security recommendations identified above" -ForegroundColor White
Write-Host " 4. Consider implementing automated monitoring for Key Vault configurations" -ForegroundColor White
Write-Host ""
Write-Host "✅ Azure Key Vault inventory completed successfully!" -ForegroundColor Green
Write-Host "======================================================================================================================================================================"
} catch {
Write-Host ""
Write-Host "❌ CRITICAL ERROR OCCURRED" -ForegroundColor Red
Write-Host "======================================================================================================================================================================"
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Line: $($_.InvocationInfo.ScriptLineNumber)" -ForegroundColor Red
Write-Host "Position: $($_.InvocationInfo.OffsetInLine)" -ForegroundColor Red
Write-Host ""
Write-Host "🔧 TROUBLESHOOTING STEPS:" -ForegroundColor Yellow
Write-Host " 1. Verify you are authenticated to Azure (Connect-AzAccount)" -ForegroundColor White
Write-Host " 2. Ensure you have Key Vault Reader permissions on target subscriptions" -ForegroundColor White
Write-Host " 3. Check that the Management Group Reader role is assigned" -ForegroundColor White
Write-Host " 4. Verify Azure PowerShell modules are installed and up to date" -ForegroundColor White
Write-Host " 5. Try running the script with -Verbose for additional diagnostic information" -ForegroundColor White
Write-Host ""
Write-Host "📞 For additional support, contact the Cloud Engineering team" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
# Ensure we exit with error code for automation scenarios
exit 1
} finally {
# Reset progress preference
$ProgressPreference = "Continue"
}

View File

@@ -1,4 +1,98 @@
#Connect-AzAccount
<#
.SYNOPSIS
Generates a hierarchical inventory of Azure Management Groups and Subscriptions.
.DESCRIPTION
This script creates a comprehensive mapping of Azure organizational structure by traversing
the management group hierarchy starting from a specified root management group. It discovers
and documents all subscriptions within a 3-level management group structure (Level 0-2).
The script provides detailed organizational visibility including:
- Hierarchical management group structure mapping
- Subscription placement and organizational context
- Subscription state tracking (Active, Disabled, etc.)
- Multi-level governance structure documentation
- CSV export for organizational analysis and compliance reporting
Note: The script is optimized for a maximum 3-level management group depth and starts
from a configurable root management group ID.
.PARAMETER RootManagementGroupId
The GUID of the root management group to start the hierarchy discovery from.
Defaults to the Effectory organization root management group.
Example: "12345678-1234-1234-1234-123456789012"
.OUTPUTS
CSV File: "<date> azure_managementgroups.csv"
Contains columns for:
- Subscription identification (ID, name, state)
- Level 0 Management Group (root level)
- Level 1 Management Group (department/division level)
- Level 2 Management Group (team/project level)
.EXAMPLE
.\ManagementGroups.ps1
Uses the default Effectory root management group and generates:
"2024-10-30 1435 azure_managementgroups.csv"
.EXAMPLE
.\ManagementGroups.ps1 -RootManagementGroupId "87654321-4321-4321-4321-210987654321"
Discovers management group hierarchy starting from a custom root management group.
.EXAMPLE
.\ManagementGroups.ps1 -RootManagementGroupId "tenant-root-mg" -Verbose
Runs with verbose output for detailed discovery logging.
.NOTES
File Name : ManagementGroups.ps1
Author : Cloud Engineering Team
Prerequisite : Azure PowerShell module (Az.Resources, Az.Accounts)
Copyright : (c) 2024 Effectory. All rights reserved.
Version History:
1.0 - Initial release with 3-level management group hierarchy discovery
1.1 - Added parameterized root management group for flexibility
.LINK
https://docs.microsoft.com/en-us/azure/governance/management-groups/
https://docs.microsoft.com/en-us/powershell/module/az.resources/
.COMPONENT
Requires Azure PowerShell modules:
- Az.Resources (for management group and subscription enumeration)
- Az.Accounts (for authentication and context management)
.ROLE
Required Azure permissions:
- Management Group Reader on the root management group and all child groups
- Reader access to view subscription details within management groups
.FUNCTIONALITY
- Hierarchical management group discovery (3-level maximum)
- Subscription placement mapping and state tracking
- Organizational structure documentation and CSV export
- Cross-tenant compatible with configurable root management group
#>
#Requires -Modules Az.Resources, Az.Accounts
#Requires -Version 5.1
# Uncomment the following line if authentication is required
#Connect-AzAccount
[CmdletBinding()]
param(
[Parameter(
Mandatory = $false,
HelpMessage = "The GUID of the root management group to start hierarchy discovery from"
)]
[ValidateNotNullOrEmpty()]
[string]$RootManagementGroupId = 'e9792fd7-4044-47e7-a40d-3fba46f1cd09'
)
class ResourceCheck {
[string] $SubscriptionId = ""
@@ -12,29 +106,74 @@ 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]
try {
# Validate Azure authentication
$context = Get-AzContext
if (-not $context) {
throw "No Azure context found. Please run Connect-AzAccount first."
}
#level 0
Write-Host "---------------------------------------------------------------------------------------------"
Write-Host "Level 0 Management group [$($rootManagementGroup.Name)]"
Write-Host "---------------------------------------------------------------------------------------------"
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 ""
$subscriptions = $rootManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
# 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 ""
foreach ($subscription in $subscriptions)
{
[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 ""
# Process Level 0 (Root) subscriptions
$managementGroupCount++
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "📋 LEVEL 0 (Root): $($rootManagementGroup.DisplayName)" -ForegroundColor Cyan
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
$subscriptions = $rootManagementGroup.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/", "")
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
@@ -43,24 +182,45 @@ foreach ($subscription in $subscriptions)
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
}
$totalSubscriptions++
#level 1
foreach ($level1ManagementGroupLister in ($rootManagementGroup.Children | Where-Object Type -EQ 'Microsoft.Management/managementGroups'))
{
$level1ManagementGroup = (Get-AzManagementGroup -Group $level1ManagementGroupLister.Name -Expand)[0]
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host " ---------------------------------------------------------------------------------------------"
Write-Host " Level 1 Management group [$($level1ManagementGroup.Name)]"
Write-Host " ---------------------------------------------------------------------------------------------"
# 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)
{
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
@@ -71,24 +231,50 @@ foreach ($level1ManagementGroupLister in ($rootManagementGroup.Children | Where-
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
$totalSubscriptions++
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
}
#level 2
foreach ($level2ManagementGroupLister in ($level1ManagementGroup.Children | Where-Object Type -EQ 'Microsoft.Management/managementGroups'))
{
$level2ManagementGroup = (Get-AzManagementGroup -Group $level2ManagementGroupLister.Name -Expand)[0]
} catch {
Write-Host " ❌ Error accessing Level 1 management group '$($level1ManagementGroupLister.Name)': $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host " ---------------------------------------------------------------------------------------------"
Write-Host " Level 2 Management group [$($level2ManagementGroup.Name)]"
Write-Host " ---------------------------------------------------------------------------------------------"
# 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)
{
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
@@ -101,13 +287,105 @@ foreach ($level1ManagementGroupLister in ($rootManagementGroup.Children | Where-
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
$totalSubscriptions++
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
}
} catch {
Write-Host " ❌ Error accessing Level 2 management group '$($level2ManagementGroupLister.Name)': $($_.Exception.Message)" -ForegroundColor Red
}
}
} else {
Write-Host " No Level 2 management groups found" -ForegroundColor DarkGray
}
}
# Export results to CSV
Write-Host ""
Write-Host "💾 Exporting results to CSV..." -ForegroundColor Cyan
$Result | Export-Csv -Path $fileName -NoTypeInformation
# Calculate execution time and generate summary report
$endTime = Get-Date
$executionTime = $endTime - $startTime
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "📊 AZURE MANAGEMENT GROUP DISCOVERY SUMMARY" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
Write-Host "⏰ Execution Time: $($executionTime.ToString('hh\:mm\:ss'))" -ForegroundColor Green
Write-Host "🏗️ Management Groups Discovered: $managementGroupCount" -ForegroundColor Green
Write-Host "📋 Total Subscriptions Found: $totalSubscriptions" -ForegroundColor Green
Write-Host "📄 Results Exported To: $fileName" -ForegroundColor Yellow
if (Test-Path $fileName) {
$fileSize = (Get-Item $fileName).Length
Write-Host "💾 File Size: $([math]::Round($fileSize/1KB, 2)) KB" -ForegroundColor Green
}
# Analyze subscription states
$subscriptionStates = $Result | Group-Object SubscriptionState
if ($subscriptionStates.Count -gt 0) {
Write-Host ""
Write-Host "📈 SUBSCRIPTION STATE ANALYSIS:" -ForegroundColor Cyan
foreach ($state in $subscriptionStates) {
$stateColor = switch ($state.Name) {
"Enabled" { "Green" }
"Disabled" { "Red" }
"Warned" { "Yellow" }
default { "White" }
}
Write-Host " $($state.Name): $($state.Count) subscriptions" -ForegroundColor $stateColor
}
}
# Provide organizational insights
$level0Subs = ($Result | Where-Object { [string]::IsNullOrEmpty($_.Level1_ManagementGroupId) }).Count
$level1Subs = ($Result | Where-Object { -not [string]::IsNullOrEmpty($_.Level1_ManagementGroupId) -and [string]::IsNullOrEmpty($_.Level2_ManagementGroupId) }).Count
$level2Subs = ($Result | Where-Object { -not [string]::IsNullOrEmpty($_.Level2_ManagementGroupId) }).Count
Write-Host ""
Write-Host "🏗️ ORGANIZATIONAL STRUCTURE:" -ForegroundColor Cyan
Write-Host " Root Level (Level 0): $level0Subs subscriptions" -ForegroundColor Green
Write-Host " Department/Division Level (Level 1): $level1Subs subscriptions" -ForegroundColor Yellow
Write-Host " Team/Project Level (Level 2): $level2Subs subscriptions" -ForegroundColor Magenta
Write-Host ""
Write-Host "📈 NEXT STEPS:" -ForegroundColor Cyan
Write-Host " 1. Review the generated CSV file for detailed organizational mapping" -ForegroundColor White
Write-Host " 2. Analyze subscription placement for governance compliance" -ForegroundColor White
Write-Host " 3. Consider moving orphaned subscriptions to appropriate management groups" -ForegroundColor White
Write-Host " 4. Use this data for policy assignment and resource organization planning" -ForegroundColor White
Write-Host ""
Write-Host "✅ Azure Management Group discovery completed successfully!" -ForegroundColor Green
Write-Host "======================================================================================================================================================================"
} catch {
Write-Host ""
Write-Host "❌ CRITICAL ERROR OCCURRED" -ForegroundColor Red
Write-Host "======================================================================================================================================================================"
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Line: $($_.InvocationInfo.ScriptLineNumber)" -ForegroundColor Red
Write-Host "Position: $($_.InvocationInfo.OffsetInLine)" -ForegroundColor Red
Write-Host ""
Write-Host "🔧 TROUBLESHOOTING STEPS:" -ForegroundColor Yellow
Write-Host " 1. Verify you are authenticated to Azure (Connect-AzAccount)" -ForegroundColor White
Write-Host " 2. Ensure you have Management Group Reader permissions on the root management group" -ForegroundColor White
Write-Host " 3. Verify the root management group ID '$RootManagementGroupId' exists and is accessible" -ForegroundColor White
Write-Host " 4. Check that Azure PowerShell modules are installed and up to date" -ForegroundColor White
Write-Host " 5. Try running with a different root management group ID if the current one is invalid" -ForegroundColor White
Write-Host ""
Write-Host "📞 For additional support, contact the Cloud Engineering team" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
# Ensure we exit with error code for automation scenarios
exit 1
} finally {
# Reset progress preference
$ProgressPreference = "Continue"
}
$Result | Export-Csv -Path $fileName -NoTypeInformation
Write-Host "============================================================================================="
Write-Host "Done."

View File

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

View File

@@ -1,4 +1,81 @@
#Connect-AzAccount
<#
.SYNOPSIS
Generates a comprehensive inventory of all Azure resources across enabled subscriptions.
.DESCRIPTION
This script enumerates all Azure resources within enabled subscriptions across the entire Azure tenant,
collecting detailed resource properties, governance tags, and managed identity information. The results
are exported to a timestamped CSV file for analysis, compliance reporting, and resource management.
Key capabilities:
- Cross-subscription resource discovery and enumeration
- Comprehensive resource metadata collection (type, location, resource group)
- Governance tag extraction for team ownership and compliance tracking
- Managed identity discovery and principal ID mapping
- Resource lifecycle management tags (delete, split, deployment tracking)
- Timestamped CSV export with complete audit trail
The script processes all enabled subscriptions and captures essential resource information
including resource hierarchy, governance tags, and security identities for comprehensive
Azure estate management and reporting.
.PARAMETER None
This script does not accept parameters and will process all resources in all enabled subscriptions.
.OUTPUTS
CSV File: "<date> Resources.csv"
Contains columns for:
- Resource identification (ID, name, type, kind, location)
- Subscription and resource group context
- Governance tags (team, product, environment, data classification)
- Lifecycle management tags (delete, split, created date, deployment)
- Managed identity information (name, principal ID)
.EXAMPLE
.\Resources.ps1
Discovers all resources across enabled subscriptions and generates:
"2024-10-30 1435 Resources.csv"
.NOTES
File Name : Resources.ps1
Author : Cloud Engineering Team
Prerequisite : Azure PowerShell module (Az.Resources, Az.Accounts, Az.ManagedServiceIdentity)
Copyright : (c) 2024 Effectory. All rights reserved.
Version History:
1.0 - Initial release with comprehensive resource inventory functionality
.LINK
https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/overview
https://docs.microsoft.com/en-us/powershell/module/az.resources/
.COMPONENT
Requires Azure PowerShell modules:
- Az.Resources (for resource enumeration and property retrieval)
- Az.Accounts (for authentication and subscription management)
- Az.ManagedServiceIdentity (for managed identity discovery)
.ROLE
Required Azure permissions:
- Reader or higher on all target subscriptions
- Managed Identity Operator (for identity information retrieval)
.FUNCTIONALITY
- Multi-subscription resource discovery and enumeration
- Governance tag extraction and compliance tracking
- Managed identity mapping and security context analysis
- CSV export with comprehensive resource metadata
#>
#Requires -Modules Az.Resources, Az.Accounts, Az.ManagedServiceIdentity
#Requires -Version 5.1
[CmdletBinding()]
param()
# Uncomment the following line if authentication is required
#Connect-AzAccount
class ResourceCheck {
[string] $ResourceId = ""
@@ -22,24 +99,77 @@ 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"
try {
# Validate Azure authentication
$context = Get-AzContext
if (-not $context) {
throw "No Azure context found. Please run Connect-AzAccount first."
}
foreach ($subscription in $subscriptions)
{
Set-AzContext -SubscriptionId $subscription.Id
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 {
Set-AzContext -SubscriptionId $subscription.Id | Out-Null
# 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
$allResources = Get-AzResource
[ResourceCheck[]]$Result = @()
$subscriptionResourceCount = 0
$subscriptionManagedIdentityCount = 0
foreach ($resource in $allResources)
{
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
@@ -59,22 +189,169 @@ Set-AzContext -SubscriptionId $subscription.Id
$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 = $null
$managedIdentity = Get-AzSystemAssignedIdentity -Scope $resource.ResourceId -erroraction 'silentlycontinue'
$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 {
} 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
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
}
} catch {
Write-Host " ❌ Error processing resource '$($resource.ResourceName)': $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host "========================================================================================================================================================================"
Write-Host "Done."
# 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 ""
}
# Calculate execution time and generate comprehensive summary report
$endTime = Get-Date
$executionTime = $endTime - $startTime
Write-Host "======================================================================================================================================================================"
Write-Host "📊 AZURE RESOURCE INVENTORY SUMMARY" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
Write-Host "⏰ Execution Time: $($executionTime.ToString('hh\:mm\:ss'))" -ForegroundColor Green
Write-Host "📋 Subscriptions Processed: $processedSubscriptions" -ForegroundColor Green
Write-Host "🔍 Total Resources Discovered: $totalResources" -ForegroundColor Green
Write-Host "🔐 Managed Identities Found: $managedIdentityCount" -ForegroundColor Cyan
Write-Host "📄 Results Exported To: $fileName" -ForegroundColor Yellow
if (Test-Path $fileName) {
$fileSize = (Get-Item $fileName).Length
Write-Host "💾 File Size: $([math]::Round($fileSize/1KB, 2)) KB" -ForegroundColor Green
}
# Display top resource types
if ($resourceTypes.Count -gt 0) {
Write-Host ""
Write-Host "📈 TOP RESOURCE TYPES:" -ForegroundColor Cyan
$topResourceTypes = $resourceTypes.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 10
foreach ($resourceType in $topResourceTypes) {
Write-Host " $($resourceType.Key): $($resourceType.Value) resources" -ForegroundColor White
}
if ($resourceTypes.Count -gt 10) {
$remainingTypes = $resourceTypes.Count - 10
$remainingResources = ($resourceTypes.GetEnumerator() | Sort-Object Value -Descending | Select-Object -Skip 10 | Measure-Object Value -Sum).Sum
Write-Host " ... and $remainingTypes more types ($remainingResources resources)" -ForegroundColor DarkGray
}
}
# Display governance analysis
if ($governanceIssues.Count -gt 0) {
Write-Host ""
Write-Host "🚨 GOVERNANCE ANALYSIS" -ForegroundColor Red
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "Found $($governanceIssues.Count) resources with missing governance tags:" -ForegroundColor Yellow
foreach ($issue in $governanceIssues | Select-Object -First 15) {
Write-Host " $issue" -ForegroundColor Yellow
}
if ($governanceIssues.Count -gt 15) {
Write-Host " ... and $($governanceIssues.Count - 15) more governance issues (see CSV for complete details)" -ForegroundColor DarkYellow
}
Write-Host ""
Write-Host "📋 Governance Recommendations:" -ForegroundColor Cyan
Write-Host " • Implement mandatory tagging policies for team, product, and environment" -ForegroundColor White
Write-Host " • Use Azure Policy to enforce governance tag compliance" -ForegroundColor White
Write-Host " • Establish resource naming conventions and tagging standards" -ForegroundColor White
Write-Host " • Regular governance audits using this resource inventory" -ForegroundColor White
} else {
Write-Host ""
Write-Host "✅ GOVERNANCE ANALYSIS: All resources have required governance tags" -ForegroundColor Green
}
# Security and identity insights
if ($managedIdentityCount -gt 0) {
$identityPercentage = [math]::Round(($managedIdentityCount / $totalResources) * 100, 1)
Write-Host ""
Write-Host "🔐 SECURITY ANALYSIS:" -ForegroundColor Cyan
Write-Host " Managed Identity Adoption: $identityPercentage% of resources ($managedIdentityCount/$totalResources)" -ForegroundColor Green
Write-Host " 💡 Consider expanding managed identity usage for enhanced security" -ForegroundColor White
}
Write-Host ""
Write-Host "📈 NEXT STEPS:" -ForegroundColor Cyan
Write-Host " 1. Review the generated CSV file for detailed resource analysis" -ForegroundColor White
Write-Host " 2. Address governance tag compliance issues identified above" -ForegroundColor White
Write-Host " 3. Analyze resource distribution across subscriptions and regions" -ForegroundColor White
Write-Host " 4. Consider resource optimization opportunities (unused resources, right-sizing)" -ForegroundColor White
Write-Host " 5. Implement automated resource monitoring and cost management" -ForegroundColor White
Write-Host " 6. Use managed identity information for security auditing" -ForegroundColor White
Write-Host ""
Write-Host "✅ Azure resource inventory completed successfully!" -ForegroundColor Green
Write-Host "======================================================================================================================================================================"
} catch {
Write-Host ""
Write-Host "❌ CRITICAL ERROR OCCURRED" -ForegroundColor Red
Write-Host "======================================================================================================================================================================"
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Line: $($_.InvocationInfo.ScriptLineNumber)" -ForegroundColor Red
Write-Host "Position: $($_.InvocationInfo.OffsetInLine)" -ForegroundColor Red
Write-Host ""
Write-Host "🔧 TROUBLESHOOTING STEPS:" -ForegroundColor Yellow
Write-Host " 1. Verify you are authenticated to Azure (Connect-AzAccount)" -ForegroundColor White
Write-Host " 2. Ensure you have Reader permissions on all target subscriptions" -ForegroundColor White
Write-Host " 3. Check that Azure PowerShell modules are installed and up to date" -ForegroundColor White
Write-Host " 4. Verify Managed Identity Operator role for identity information retrieval" -ForegroundColor White
Write-Host " 5. Try running the script with -Verbose for additional diagnostic information" -ForegroundColor White
Write-Host " 6. Consider processing subscriptions individually if encountering timeout issues" -ForegroundColor White
Write-Host ""
Write-Host "📞 For additional support, contact the Cloud Engineering team" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
# Ensure we exit with error code for automation scenarios
exit 1
} finally {
# Reset progress preference
$ProgressPreference = "Continue"
}

View File

@@ -1,10 +1,88 @@
<#
.SYNOPSIS
Generates a comprehensive inventory of Azure Service Bus resources across all management groups and subscriptions.
.DESCRIPTION
This script enumerates all Azure Service Bus namespaces, topics, topic subscriptions, and queues
within active subscriptions across the entire Azure tenant. It provides detailed hierarchical
mapping of Service Bus messaging infrastructure for monitoring, compliance, and governance reporting.
Key capabilities:
- Multi-tenant Service Bus discovery across all management groups
- Hierarchical messaging structure mapping (namespaces → topics → subscriptions, queues)
- Complete Service Bus topology documentation
- Management group and subscription context tracking
- Resource hierarchy analysis for messaging architecture
- Timestamped CSV export for audit trails and capacity planning
The script processes the complete Service Bus messaging estate including:
- Service Bus namespaces (messaging containers)
- Topics (pub/sub messaging endpoints)
- Topic subscriptions (message consumers)
- Queues (point-to-point messaging endpoints)
.PARAMETER None
This script does not accept parameters and will process all Service Bus resources across all accessible namespaces.
.OUTPUTS
CSV File: "<date> azure service bus.csv"
Contains columns for:
- Resource identification (ID, type, location)
- Management hierarchy (management group, subscription, resource group)
- Service Bus namespace information
- Messaging topology (topic names, subscription names, queue names)
.EXAMPLE
.\ServiceBus.ps1
Discovers all Service Bus resources and generates:
"2024-10-30 1435 azure service bus.csv"
.NOTES
File Name : ServiceBus.ps1
Author : Cloud Engineering Team
Prerequisite : Azure PowerShell modules (Az.ServiceBus, Az.Resources, Az.Accounts)
Copyright : (c) 2024 Effectory. All rights reserved.
Version History:
1.0 - Initial release with comprehensive Service Bus inventory functionality
.LINK
https://docs.microsoft.com/en-us/azure/service-bus-messaging/
https://docs.microsoft.com/en-us/powershell/module/az.servicebus/
.COMPONENT
Requires Azure PowerShell modules:
- Az.ServiceBus (for Service Bus namespace, topic, queue, and subscription enumeration)
- Az.Resources (for resource group and management group access)
- Az.Accounts (for authentication and subscription management)
- Az.Automation (for automation context support)
.ROLE
Required Azure permissions:
- Service Bus Data Reader or higher on all Service Bus namespaces
- Management Group Reader for organizational hierarchy access
- Reader access on target subscriptions
.FUNCTIONALITY
- Multi-subscription Service Bus discovery
- Messaging topology analysis and hierarchical mapping
- Complete Service Bus infrastructure documentation
- CSV export with comprehensive messaging architecture details
#>
#Requires -Modules Az.ServiceBus, Az.Resources, Az.Accounts, Az.Automation
#Requires -Version 5.1
[CmdletBinding()]
param()
# Import required modules
Import-Module Az.Accounts
Import-Module Az.Automation
Import-Module Az.ServiceBus
Import-Module Az.Resources
$subscriptions = Get-AzSubscription
class ResourceCheck {
[string] $ResourceId = ""
[string] $ManagementGroupId = ""
@@ -20,36 +98,94 @@ 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."
}
$managementGroups = Get-AzManagementGroup
Write-Host "🔐 Authenticated as: $($context.Account.Id)" -ForegroundColor Green
Write-Host "🏢 Tenant: $($context.Tenant.Id)" -ForegroundColor Green
Write-Host ""
foreach ($managementGroup in $managementGroups) {
# 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 ""
# Process each management group
foreach ($managementGroup in $managementGroups) {
$processedManagementGroups++
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "Management group [$($managementGroup.Name)]"
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) {
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
$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 "Subscription [$($subscription.DisplayName) - $subscriptionId]"
Write-Host " ID: $subscriptionId" -ForegroundColor DarkGray
Set-AzContext -SubscriptionId $subscriptionId | Out-Null
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
$servicebusses = Get-AzServiceBusNamespaceV2
# 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
Write-Host "Getting info for service bus [$($servicebus.Name)]"
try {
[ResourceCheck[]]$Result = @()
$namespaceTopics = 0
$namespaceTopicSubs = 0
$namespaceQueues = 0
# Add namespace entry
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.ResourceId = $servicebus.Id
$resourceCheck.ManagementGroupId = $managementGroup.Id
@@ -62,8 +198,18 @@ foreach ($managementGroup in $managementGroups) {
$resourceCheck.ServiceBusName = $servicebus.Name
$Result += $resourceCheck
#topics
$topics = Get-AzServiceBusTopic -NamespaceName $servicebus.Name -ResourceGroupName $servicebus.ResourceGroupName
# 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()
@@ -79,11 +225,11 @@ foreach ($managementGroup in $managementGroups) {
$resourceCheck.TopicName = $topic.Name
$Result += $resourceCheck
# topic subscriptions
$topicSubs = Get-AzServiceBusSubscription -NamespaceName $servicebus.Name -ResourceGroupName $servicebus.ResourceGroupName -TopicName $topic.Name
# 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
@@ -97,14 +243,31 @@ foreach ($managementGroup in $managementGroups) {
$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
}
# queues
$queues = Get-AzServiceBusQueue -NamespaceName $servicebus.Name -ResourceGroupName $servicebus.ResourceGroupName
# 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
@@ -118,11 +281,126 @@ foreach ($managementGroup in $managementGroups) {
$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
}
}
}
Write-Host "======================================================================================================================================================================"
Write-Host "Done."
} 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
}
Write-Host ""
}
# Calculate execution time and generate comprehensive summary report
$endTime = Get-Date
$executionTime = $endTime - $startTime
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 "📈 NEXT STEPS:" -ForegroundColor Cyan
Write-Host " 1. Review the generated CSV file for detailed Service Bus topology" -ForegroundColor White
Write-Host " 2. Analyze messaging patterns and identify optimization opportunities" -ForegroundColor White
Write-Host " 3. Monitor Service Bus performance metrics and throughput" -ForegroundColor White
Write-Host " 4. Consider namespace consolidation for cost optimization" -ForegroundColor White
Write-Host " 5. Implement message monitoring and alerting for critical endpoints" -ForegroundColor White
Write-Host " 6. Review security settings and access policies for each namespace" -ForegroundColor White
Write-Host ""
Write-Host "✅ Azure Service Bus inventory completed successfully!" -ForegroundColor Green
Write-Host "======================================================================================================================================================================"
} catch {
Write-Host ""
Write-Host "❌ CRITICAL ERROR OCCURRED" -ForegroundColor Red
Write-Host "======================================================================================================================================================================"
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Line: $($_.InvocationInfo.ScriptLineNumber)" -ForegroundColor Red
Write-Host "Position: $($_.InvocationInfo.OffsetInLine)" -ForegroundColor Red
Write-Host ""
Write-Host "🔧 TROUBLESHOOTING STEPS:" -ForegroundColor Yellow
Write-Host " 1. Verify you are authenticated to Azure (Connect-AzAccount)" -ForegroundColor White
Write-Host " 2. Ensure you have Service Bus Data Reader permissions on target namespaces" -ForegroundColor White
Write-Host " 3. Check that the Management Group Reader role is assigned" -ForegroundColor White
Write-Host " 4. Verify Azure PowerShell modules are installed and up to date" -ForegroundColor White
Write-Host " 5. Confirm that Service Bus namespaces are accessible and not deleted" -ForegroundColor White
Write-Host " 6. Try running the script with -Verbose for additional diagnostic information" -ForegroundColor White
Write-Host ""
Write-Host "📞 For additional support, contact the Cloud Engineering team" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
# Ensure we exit with error code for automation scenarios
exit 1
} finally {
# Reset progress preference
$ProgressPreference = "Continue"
}

View File

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

View File

@@ -1,48 +1,147 @@
<#
.SYNOPSIS
Exports Azure DevOps pipeline information to a CSV file.
.DESCRIPTION
This script retrieves all build pipelines from an Azure DevOps project and exports detailed information
about each pipeline to a CSV file. It collects pipeline metadata including ID, name, path, type,
author, creation date, pipeline type (Classic or YAML), and edit URLs.
.PARAMETER Token
The Azure DevOps Personal Access Token (PAT) used for authentication. This token must have
'Build (Read)' permissions to access pipeline information.
.PARAMETER Organization
The Azure DevOps organization name. Defaults to "effectory" if not specified.
.PARAMETER Project
The Azure DevOps project name. Defaults to "Survey%20Software" if not specified.
Note: URL-encoded project names should be used (spaces as %20).
.EXAMPLE
.\Pipelines.ps1 -Token "your-personal-access-token"
Exports pipeline information using the default organization and project settings.
.EXAMPLE
.\Pipelines.ps1 -Token "your-pat-token" -Organization "myorg" -Project "MyProject"
Exports pipeline information for a specific organization and project.
.EXAMPLE
.\Pipelines.ps1 -Token "your-pat-token" -Project "My%20Custom%20Project"
Exports pipeline information for a custom project (with URL-encoded name) using the default organization.
.OUTPUTS
Creates a timestamped CSV file in the current directory with the format: "yyyy-MM-dd HHmm pipelines.csv"
The CSV contains the following columns:
- Id: Pipeline ID
- Name: Pipeline name
- Path: Pipeline folder path
- Type: Pipeline type (build/release)
- Author: Pipeline author
- CreatedDate: Pipeline creation date
- PipelineType: Classic or YAML
- PipelineEditUrl: Direct URL to edit the pipeline
.NOTES
Author: Cloud Engineering Team
Created: 2025
Requires: PowerShell 5.1 or later
Dependencies: Internet connectivity to Azure DevOps
The script uses Azure DevOps REST API version 7.1-preview.7 to retrieve pipeline information.
Ensure your Personal Access Token has the necessary permissions before running the script.
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/build/definitions/list
#>
param(
[Parameter(Mandatory = $true, HelpMessage = "Azure DevOps Personal Access Token with Build (Read) permissions")]
[string]$Token,
[Parameter(Mandatory = $false, HelpMessage = "Azure DevOps organization name")]
[string]$Organization = "effectory",
[Parameter(Mandatory = $false, HelpMessage = "Azure DevOps project name (URL-encoded if contains spaces)")]
[string]$Project = "Survey%20Software"
)
# Define a class to structure pipeline information
class PipelineInfo {
[string] $Id = ""
[string] $Name = ""
[string] $Path = ""
[string] $Type = ""
[string] $Author = ""
[string] $CreatedDate = ""
[string] $PipelineType = ""
[string] $PipelineEditUrl = ""
[string] $Id = "" # Pipeline unique identifier
[string] $Name = "" # Pipeline display name
[string] $Path = "" # Pipeline folder path in Azure DevOps
[string] $Type = "" # Pipeline type (build/release)
[string] $Author = "" # Pipeline creator
[string] $CreatedDate = "" # Pipeline creation timestamp
[string] $PipelineType = "" # Classic or YAML pipeline type
[string] $PipelineEditUrl = "" # Direct URL to edit the pipeline
}
$token = "hyrvwxicogy37djvmhkwrcdexokcrpyudkk4j2n3n7gnjb5wsv5a"
$token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($token)"))
$organization = "effectory"
$project = "Survey%20Software"
$head = @{ Authorization =" Basic $token" }
# Encode the Personal Access Token for Basic Authentication
# Azure DevOps requires the token to be base64 encoded with a colon prefix
$encodedToken = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($Token)"))
# Create authentication header for REST API calls
$head = @{ Authorization =" Basic $encodedToken" }
<#
.SYNOPSIS
Determines if a pipeline is Classic or YAML based.
.DESCRIPTION
Makes an additional API call to determine the pipeline process type.
Returns 1 for Classic pipelines, 2 for YAML pipelines.
.PARAMETER pipeLineId
The unique identifier of the pipeline to check.
.OUTPUTS
Integer value: 1 = Classic pipeline, 2 = YAML pipeline
#>
function GetPipelineType {
param (
[int] $pipeLineId
)
$url = "https://dev.azure.com/$organization/$project/_apps/hub/ms.vss-build-web.ci-designer-hub?pipelineId=$pipeLineId&__rt=fps&__ver=2"
# Call Azure DevOps internal API to get pipeline process type
$url = "https://dev.azure.com/$Organization/$Project/_apps/hub/ms.vss-build-web.ci-designer-hub?pipelineId=$pipeLineId&__rt=fps&__ver=2"
$response = Invoke-RestMethod -Uri $url -Method GET -Headers $head
# Extract the definition process type from the response
return $response.fps.dataProviders.data."ms.vss-build-web.pipeline-detail-data-provider".definitionProcessType
}
# Generate timestamped filename for the output CSV
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date pipelines.csv"
# Display script execution banner
Write-Host "========================================================================================================================================================================"
Write-Host "Creating service connection overview."
Write-Host "Creating pipeline overview for Organization: $Organization, Project: $Project"
Write-Host "Output file: $fileName"
Write-Host "========================================================================================================================================================================"
$url="https://dev.azure.com/$organization/$project/_apis/build/definitions?api-version=7.1-preview.7"
# Call Azure DevOps REST API to get all build definitions
$url="https://dev.azure.com/$Organization/$Project/_apis/build/definitions?api-version=7.1-preview.7"
$response = Invoke-RestMethod -Uri $url -Method GET -Headers $head
# Initialize array to store pipeline information
[PipelineInfo[]]$Result = @()
$response.value | ForEach-Object {
# Process each pipeline returned from the API
$response.value | ForEach-Object {
Write-Host "Processing pipeline: $($_.name)" -ForegroundColor Green
# Determine if this is a Classic or YAML pipeline
$definitionProcessType = GetPipelineType -pipeLineId $_.id
# Create new pipeline info object and populate properties
[PipelineInfo] $pipelineInfo = [PipelineInfo]::new()
$pipelineInfo.Id = $_.id
$pipelineInfo.Name = $_.name
@@ -51,15 +150,22 @@ $response.value | ForEach-Object {
$pipelineInfo.Author = $_.authoredby.DisplayName
$pipelineInfo.CreatedDate = $_.createdDate
$pipelineInfo.PipelineType = $definitionProcessType -eq 1 ? "Classic" : "Yaml"
# Generate appropriate edit URL based on pipeline type
$pipelineInfo.PipelineEditUrl = $definitionProcessType -eq 1 ?
"https://dev.azure.com/$organization/$project/_apps/hub/ms.vss-ciworkflow.build-ci-hub?_a=edit-build-definition&id=$($_.id)" :
"https://dev.azure.com/$organization/$project/_apps/hub/ms.vss-build-web.ci-designer-hub?pipelineId=$($_.id)&branch=master"
"https://dev.azure.com/$Organization/$Project/_apps/hub/ms.vss-ciworkflow.build-ci-hub?_a=edit-build-definition&id=$($_.id)" :
"https://dev.azure.com/$Organization/$Project/_apps/hub/ms.vss-build-web.ci-designer-hub?pipelineId=$($_.id)&branch=master"
# Add to results array
$Result += $pipelineInfo
}
# Export results to CSV file
$Result | Export-Csv -Path $fileName -NoTypeInformation
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
# Display completion summary
Write-Host "========================================================================================================================================================================"
Write-Host "Export completed successfully!" -ForegroundColor Green
Write-Host "Total pipelines processed: $($Result.Count)" -ForegroundColor Yellow
Write-Host "Output file: $fileName" -ForegroundColor Yellow
Write-Host "========================================================================================================================================================================"
Write-Host "Done."

View File

@@ -1,36 +1,121 @@
<#
.SYNOPSIS
Exports Azure DevOps pull request information across all repositories to a CSV file.
.DESCRIPTION
This script retrieves all repositories from an Azure DevOps project and then collects
detailed information about all pull requests (active, completed, and abandoned) from each
repository. The information is exported to a timestamped CSV file for analysis and reporting.
.PARAMETER Organization
The Azure DevOps organization name. Defaults to "effectory" if not specified.
.PARAMETER Project
The Azure DevOps project name. Defaults to "survey software" if not specified.
.EXAMPLE
.\PullRequests.ps1
Exports pull request information using the default organization and project settings.
.EXAMPLE
.\PullRequests.ps1 -Organization "myorg" -Project "myproject"
Exports pull request information for a specific organization and project.
.OUTPUTS
Creates a timestamped CSV file in the current directory with the format: "yyyy-MM-dd HHmm pull requests.csv"
The CSV contains the following columns:
- RepositoryId: Repository unique identifier
- RepositoryName: Repository name
- DefaultBranch: Repository default branch
- RepositoryWebUrl: Repository web URL
- PullRequestId: Pull request unique identifier
- PullRequestDate: Pull request creation date
- PullRequestName: Pull request title
- PullRequestCreatedBy: Pull request author
- PullRequestReviewers: Comma-separated list of reviewers
- PullRequestStatus: Pull request status (Active, Completed, Abandoned)
- PullRequestWebUrl: Direct URL to the pull request
- CompletionBypassReason: Reason for bypassing completion requirements (if any)
.NOTES
Author: Cloud Engineering Team
Created: 2025
Requires: PowerShell 5.1 or later, Azure CLI installed and authenticated
Dependencies: Azure CLI (az) must be installed and user must be authenticated
Prerequisites:
- Install Azure CLI: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli
- Authenticate: az login
- Set default subscription if needed: az account set --subscription "subscription-name"
The script processes all active repositories in the specified project and retrieves
all pull requests regardless of their status (active, completed, abandoned).
.LINK
https://docs.microsoft.com/en-us/cli/azure/repos/pr
#>
param(
[Parameter(Mandatory = $false, HelpMessage = "Azure DevOps organization name")]
[string]$Organization = "effectory",
[Parameter(Mandatory = $false, HelpMessage = "Azure DevOps project name")]
[string]$Project = "survey software"
)
# Define a class to structure pull request information
class PullRequest {
[string] $RepositoryId = ""
[string] $RepositoryName = ""
[string] $DefaultBranch = ""
[string] $RepositoryWebUrl = ""
[string] $PullRequestId = ""
[string] $PullRequestDate = ""
[string] $PullRequestName = ""
[string] $PullRequestCreatedBy = ""
[string] $PullRequestReviewers = ""
[string] $PullRequestStatus = ""
[string] $PullRequestWebUrl = ""
[string] $CompletionBypassReason = ""
[string] $RepositoryId = "" # Repository unique identifier
[string] $RepositoryName = "" # Repository display name
[string] $DefaultBranch = "" # Repository default branch (e.g., main, master)
[string] $RepositoryWebUrl = "" # Repository web URL in Azure DevOps
[string] $PullRequestId = "" # Pull request unique identifier
[string] $PullRequestDate = "" # Pull request creation timestamp
[string] $PullRequestName = "" # Pull request title/name
[string] $PullRequestCreatedBy = "" # Pull request author display name
[string] $PullRequestReviewers = "" # Comma-separated list of reviewer names
[string] $PullRequestStatus = "" # PR status: Active, Completed, Abandoned
[string] $PullRequestWebUrl = "" # Direct URL to view the pull request
[string] $CompletionBypassReason = "" # Reason for bypassing completion policies
}
# Generate timestamped filename for the output CSV
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date pull requests.csv"
# Display script execution banner
Write-Host "========================================================================================================================================================================"
Write-Host "Creating pull request overview."
Write-Host "Creating pull request overview for Organization: $Organization, Project: $Project"
Write-Host "Output file: $fileName"
Write-Host "Note: This script requires Azure CLI to be installed and authenticated (az login)"
Write-Host "========================================================================================================================================================================"
$repos = az repos list --organization "https://dev.azure.com/effectory/" --project "survey software" | ConvertFrom-Json | Select-Object | Where-Object { $true -ne $_.isDisabled }
# Retrieve all active repositories from the Azure DevOps project
Write-Host "Fetching repositories from project '$Project'..." -ForegroundColor Yellow
$repos = az repos list --organization "https://dev.azure.com/$Organization/" --project "$Project" | ConvertFrom-Json | Select-Object | Where-Object { $true -ne $_.isDisabled }
Write-Host "Found $($repos.Count) active repositories" -ForegroundColor Green
# Process each repository to collect pull request information
$totalPullRequests = 0
foreach ($repo in $repos)
{
$prs = az repos pr list --project "survey software" --repository "$($repo.name)" --organization "https://dev.azure.com/effectory/" --status all | ConvertFrom-Json | Select-Object
Write-Host "Processing repository: $($repo.name)" -ForegroundColor Cyan
# Retrieve all pull requests from the current repository (all statuses: active, completed, abandoned)
$prs = az repos pr list --project "$Project" --repository "$($repo.name)" --organization "https://dev.azure.com/$Organization/" --status all | ConvertFrom-Json | Select-Object
Write-Host " Found $($prs.Count) pull requests" -ForegroundColor Gray
# Initialize array to store pull request information for this repository
[PullRequest[]]$Result = @()
# Process each pull request in the current repository
foreach ($pr in $prs)
{
# Create new pull request object and populate all properties
[PullRequest] $pullRequest = [PullRequest]::new()
$pullRequest.RepositoryId = $repo.id
$pullRequest.RepositoryName = $repo.name
@@ -40,17 +125,30 @@ foreach ($repo in $repos)
$pullRequest.PullRequestDate = $pr.creationDate
$pullRequest.PullRequestName = $pr.title
$pullRequest.PullRequestCreatedBy = $pr.createdBy.displayName
# Join all reviewer names into a comma-separated string
$pullRequest.PullRequestReviewers = $pr.reviewers | join-string -property displayName -Separator ','
$pullRequest.PullRequestStatus = $pr.status
# Construct direct URL to the pull request
$pullRequest.PullRequestWebUrl = "$($repo.webUrl)/pullrequest/$($pr.pullRequestId)"
# Capture bypass reason if completion policies were bypassed
$pullRequest.CompletionBypassReason = $pr.completionOptions.bypassReason
# Add to results array
$Result += $pullRequest
}
# Append results for this repository to the CSV file
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
$totalPullRequests += $Result.Count
}
# Display completion summary
Write-Host "========================================================================================================================================================================"
Write-Host "Export completed successfully!" -ForegroundColor Green
Write-Host "Total repositories processed: $($repos.Count)" -ForegroundColor Yellow
Write-Host "Total pull requests exported: $totalPullRequests" -ForegroundColor Yellow
Write-Host "Output file: $fileName" -ForegroundColor Yellow
Write-Host "========================================================================================================================================================================"
Write-Host "Done."

View File

@@ -1,30 +1,110 @@
<#
.SYNOPSIS
Exports Azure DevOps repository information along with last completed pull request details to a CSV file.
.DESCRIPTION
This script retrieves all repositories from an Azure DevOps project and collects detailed information
about each repository including basic metadata and information about the most recent completed pull request.
For active repositories, it identifies the last completed PR and captures details about the author,
reviewers, and other relevant information. The data is exported to a timestamped CSV file for analysis.
.PARAMETER Organization
The Azure DevOps organization name. Defaults to "effectory" if not specified.
.PARAMETER Project
The Azure DevOps project name. Defaults to "survey software" if not specified.
.EXAMPLE
.\Repositories.ps1
Exports repository information using the default organization and project settings.
.EXAMPLE
.\Repositories.ps1 -Organization "myorg" -Project "myproject"
Exports repository information for a specific organization and project.
.OUTPUTS
Creates a timestamped CSV file in the current directory with the format: "yyyy-MM-dd HHmm repositories.csv"
The CSV contains the following columns:
- Id: Repository unique identifier
- Name: Repository name
- DefaultBranch: Repository default branch (e.g., main, master)
- IsDisabled: Boolean indicating if the repository is disabled
- WebUrl: Repository web URL in Azure DevOps
- LastPRDate: Creation date of the most recent completed pull request
- LastPRName: Title of the most recent completed pull request
- LastPRCreatedBy: Author of the most recent completed pull request
- LastPRReviewers: Comma-separated list of reviewers for the most recent completed PR
- LastPRUrl: Direct URL to the most recent completed pull request
.NOTES
Author: Cloud Engineering Team
Created: 2025
Requires: PowerShell 5.1 or later, Azure CLI installed and authenticated
Dependencies: Azure CLI (az) must be installed and user must be authenticated
Prerequisites:
- Install Azure CLI: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli
- Authenticate: az login
- Set default subscription if needed: az account set --subscription "subscription-name"
The script processes all repositories in the specified project, including disabled ones.
For disabled repositories, pull request information will be empty as they cannot be accessed.
Only completed pull requests are considered when determining the "last" PR.
.LINK
https://docs.microsoft.com/en-us/cli/azure/repos
#>
param(
[Parameter(Mandatory=$false, HelpMessage="Azure DevOps organization name")]
[string]$Organization = "effectory",
[Parameter(Mandatory=$false, HelpMessage="Azure DevOps project name")]
[string]$Project = "survey software"
)
# Define a class to structure repository information
class Repository {
[string] $Id = ""
[string] $Name = ""
[string] $DefaultBranch = ""
[string] $IsDisabled = ""
[string] $WebUrl = ""
[string] $LastPRDate = ""
[string] $LastPRName = ""
[string] $LastPRCreatedBy = ""
[string] $LastPRReviewers = ""
[string] $LastPRUrl = ""
[string] $Id = "" # Repository unique identifier
[string] $Name = "" # Repository display name
[string] $DefaultBranch = "" # Repository default branch (e.g., main, master)
[string] $IsDisabled = "" # Whether the repository is disabled (True/False)
[string] $WebUrl = "" # Repository web URL in Azure DevOps
[string] $LastPRDate = "" # Creation date of most recent completed PR
[string] $LastPRName = "" # Title of most recent completed PR
[string] $LastPRCreatedBy = "" # Author of most recent completed PR
[string] $LastPRReviewers = "" # Comma-separated list of reviewers
[string] $LastPRUrl = "" # Direct URL to most recent completed PR
}
# Generate timestamped filename for the output CSV
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date repositories.csv"
# Display script execution banner
Write-Host "========================================================================================================================================================================"
Write-Host "Creating repository overview."
Write-Host "Creating repository overview for Organization: $Organization, Project: $Project"
Write-Host "Output file: $fileName"
Write-Host "Note: This script requires Azure CLI to be installed and authenticated (az login)"
Write-Host "========================================================================================================================================================================"
$repos = az repos list --organization "https://dev.azure.com/effectory/" --project "survey software" | ConvertFrom-Json | Select-Object
# Retrieve all repositories from the Azure DevOps project
Write-Host "Fetching repositories from project '$Project'..." -ForegroundColor Yellow
$repos = az repos list --organization "https://dev.azure.com/$Organization/" --project "$Project" | ConvertFrom-Json | Select-Object
Write-Host "Found $($repos.Count) repositories" -ForegroundColor Green
# Initialize array to store repository information
[Repository[]]$Result = @()
# Process each repository to collect information and last PR details
foreach ($repo in $repos)
{
Write-Host "Processing repository: $($repo.name)" -ForegroundColor Cyan
# Create new repository object and populate basic information
[Repository] $repository = [Repository]::new()
$repository.Id = $repo.id
$repository.Name = $repo.name
@@ -32,26 +112,56 @@ foreach ($repo in $repos)
$repository.IsDisabled = $repo.isDisabled
$repository.WebUrl = $repo.webUrl
# Only attempt to get pull request information for active repositories
if ($true -ne $repo.isDisabled)
{
$lastPr = az repos pr list --project "survey software" --repository $repo.name --organization "https://dev.azure.com/effectory/" --status completed --top 1 | ConvertFrom-Json | Select-Object
Write-Host " Fetching last completed pull request..." -ForegroundColor Gray
# Get the most recent completed pull request (top 1, sorted by most recent)
$lastPr = az repos pr list --project "$Project" --repository $repo.name --organization "https://dev.azure.com/$Organization/" --status completed --top 1 | ConvertFrom-Json | Select-Object
# If a completed PR exists, capture its details
if ($lastPr)
{
$repository.LastPRDate = $lastPr.creationDate
$repository.LastPRName = $lastPr.title
$repository.LastPRUrl = $lastPr.url
$repository.LastPRCreatedBy = $lastPr.createdBy.displayName
# Join all reviewer names into a comma-separated string
$repository.LastPRReviewers = $lastPr.reviewers | join-string -property displayName -Separator ','
Write-Host " Last PR: $($lastPr.title) ($(Get-Date $lastPr.creationDate -Format 'yyyy-MM-dd'))" -ForegroundColor Gray
}
else
{
Write-Host " No completed pull requests found" -ForegroundColor Gray
}
}
else
{
Write-Host " Repository is disabled - skipping pull request analysis" -ForegroundColor Yellow
}
# Add repository to results array
$Result += $repository
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
# Export results to CSV file
$Result | Export-Csv -Path $fileName -NoTypeInformation
# Display completion summary
Write-Host "========================================================================================================================================================================"
Write-Host "Done."
Write-Host "Export completed successfully!" -ForegroundColor Green
Write-Host "Total repositories processed: $($Result.Count)" -ForegroundColor Yellow
# az repos pr list --project "survey software" --repository "ProjectCenter" --organization "https://dev.azure.com/effectory/" --status all --top 1
# Count repositories with and without recent PRs
$reposWithPRs = ($Result | Where-Object { $_.LastPRDate -ne "" }).Count
$activeRepos = ($Result | Where-Object { $_.IsDisabled -ne "True" }).Count
$disabledRepos = ($Result | Where-Object { $_.IsDisabled -eq "True" }).Count
Write-Host "Active repositories: $activeRepos" -ForegroundColor Yellow
Write-Host "Disabled repositories: $disabledRepos" -ForegroundColor Yellow
Write-Host "Repositories with completed PRs: $reposWithPRs" -ForegroundColor Yellow
Write-Host "Output file: $fileName" -ForegroundColor Yellow
Write-Host "========================================================================================================================================================================"

View File

@@ -1,41 +1,144 @@
<#
.SYNOPSIS
Analyzes Azure DevOps repositories to identify which have 'test' and 'accept' branches and their last activity.
.DESCRIPTION
This script retrieves all repositories from an Azure DevOps project and analyzes each one to determine:
- Basic repository information (ID, name, default branch, status, URL)
- Last commit activity on the default branch
- Whether a 'test' branch exists and its last commit activity
- Whether an 'accept' branch exists and its last commit activity
This is commonly used in development workflows where 'test' and 'accept' branches represent
specific deployment environments or approval stages in the development pipeline.
.PARAMETER Token
The Azure DevOps Personal Access Token (PAT) used for authentication. This token must have
'Code (Read)' permissions to access repository and commit information.
.PARAMETER Organization
The Azure DevOps organization name. Defaults to "effectory" if not specified.
.PARAMETER Project
The Azure DevOps project name. Defaults to "Survey Software" if not specified.
.EXAMPLE
.\RepositoriesWithTestAccept.ps1 -Token "your-personal-access-token"
Analyzes repositories using the default organization and project settings.
.EXAMPLE
.\RepositoriesWithTestAccept.ps1 -Token "your-pat-token" -Organization "myorg" -Project "MyProject"
Analyzes repositories for a specific organization and project.
.OUTPUTS
Creates a timestamped CSV file in the current directory with the format: "yyyy-MM-dd HHmm repositories with test and accept.csv"
The CSV contains the following columns:
- Id: Repository unique identifier
- Name: Repository name
- DefaultBranch: Repository default branch (e.g., main, master)
- IsDisabled: Boolean indicating if the repository is disabled
- WebUrl: Repository web URL in Azure DevOps
- LastDefaultChange: Date of last commit on the default branch
- HasTest: Boolean indicating if a 'test' branch exists (True/False)
- LastTestChange: Date of last commit on the 'test' branch (empty if branch doesn't exist)
- HasAccept: Boolean indicating if an 'accept' branch exists (True/False)
- LastAcceptChange: Date of last commit on the 'accept' branch (empty if branch doesn't exist)
.NOTES
Author: Cloud Engineering Team
Created: 2025
Requires: PowerShell 5.1 or later, Azure CLI installed and authenticated
Dependencies: Azure CLI (az) for repository listing, Azure DevOps REST API for commit information
Prerequisites:
- Install Azure CLI: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli
- Authenticate: az login
- Azure DevOps Personal Access Token with Code (Read) permissions
The script uses both Azure CLI commands and REST API calls:
- Azure CLI for listing repositories
- REST API for checking branch existence and commit history
Branch Analysis:
- 'test' branch: Often used for testing environment deployments
- 'accept' branch: Often used for acceptance testing or staging environments
- Default branch: Usually 'main' or 'master', represents the primary development branch
Error Handling:
- If a branch doesn't exist, the API call will fail and the branch is marked as non-existent
- Disabled repositories are processed but branch analysis is skipped
- Network or authentication errors are handled gracefully
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/git/commits/get-commits
#>
param(
[Parameter(Mandatory = $true, HelpMessage = "Azure DevOps Personal Access Token with Code (Read) permissions")]
[string]$Token,
[Parameter(Mandatory = $false, HelpMessage = "Azure DevOps organization name")]
[string]$Organization = "effectory",
[Parameter(Mandatory = $false, HelpMessage = "Azure DevOps project name")]
[string]$Project = "Survey Software"
)
# Define a class to structure repository information with branch analysis
class Repository {
[string] $Id = ""
[string] $Name = ""
[string] $DefaultBranch = ""
[string] $IsDisabled = ""
[string] $WebUrl = ""
[string] $LastDefaultChange = ""
[string] $HasTest = ""
[string] $LastTestChange = ""
[string] $HasAccept = ""
[string] $LastAcceptChange = ""
[string] $Id = "" # Repository unique identifier
[string] $Name = "" # Repository display name
[string] $DefaultBranch = "" # Repository default branch (e.g., main, master)
[string] $IsDisabled = "" # Whether the repository is disabled (True/False)
[string] $WebUrl = "" # Repository web URL in Azure DevOps
[string] $LastDefaultChange = "" # Date of last commit on default branch
[string] $HasTest = "" # Whether 'test' branch exists (True/False)
[string] $LastTestChange = "" # Date of last commit on 'test' branch
[string] $HasAccept = "" # Whether 'accept' branch exists (True/False)
[string] $LastAcceptChange = "" # Date of last commit on 'accept' branch
}
# Initialize variables for API calls
[string] $url = ""
[string] $repositoryId = ""
[string] $branchName = ""
# Generate timestamped filename for the output CSV
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date repositories with test and accept.csv"
[string] $token = "yixqmupncd3b72zij4y5lfsenepak5rtvlba3sj33tvxvc4s7a6q" #"{INSERT_PERSONAL_ACCESS_TOKEN}"
$token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($token)"))
$head = @{ Authorization =" Basic $token" }
[string] $organization = "effectory"
[string] $project = "Survey%20Software"
# Prepare authentication for Azure DevOps REST API calls
# Personal Access Token must be base64 encoded with a colon prefix
$encodedToken = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($Token)"))
$head = @{ Authorization =" Basic $encodedToken" }
# URL-encode the project name for API calls
$projectEncoded = $Project -replace " ", "%20"
# Display script execution banner
Write-Host "========================================================================================================================================================================"
Write-Host "Creating repository overview."
Write-Host "Analyzing repositories for 'test' and 'accept' branches"
Write-Host "Organization: $Organization, Project: $Project"
Write-Host "Output file: $fileName"
Write-Host "========================================================================================================================================================================"
$repos = az repos list --organization "https://dev.azure.com/$organization/" --project "survey software" | ConvertFrom-Json | Select-Object
# Retrieve all repositories from the Azure DevOps project
Write-Host "Fetching repositories from project '$Project'..." -ForegroundColor Yellow
$repos = az repos list --organization "https://dev.azure.com/$Organization/" --project "$Project" | ConvertFrom-Json | Select-Object
Write-Host "Found $($repos.Count) repositories" -ForegroundColor Green
# Initialize array to store repository analysis results
[Repository[]]$Result = @()
# Process each repository to analyze branch structure and activity
foreach ($repo in $repos)
{
Write-Host $repo.name
Write-Host "Analyzing repository: $($repo.name)" -ForegroundColor Cyan
# Create new repository object and populate basic information
[Repository] $repository = [Repository]::new()
$repository.Id = $repo.id
$repository.Name = $repo.name
@@ -43,50 +146,96 @@ foreach ($repo in $repos)
$repository.IsDisabled = $repo.isDisabled
$repository.WebUrl = $repo.webUrl
# Only analyze branches for active repositories
if ($true -ne $repo.isDisabled)
{
$repositoryId = $repo.id
# Analyze default branch activity
$branchName = $repo.defaultBranch
$branchName = $branchName.Replace("refs/heads/", "")
Write-Host " Checking default branch: $branchName" -ForegroundColor Gray
try {
$url="https://dev.azure.com/$organization/$project/_apis/git/repositories/$repositoryId/commits?searchCriteria.itemVersion.version=$branchName&searchCriteria.`$top=1&api-version=6.0"
$url="https://dev.azure.com/$Organization/$projectEncoded/_apis/git/repositories/$repositoryId/commits?searchCriteria.itemVersion.version=$branchName&searchCriteria.`$top=1&api-version=6.0"
$response = Invoke-RestMethod -Uri $url -Method GET -Headers $head
$repository.LastDefaultChange = $response.value[0].committer.date
Write-Host " Last commit: $(Get-Date $response.value[0].committer.date -Format 'yyyy-MM-dd HH:mm')" -ForegroundColor Gray
}
catch {
Write-Host " No commits found or branch inaccessible" -ForegroundColor Yellow
$repository.LastDefaultChange = ""
}
# Check for 'test' branch existence and activity
Write-Host " Checking for 'test' branch..." -ForegroundColor Gray
try {
$branchName = "test"
$url="https://dev.azure.com/$organization/$project/_apis/git/repositories/$repositoryId/commits?searchCriteria.itemVersion.version=$branchName&searchCriteria.`$top=1&api-version=6.0"
$url="https://dev.azure.com/$Organization/$projectEncoded/_apis/git/repositories/$repositoryId/commits?searchCriteria.itemVersion.version=$branchName&searchCriteria.`$top=1&api-version=6.0"
$response = Invoke-RestMethod -Uri $url -Method GET -Headers $head
$repository.HasTest = "True"
$repository.LastTestChange = $response.value[0].committer.date
Write-Host " 'test' branch found - Last commit: $(Get-Date $response.value[0].committer.date -Format 'yyyy-MM-dd HH:mm')" -ForegroundColor Green
}
catch {
$repository.HasTest = "False"
$repository.LastTestChange = ""
Write-Host " 'test' branch not found" -ForegroundColor Yellow
}
# Check for 'accept' branch existence and activity
Write-Host " Checking for 'accept' branch..." -ForegroundColor Gray
try {
$branchName = "accept"
$url="https://dev.azure.com/$organization/$project/_apis/git/repositories/$repositoryId/commits?searchCriteria.itemVersion.version=$branchName&searchCriteria.`$top=1&api-version=6.0"
$url="https://dev.azure.com/$Organization/$projectEncoded/_apis/git/repositories/$repositoryId/commits?searchCriteria.itemVersion.version=$branchName&searchCriteria.`$top=1&api-version=6.0"
$response = Invoke-RestMethod -Uri $url -Method GET -Headers $head
$repository.HasAccept = "True"
$repository.LastAcceptChange = $response.value[0].committer.date
Write-Host " 'accept' branch found - Last commit: $(Get-Date $response.value[0].committer.date -Format 'yyyy-MM-dd HH:mm')" -ForegroundColor Green
}
catch {
$repository.HasAccept = "False"
$repository.LastAcceptChange = ""
Write-Host " 'accept' branch not found" -ForegroundColor Yellow
}
}
else
{
Write-Host " Repository is disabled - skipping branch analysis" -ForegroundColor Yellow
# Set default values for disabled repositories
$repository.LastDefaultChange = ""
$repository.HasTest = "N/A"
$repository.LastTestChange = ""
$repository.HasAccept = "N/A"
$repository.LastAcceptChange = ""
}
# Add repository to results array
$Result += $repository
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
# Export results to CSV file
$Result | Export-Csv -Path $fileName -NoTypeInformation
# Calculate and display summary statistics
$totalRepos = $Result.Count
$activeRepos = ($Result | Where-Object { $_.IsDisabled -ne "True" }).Count
$disabledRepos = ($Result | Where-Object { $_.IsDisabled -eq "True" }).Count
$reposWithTest = ($Result | Where-Object { $_.HasTest -eq "True" }).Count
$reposWithAccept = ($Result | Where-Object { $_.HasAccept -eq "True" }).Count
$reposWithBoth = ($Result | Where-Object { $_.HasTest -eq "True" -and $_.HasAccept -eq "True" }).Count
# Display completion summary
Write-Host "========================================================================================================================================================================"
Write-Host "Branch analysis completed successfully!" -ForegroundColor Green
Write-Host ""
Write-Host "SUMMARY STATISTICS:" -ForegroundColor Cyan
Write-Host "Total repositories: $totalRepos" -ForegroundColor Yellow
Write-Host "Active repositories: $activeRepos" -ForegroundColor Yellow
Write-Host "Disabled repositories: $disabledRepos" -ForegroundColor Yellow
Write-Host "Repositories with 'test' branch: $reposWithTest" -ForegroundColor Yellow
Write-Host "Repositories with 'accept' branch: $reposWithAccept" -ForegroundColor Yellow
Write-Host "Repositories with both 'test' and 'accept' branches: $reposWithBoth" -ForegroundColor Yellow
Write-Host ""
Write-Host "Output file: $fileName" -ForegroundColor Yellow
Write-Host "========================================================================================================================================================================"
Write-Host "Done."

View File

@@ -1,55 +1,256 @@
<#
.SYNOPSIS
Exports Azure DevOps service connections with detailed service principal information to a CSV file.
.DESCRIPTION
This script retrieves all service connections from an Azure DevOps project and analyzes their
associated service principals. It combines information from Azure DevOps REST API and Azure
PowerShell cmdlets to provide comprehensive details about:
- Service connection metadata (ID, name, status, authorization scheme)
- Associated service principal details (Application ID, Object ID, display name)
- Service principal credential expiration dates
This is particularly useful for security auditing and monitoring service principal
credential expiration to prevent service disruptions.
.PARAMETER Token
The Azure DevOps Personal Access Token (PAT) used for authentication. This token must have
'Service Connections (Read)' permissions to access service endpoint information.
.PARAMETER Organization
The Azure DevOps organization name. Defaults to "effectory" if not specified.
.PARAMETER Project
The Azure DevOps project name. Defaults to "Survey Software" if not specified.
.EXAMPLE
.\ServiceConnections.ps1 -Token "your-personal-access-token"
Analyzes service connections using the default organization and project settings.
.EXAMPLE
.\ServiceConnections.ps1 -Token "your-pat-token" -Organization "myorg" -Project "MyProject"
Analyzes service connections for a specific organization and project.
.OUTPUTS
Creates a timestamped CSV file in the current directory with the format: "yyyy-MM-dd HHmm serviceconnections.csv"
The CSV contains the following columns:
- Id: Service connection unique identifier
- Name: Service connection display name
- OperationStatus: Current operational status of the service connection
- AuthorizationScheme: Authentication method used (e.g., ServicePrincipal, ManagedServiceIdentity)
- ServicePrincipalApplicationId: Application (Client) ID of the associated service principal
- ServicePrincipalObjectId: Object ID of the service principal in Azure AD
- ServicePrincipalName: Display name of the service principal
- ServicePrincipalEndDateTime: Expiration date of the service principal credentials
.NOTES
Author: Cloud Engineering Team
Created: 2025
Requires: PowerShell 5.1 or later, Az PowerShell module
Dependencies: Az PowerShell module must be installed and authenticated to Azure
Prerequisites:
- Install Az PowerShell module: Install-Module -Name Az
- Connect to Azure: Connect-AzAccount
- Azure DevOps Personal Access Token with Service Connections (Read) permissions
- Appropriate Azure AD permissions to read service principal information
The script combines two data sources:
1. Azure DevOps REST API - for service connection metadata
2. Azure PowerShell cmdlets - for detailed service principal information
Security Considerations:
- Monitor credential expiration dates to prevent service disruptions
- Regular auditing of service connections helps maintain security posture
- Service principals with expired credentials will cause deployment failures
Error Handling:
- If service principal information cannot be retrieved, those fields will be empty
- The script continues processing even if individual service principals fail
- Network or authentication errors are handled gracefully
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/serviceendpoint/endpoints
https://docs.microsoft.com/en-us/powershell/module/az.resources/get-azadserviceprincipal
#>
param(
[Parameter(Mandatory = $true, HelpMessage = "Azure DevOps Personal Access Token with Service Connections (Read) permissions")]
[string]$Token,
[Parameter(Mandatory = $false, HelpMessage = "Azure DevOps organization name")]
[string]$Organization = "effectory",
[Parameter(Mandatory = $false, HelpMessage = "Azure DevOps project name")]
[string]$Project = "Survey Software"
)
# Define a class to structure service connection information with service principal details
class ServiceConnection {
[string] $Id = ""
[string] $Name = ""
[string] $OperationStatus = ""
[string] $AuthorizationScheme = ""
[string] $ServicePrincipalApplicationId = ""
[string] $ServicePrincipalObjectId = ""
[string] $ServicePrincipalName = ""
[string] $ServicePrincipalEndDateTime = ""
[string] $Id = "" # Service connection unique identifier
[string] $Name = "" # Service connection display name
[string] $OperationStatus = "" # Current operational status
[string] $AuthorizationScheme = "" # Authentication method (e.g., ServicePrincipal)
[string] $ServicePrincipalApplicationId = "" # Application (Client) ID of service principal
[string] $ServicePrincipalObjectId = "" # Object ID of service principal in Azure AD
[string] $ServicePrincipalName = "" # Display name of service principal
[string] $ServicePrincipalEndDateTime = "" # Expiration date of service principal credentials
}
# Generate timestamped filename for the output CSV
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date serviceconnections.csv"
# Display script execution banner
Write-Host "========================================================================================================================================================================"
Write-Host "Creating service connection overview."
Write-Host "Analyzing Azure DevOps service connections and associated service principals"
Write-Host "Organization: $Organization, Project: $Project"
Write-Host "Output file: $fileName"
Write-Host "Note: This script requires Azure PowerShell (Az module) to be installed and authenticated"
Write-Host "========================================================================================================================================================================"
$token = "adlgsqh2uoedv6rf44hjd47z3ssuo5zonrqicif4ctjqlqqtlhdq"
$token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($token)"))
$organization = "effectory"
$project = "Survey%20Software"
# Prepare authentication for Azure DevOps REST API calls
# Personal Access Token must be base64 encoded with a colon prefix
$encodedToken = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($Token)"))
$url="https://dev.azure.com/$organization/$project/_apis/serviceendpoint/endpoints?api-version=6.0-preview.4"
$head = @{ Authorization =" Basic $token" }
# URL-encode the project name for API calls
$projectEncoded = $Project -replace " ", "%20"
# Check if Azure PowerShell is available and user is authenticated
Write-Host "Verifying Azure PowerShell authentication..." -ForegroundColor Yellow
try {
$azContext = Get-AzContext
if (-not $azContext) {
Write-Host "ERROR: Not authenticated to Azure. Please run 'Connect-AzAccount' first." -ForegroundColor Red
exit 1
}
Write-Host "Azure authentication verified - Tenant: $($azContext.Tenant.Id)" -ForegroundColor Green
}
catch {
Write-Host "ERROR: Azure PowerShell module not found. Please install with 'Install-Module -Name Az'" -ForegroundColor Red
exit 1
}
# Retrieve service connections from Azure DevOps
Write-Host "Fetching service connections from Azure DevOps..." -ForegroundColor Yellow
$url="https://dev.azure.com/$Organization/$projectEncoded/_apis/serviceendpoint/endpoints?api-version=6.0-preview.4"
$head = @{ Authorization =" Basic $encodedToken" }
$response = Invoke-RestMethod -Uri $url -Method GET -Headers $head
Write-Host "Found $($response.count) service connections" -ForegroundColor Green
# Initialize array to store service connection analysis results
[ServiceConnection[]]$Result = @()
# Process each service connection to gather detailed information
$response.value | ForEach-Object {
Write-Host "Analyzing service connection: $($_.name)" -ForegroundColor Cyan
# Create new service connection object and populate basic information
[ServiceConnection] $serviceConnection = [ServiceConnection]::new()
$serviceConnection.Id = $_.id
$serviceConnection.Name = $_.name
$serviceConnection.OperationStatus = $_.operationStatus
$serviceConnection.AuthorizationScheme = $_.authorization.scheme
Write-Host " Authorization scheme: $($_.authorization.scheme)" -ForegroundColor Gray
Write-Host " Operation status: $($_.operationStatus)" -ForegroundColor Gray
# Extract service principal information if available
$principalid = $_.authorization.parameters.serviceprincipalid
if ($null -ne $principalid) {
$principal = Get-AzADServicePrincipal -ApplicationId $principalid
$credential = Get-AzADAppCredential -ApplicationId $principalid
Write-Host " Retrieving service principal details..." -ForegroundColor Gray
try {
# Get service principal information from Azure AD
$principal = Get-AzADServicePrincipal -ApplicationId $principalid -ErrorAction Stop
$credential = Get-AzADAppCredential -ApplicationId $principalid -ErrorAction Stop
$serviceConnection.ServicePrincipalApplicationId = $principalid
$serviceConnection.ServicePrincipalObjectId = $principal.Id
$serviceConnection.ServicePrincipalName = $principal.DisplayName
$serviceConnection.ServicePrincipalEndDateTime = $credential.EndDateTime
# Handle multiple credentials - get the latest expiration date
if ($credential) {
$latestExpiration = ($credential | Sort-Object EndDateTime -Descending | Select-Object -First 1).EndDateTime
$serviceConnection.ServicePrincipalEndDateTime = $latestExpiration
Write-Host " Service Principal: $($principal.DisplayName)" -ForegroundColor Gray
Write-Host " Application ID: $principalid" -ForegroundColor Gray
Write-Host " Credential expires: $latestExpiration" -ForegroundColor Gray
# Warn about expiring credentials (within 30 days)
if ($latestExpiration -and $latestExpiration -lt (Get-Date).AddDays(30)) {
Write-Host " WARNING: Credential expires within 30 days!" -ForegroundColor Red
}
}
else {
Write-Host " No credentials found for this service principal" -ForegroundColor Yellow
}
}
catch {
Write-Host " Error retrieving service principal details: $($_.Exception.Message)" -ForegroundColor Red
# Keep the Application ID even if other details fail
$serviceConnection.ServicePrincipalApplicationId = $principalid
}
}
else {
Write-Host " No service principal associated with this connection" -ForegroundColor Gray
}
# Add service connection to results array
$Result += $serviceConnection
}
}
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
# Export results to CSV file
$Result | Export-Csv -Path $fileName -NoTypeInformation
# Calculate and display summary statistics
$totalConnections = $Result.Count
$connectionsWithServicePrincipals = ($Result | Where-Object { $_.ServicePrincipalApplicationId -ne "" }).Count
$connectionsWithoutServicePrincipals = $totalConnections - $connectionsWithServicePrincipals
# Check for expiring credentials (within 30 days)
$expiringCredentials = $Result | Where-Object {
$_.ServicePrincipalEndDateTime -ne "" -and
[DateTime]::Parse($_.ServicePrincipalEndDateTime) -lt (Get-Date).AddDays(30)
}
# Check for expired credentials
$expiredCredentials = $Result | Where-Object {
$_.ServicePrincipalEndDateTime -ne "" -and
[DateTime]::Parse($_.ServicePrincipalEndDateTime) -lt (Get-Date)
}
# Display completion summary
Write-Host "========================================================================================================================================================================"
Write-Host "Service connection analysis completed successfully!" -ForegroundColor Green
Write-Host ""
Write-Host "SUMMARY STATISTICS:" -ForegroundColor Cyan
Write-Host "Total service connections: $totalConnections" -ForegroundColor Yellow
Write-Host "Connections with service principals: $connectionsWithServicePrincipals" -ForegroundColor Yellow
Write-Host "Connections without service principals: $connectionsWithoutServicePrincipals" -ForegroundColor Yellow
if ($expiredCredentials.Count -gt 0) {
Write-Host ""
Write-Host "EXPIRED CREDENTIALS (IMMEDIATE ATTENTION REQUIRED):" -ForegroundColor Red
$expiredCredentials | ForEach-Object {
Write-Host " - $($_.Name): $($_.ServicePrincipalName) (expired $($_.ServicePrincipalEndDateTime))" -ForegroundColor Red
}
}
if ($expiringCredentials.Count -gt 0) {
Write-Host ""
Write-Host "EXPIRING CREDENTIALS (WITHIN 30 DAYS):" -ForegroundColor Yellow
$expiringCredentials | ForEach-Object {
Write-Host " - $($_.Name): $($_.ServicePrincipalName) (expires $($_.ServicePrincipalEndDateTime))" -ForegroundColor Yellow
}
}
Write-Host ""
Write-Host "Output file: $fileName" -ForegroundColor Yellow
Write-Host "========================================================================================================================================================================"
Write-Host "Done."

View File

@@ -1,20 +1,129 @@
# PowerShell script to analyze renovate PRs across repositories with detailed statistics
<#
.SYNOPSIS
Analyzes Renovate pull requests across all repositories in an Azure DevOps project and generates detailed statistics.
.DESCRIPTION
This script connects to an Azure DevOps organization and project to analyze Renovate dependency update pull requests.
It processes all repositories (active, disabled, and locked) and generates comprehensive statistics including:
- Total Renovate PRs per repository
- Open, completed, and abandoned PR counts
- Latest creation and completion dates
- Branch information for latest PRs
- Repository status (active, disabled, locked, error)
The script outputs both a detailed text report and a CSV file for further analysis.
Renovate PRs are identified by their branch naming pattern: "refs/heads/renovate/*"
.PARAMETER Organization
The Azure DevOps organization name. Defaults to "effectory" if not specified.
.PARAMETER Project
The Azure DevOps project name. Defaults to "Survey Software" if not specified.
.PARAMETER PAT
The Azure DevOps Personal Access Token (PAT) used for authentication. This token must have
'Code (Read)' permissions to access repository and pull request information.
.PARAMETER OutputFile
The path and filename for the output text report. Defaults to a timestamped filename:
"RenovatePRs_Stats_yyyyMMdd_HHmmss.txt"
.EXAMPLE
.\renovate-stats.ps1 -PAT "your-personal-access-token"
Analyzes Renovate PRs using default organization and project settings.
.EXAMPLE
.\renovate-stats.ps1 -PAT "your-pat-token" -Organization "myorg" -Project "MyProject"
Analyzes Renovate PRs for a specific organization and project.
.EXAMPLE
.\renovate-stats.ps1 -PAT "your-token" -OutputFile "C:\Reports\renovate-analysis.txt"
Analyzes Renovate PRs and saves the report to a custom location.
.OUTPUTS
Creates two output files:
1. Text Report: Detailed statistics with tables and summaries (.txt)
2. CSV Export: Raw data for further analysis (.csv)
The text report includes:
- Repository-by-repository statistics table (sorted by last created date)
- Summary statistics (total repos, repos with/without Renovate, PR counts)
- List of disabled/locked/error repositories
The CSV contains columns:
- Repository: Repository name
- TotalRenovatePRs: Total count of Renovate PRs
- OpenPRs: Count of active Renovate PRs
- CompletedPRs: Count of completed Renovate PRs
- AbandonedPRs: Count of abandoned Renovate PRs
- LastCreated: Date of most recent Renovate PR creation (yyyy-MM-dd format)
- LastCompleted: Date of most recent Renovate PR completion (yyyy-MM-dd format)
- LatestOpenBranch: Branch name of most recent open Renovate PR
- LatestCompletedBranch: Branch name of most recent completed Renovate PR
- LastCompletedPRTitle: Title of most recent completed Renovate PR
.NOTES
Author: Cloud Engineering Team
Created: 2025
Requires: PowerShell 5.1 or later
Dependencies: Internet connectivity to Azure DevOps
The script uses Azure DevOps REST API version 6.0 to retrieve repository and pull request information.
Ensure your Personal Access Token has the necessary permissions before running the script.
Renovate is a dependency update tool that creates automated pull requests. This script specifically
looks for PRs with branch names matching the pattern "refs/heads/renovate/*"
The script handles various repository states:
- Active: Normal repositories that can be analyzed
- Disabled: Repositories marked as disabled in Azure DevOps
- Locked: Repositories that are locked (read-only)
- Error: Repositories that couldn't be accessed due to API errors
Date parsing is culture-invariant and includes fallback mechanisms to handle various date formats
from the Azure DevOps API.
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull-requests/get-pull-requests
https://renovatebot.com/
#>
param(
[Parameter(Mandatory=$false)]
[Parameter(Mandatory=$false, HelpMessage="Azure DevOps organization name")]
[string]$Organization = "effectory",
[Parameter(Mandatory=$false)]
[Parameter(Mandatory=$false, HelpMessage="Azure DevOps project name")]
[string]$Project = "Survey Software",
[Parameter(Mandatory=$true)]
[Parameter(Mandatory=$true, HelpMessage="Azure DevOps Personal Access Token with Code (Read) permissions")]
[string]$PAT,
[Parameter(Mandatory=$false)]
[Parameter(Mandatory=$false, HelpMessage="Output file path for the text report")]
[string]$OutputFile = "RenovatePRs_Stats_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"
)
<#
.SYNOPSIS
Outputs a message to both the console and the output file.
.DESCRIPTION
This helper function writes messages to the console with optional color formatting
and simultaneously appends the same message to the output file, unless ConsoleOnly is specified.
.PARAMETER Message
The message to output.
.PARAMETER ForegroundColor
The console text color. Defaults to "White".
.PARAMETER ConsoleOnly
If specified, the message will only be written to the console and not the output file.
Useful for progress messages that shouldn't clutter the report.
#>
function Write-Output-Both {
param (
[string]$Message,
@@ -29,28 +138,34 @@ function Write-Output-Both {
}
# Initialize the output file with a header
Set-Content -Path $OutputFile -Value "Renovate Pull Requests Statistics - $(Get-Date)`n"
# Prepare authentication for Azure DevOps REST API calls
# Personal Access Token must be base64 encoded with a colon prefix
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$PAT"))
$headers = @{
Authorization = "Basic $base64AuthInfo"
}
# Retrieve all repositories from the Azure DevOps project
$reposUrl = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories?api-version=6.0"
$repositories = Invoke-RestMethod -Uri $reposUrl -Method Get -Headers $headers
$repoStats = @()
$reposWithoutRenovate = @()
$disabledRepos = @()
# Initialize arrays to categorize repositories and store statistics
$repoStats = @() # Active repositories with detailed statistics
$reposWithoutRenovate = @() # Active repositories with no Renovate PRs
$disabledRepos = @() # Disabled, locked, or error repositories
# Process each repository in the project
foreach ($repo in $repositories.value) {
$repoName = $repo.name
$repoId = $repo.id
Write-Output-Both "Analyzing repository: $repoName" -ForegroundColor Gray -ConsoleOnly
# Check repository status (disabled, locked, or active)
$isDisabled = $repo.isDisabled -eq $true
$repoDetailsUrl = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories/$repoId`?api-version=6.0"
try {
@@ -62,10 +177,12 @@ foreach ($repo in $repositories.value) {
$isLocked = $false
}
# Skip analysis for disabled or locked repositories
if ($isDisabled -or $isLocked) {
$status = if ($isDisabled) { "DISABLED" } elseif ($isLocked) { "LOCKED" } else { "UNKNOWN" }
Write-Output-Both " Repository status: $status - Skipping analysis" -ForegroundColor Yellow -ConsoleOnly
# Create entry for disabled/locked repository with N/A values
$disabledRepo = [PSCustomObject]@{
Repository = "$repoName ($status)"
TotalRenovatePRs = "N/A"
@@ -83,23 +200,31 @@ foreach ($repo in $repositories.value) {
continue
}
# Retrieve all pull requests for the current repository
$prsUrl = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories/$repoId/pullrequests`?api-version=6.0&searchCriteria.status=all"
try {
$pullRequests = Invoke-RestMethod -Uri $prsUrl -Method Get -Headers $headers
# Filter for Renovate PRs based on branch naming pattern
$renovatePRs = $pullRequests.value | Where-Object { $_.sourceRefName -like "refs/heads/renovate/*" }
if ($renovatePRs.Count -gt 0) {
# Categorize Renovate PRs by status
$openPRs = $renovatePRs | Where-Object { $_.status -eq "active" }
$completedPRs = $renovatePRs | Where-Object { $_.status -eq "completed" }
$abandonedPRs = $renovatePRs | Where-Object { $_.status -eq "abandoned" }
# Count PRs in each category
$openCount = $openPRs.Count
$completedCount = $completedPRs.Count
$abandonedCount = $abandonedPRs.Count
# Find the most recent PRs in each category for detailed reporting
$latestOpen = $openPRs | Sort-Object creationDate -Descending | Select-Object -First 1
$latestCompleted = $completedPRs | Sort-Object closedDate -Descending | Select-Object -First 1
$latestCreated = $renovatePRs | Sort-Object creationDate -Descending | Select-Object -First 1
# Extract key information from the latest PRs
$lastCreatedDate = if ($latestCreated) { $latestCreated.creationDate } else { "N/A" }
$lastCompletedDate = if ($latestCompleted) { $latestCompleted.closedDate } else { "N/A" }
$lastCompletedPRTitle = if ($latestCompleted) { $latestCompleted.title } else { "N/A" }
@@ -118,7 +243,20 @@ foreach ($repo in $repositories.value) {
LatestCompletedBranch = $latestCompletedBranch
LastCompletedPRTitle = $lastCompletedPRTitle
RepoStatus = "ACTIVE"
SortDate = if ($lastCreatedDate -eq "N/A") { [DateTime]::MinValue } else { [DateTime]::Parse($lastCreatedDate) }
SortDate = if ($lastCreatedDate -eq "N/A") {
[DateTime]::MinValue
} else {
try {
[DateTime]::Parse($lastCreatedDate, [System.Globalization.CultureInfo]::InvariantCulture)
} catch {
try {
[DateTime]::ParseExact($lastCreatedDate, "MM/dd/yyyy HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture)
} catch {
Write-Output-Both " Warning: Could not parse date '$lastCreatedDate' for repository $repoName" -ForegroundColor Yellow -ConsoleOnly
[DateTime]::MinValue
}
}
}
}
$repoStats += $repoStat
@@ -160,41 +298,71 @@ foreach ($repo in $repositories.value) {
}
}
# Generate and display the main statistics report
Write-Output-Both "`n===== RENOVATE PULL REQUEST STATISTICS BY REPOSITORY (SORTED BY LAST CREATED DATE) =====" -ForegroundColor Green
if ($repoStats.Count -gt 0) {
# Sort repositories by last created date (most recent first)
$sortedStats = $repoStats | Sort-Object -Property SortDate -Descending
# Format the data for display with culture-invariant date parsing
$displayStats = $sortedStats | Select-Object Repository, TotalRenovatePRs, OpenPRs, CompletedPRs, AbandonedPRs,
@{Name="LastCreated"; Expression={
if ($_.LastCreatedDate -eq "N/A" -or $_.LastCreatedDate -eq "Error") { $_.LastCreatedDate }
else { [DateTime]::Parse($_.LastCreatedDate).ToString("yyyy-MM-dd") }
if ($_.LastCreatedDate -eq "N/A" -or $_.LastCreatedDate -eq "Error") {
$_.LastCreatedDate
} else {
try {
[DateTime]::Parse($_.LastCreatedDate, [System.Globalization.CultureInfo]::InvariantCulture).ToString("yyyy-MM-dd")
} catch {
try {
[DateTime]::ParseExact($_.LastCreatedDate, "MM/dd/yyyy HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture).ToString("yyyy-MM-dd")
} catch {
$_.LastCreatedDate
}
}
}
}},
@{Name="LastCompleted"; Expression={
if ($_.LastCompletedDate -eq "N/A" -or $_.LastCompletedDate -eq "Error") { $_.LastCompletedDate }
else { [DateTime]::Parse($_.LastCompletedDate).ToString("yyyy-MM-dd") }
if ($_.LastCompletedDate -eq "N/A" -or $_.LastCompletedDate -eq "Error") {
$_.LastCompletedDate
} else {
try {
[DateTime]::Parse($_.LastCompletedDate, [System.Globalization.CultureInfo]::InvariantCulture).ToString("yyyy-MM-dd")
} catch {
try {
[DateTime]::ParseExact($_.LastCompletedDate, "MM/dd/yyyy HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture).ToString("yyyy-MM-dd")
} catch {
$_.LastCompletedDate
}
}
}
}},
LatestOpenBranch, LatestCompletedBranch, LastCompletedPRTitle
# Export detailed data to CSV for further analysis
$displayStats | Export-Csv -Path "$($OutputFile).csv" -NoTypeInformation
# Add a note to the original output file
Add-Content -Path $OutputFile -Value "Full data available in: $($OutputFile).csv"
# Add formatted table to the text report and display on console
$statsTable = $displayStats | Format-Table -Property Repository, TotalRenovatePRs, OpenPRs, CompletedPRs, AbandonedPRs, LastCreated, LastCompleted, LatestCompletedBranch, LastCompletedPRTitle | Out-String
Add-Content -Path $OutputFile -Value $statsTable.Trim() # Trim to remove extra whitespace
Add-Content -Path $OutputFile -Value $statsTable.Trim()
$displayStats | Format-Table -AutoSize
# Calculate summary statistics
$totalRepos = $repositories.value.Count
$reposWithRenovate = ($repoStats | Where-Object { $_.TotalRenovatePRs -gt 0 }).Count
$reposDisabledOrLocked = $disabledRepos.Count
$activeRepos = $totalRepos - $reposDisabledOrLocked
# Calculate percentage of active repositories with Renovate PRs
$percentWithRenovate = if ($activeRepos -gt 0) { [math]::Round(($reposWithRenovate / $activeRepos) * 100, 2) } else { 0 }
# Calculate totals across all active repositories
$totalPRs = ($repoStats | Measure-Object -Property TotalRenovatePRs -Sum).Sum
$totalOpenPRs = ($repoStats | Measure-Object -Property OpenPRs -Sum).Sum
$totalCompletedPRs = ($repoStats | Measure-Object -Property CompletedPRs -Sum).Sum
$totalAbandonedPRs = ($repoStats | Measure-Object -Property AbandonedPRs -Sum).Sum
# Display comprehensive summary statistics
Write-Output-Both "`n===== SUMMARY STATISTICS =====" -ForegroundColor Cyan
Write-Output-Both "Total repositories: $totalRepos"
Write-Output-Both "Disabled, locked, or error repositories: $reposDisabledOrLocked"
@@ -206,6 +374,7 @@ if ($repoStats.Count -gt 0) {
Write-Output-Both "Total completed renovate PRs: $totalCompletedPRs"
Write-Output-Both "Total abandoned renovate PRs: $totalAbandonedPRs"
# Display list of repositories that couldn't be analyzed
if ($disabledRepos.Count -gt 0) {
Write-Output-Both "`n===== DISABLED/LOCKED/ERROR REPOSITORIES (NOT INCLUDED IN MAIN REPORT) =====" -ForegroundColor Yellow
$disabledList = $disabledRepos | ForEach-Object { $_.Repository }
@@ -215,4 +384,5 @@ if ($repoStats.Count -gt 0) {
Write-Output-Both "No active repositories found." -ForegroundColor Yellow
}
# Display completion message
Write-Output-Both "`nReport saved to: $OutputFile" -ForegroundColor Cyan

View File

@@ -1,101 +1,329 @@
<#
.SYNOPSIS
Comprehensive Entra ID (Azure AD) group membership analysis with recursive member enumeration and parent group discovery.
.DESCRIPTION
This script provides detailed analysis of Entra ID group memberships by recursively enumerating all members
of a specified group and discovering all parent groups the target group belongs to. It handles nested group
structures, prevents infinite loops through circular reference detection, and exports comprehensive reports.
Key Features:
• Recursive member enumeration with circular reference protection
• Parent group discovery and membership chain analysis
• Support for both group names and Group IDs as input
• Detailed member information including user principals and group types
• Dual CSV export: group members and parent group memberships
• Maximum recursion depth protection (50 levels)
• Comprehensive error handling and logging
.PARAMETER GroupId
The Group ID (GUID) or display name of the Entra ID group to analyze.
Supports both formats:
- Group ID: "12345678-1234-1234-1234-123456789012"
- Group Name: "Developer Team" or "# Developer ADM"
.EXAMPLE
.\GroupMemberships.ps1 -GroupId "12345678-1234-1234-1234-123456789012"
Analyzes the group with the specified GUID, recursively enumerating all members and parent groups.
.EXAMPLE
.\GroupMemberships.ps1 -GroupId "# Developer ADM"
Analyzes the group with display name "# Developer ADM", automatically resolving the name to Group ID.
.EXAMPLE
.\GroupMemberships.ps1 -GroupId "Domain Admins"
Analyzes the "Domain Admins" group, useful for security auditing of privileged groups.
.OUTPUTS
Two CSV files are generated:
1. "[timestamp] ([GroupName]) group members.csv" - Complete recursive member listing
2. "[timestamp] ([GroupName]) group memberships - parent groups.csv" - Parent group hierarchy
CSV Columns for Members:
- ParentGroupId: ID of the group containing this member
- ParentGroupName: Display name of the parent group
- ParentGroupType: Group type classification
- MemberId: Unique identifier of the member
- MemberType: Type of member (user, group, etc.)
- MemberName: Display name of the member
- MemberUPN: User Principal Name (for users)
- MemberEmail: Email address
- Level: Nesting level in the group hierarchy
- Path: Complete membership path showing nested relationships
CSV Columns for Parent Groups:
- ChildGroupId: ID of the child group
- ParentGroupId: ID of the parent group
- ParentGroupName: Display name of the parent group
- ParentGroupType: Group type classification
- ParentGroupEmail: Email address of the parent group
- MembershipLevel: Level in the parent hierarchy
.NOTES
File Name : GroupMemberships.ps1
Author : Cloud Engineering Team
Prerequisite : Microsoft Graph PowerShell SDK
Created : 2024
Updated : 2025-10-30
Version : 2.0
Required Permissions:
• Group.Read.All - Read group properties and memberships
• GroupMember.Read.All - Read group member details
• User.Read.All - Read user properties (for member details)
Security Considerations:
• Script requires privileged Graph API permissions
• Handles sensitive group membership data
• Implements circular reference protection
• Maximum recursion depth prevents infinite loops
• Comprehensive audit trail in CSV exports
Performance Notes:
• Large groups may take considerable time to process
• Recursive enumeration can be resource-intensive
• Implements caching to prevent duplicate API calls
• Progress indicators help track long-running operations
.LINK
https://docs.microsoft.com/en-us/graph/api/group-list-members
https://docs.microsoft.com/en-us/graph/api/group-list-memberof
.FUNCTIONALITY
• Entra ID group membership analysis
• Recursive member enumeration
• Parent group discovery
• Circular reference detection
• Comprehensive reporting
• Security auditing support
#>
param(
[Parameter(Mandatory=$true)]
[Parameter(Mandatory=$true, HelpMessage="Enter the Group ID (GUID) or display name of the Entra ID group to analyze")]
[ValidateNotNullOrEmpty()]
[string]$GroupId
)
# GroupMemberships.ps1 -GroupId "# Developer ADM"
# GroupMemberships.ps1 -GroupId "Domain Admins"
# GroupMemberships.ps1 -GroupId "# Developer"
# GroupMemberships.ps1 -GroupId "# Interne Automatisering Team-Assistent"
# GroupMemberships.ps1 -GroupId "# Interne Automatisering"
# Example usage patterns:
# .\GroupMemberships.ps1 -GroupId "# Developer ADM"
# .\GroupMemberships.ps1 -GroupId "Domain Admins"
# .\GroupMemberships.ps1 -GroupId "# Developer"
# .\GroupMemberships.ps1 -GroupId "# Interne Automatisering Team-Assistent"
# .\GroupMemberships.ps1 -GroupId "# Interne Automatisering"
#Requires -Modules Microsoft.Graph.Groups, Microsoft.Graph.Users
#Requires -Version 5.1
# Initialize script execution
Write-Host "🔍 Entra ID Group Membership Analyzer" -ForegroundColor Cyan
Write-Host "======================================" -ForegroundColor Cyan
Write-Host "📅 Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Green
Write-Host "🎯 Target Group: $GroupId" -ForegroundColor Green
Write-Host ""
# Generate timestamped output file paths
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
[string] $OutputPath = ".\$date ($GroupId) group members.csv"
[string] $membershipOutputPath = ".\$date ($GroupId) group memberships - parent groups .csv"
# Connect to Microsoft Graph if not already connected
Write-Host "Connecting to Microsoft Graph..."
Connect-MgGraph -Scopes "Group.Read.All", "GroupMember.Read.All" -NoWelcome
# Initialize error tracking
$Global:ErrorCount = 0
$Global:WarningCount = 0
# If GroupId is actually a group name, resolve it to the actual GroupId
try {
# Connect to Microsoft Graph with required permissions
Write-Host "🔐 Establishing Microsoft Graph connection..." -ForegroundColor Yellow
$requiredScopes = @("Group.Read.All", "GroupMember.Read.All", "User.Read.All")
# Check if already connected with required scopes
$currentContext = Get-MgContext
if ($currentContext -and $currentContext.Scopes) {
$missingScopes = $requiredScopes | Where-Object { $_ -notin $currentContext.Scopes }
if ($missingScopes.Count -gt 0) {
Write-Warning "Missing required scopes: $($missingScopes -join ', '). Reconnecting..."
$Global:WarningCount++
Disconnect-MgGraph -ErrorAction SilentlyContinue
$currentContext = $null
}
}
if (-not $currentContext) {
Connect-MgGraph -Scopes $requiredScopes -NoWelcome -ErrorAction Stop
Write-Host "✅ Successfully connected to Microsoft Graph" -ForegroundColor Green
} else {
Write-Host "✅ Using existing Microsoft Graph connection" -ForegroundColor Green
}
# Display connection context
$context = Get-MgContext
Write-Host "🏢 Tenant: $($context.TenantId)" -ForegroundColor Gray
Write-Host "👤 Account: $($context.Account)" -ForegroundColor Gray
Write-Host "🔑 Scopes: $($context.Scopes -join ', ')" -ForegroundColor Gray
Write-Host ""
}
catch {
Write-Error "❌ Failed to connect to Microsoft Graph: $($_.Exception.Message)"
$Global:ErrorCount++
exit 1
}
# Resolve group identifier (support both Group ID and display name)
if ($GroupId -notmatch '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') {
Write-Host "Resolving group name '$GroupId' to GroupId..."
Write-Host "🔍 Resolving group name '$GroupId' to Group ID..." -ForegroundColor Yellow
try {
# Use filter to find groups by display name
$group = Get-MgGroup -Filter "displayName eq '$GroupId'" -ErrorAction Stop
if ($group) {
if ($group.Count -gt 1) {
Write-Warning "Multiple groups found with name '$GroupId'. Using the first one."
Write-Warning "⚠️ Multiple groups found with name '$GroupId'. Using the first match."
$Global:WarningCount++
Write-Host " Found groups:" -ForegroundColor Yellow
$group | ForEach-Object { Write-Host " - $($_.DisplayName) ($($_.Id))" -ForegroundColor Yellow }
$GroupId = $group[0].Id
} else {
$GroupId = $group.Id
}
Write-Host "Resolved to GroupId: $GroupId"
Write-Host "Resolved to Group ID: $GroupId" -ForegroundColor Green
} else {
Write-Error "Group with name '$GroupId' not found."
Write-Error "Group with name '$GroupId' not found in tenant."
$Global:ErrorCount++
exit 1
}
} catch {
Write-Error "Error resolving group name: $($_.Exception.Message)"
Write-Error "Error resolving group name: $($_.Exception.Message)"
$Global:ErrorCount++
exit 1
}
} else {
Write-Host "🆔 Using provided Group ID: $GroupId" -ForegroundColor Green
}
# Function to get groups that this group is a member of (reverse membership)
# Validate the resolved group exists and get basic information
try {
Write-Host "🔍 Validating target group..." -ForegroundColor Yellow
$targetGroup = Get-MgGroup -GroupId $GroupId -Select "Id,DisplayName,Description,GroupTypes,Mail,SecurityEnabled,MailEnabled" -ErrorAction Stop
Write-Host "✅ Target group validated:" -ForegroundColor Green
Write-Host " 📝 Name: $($targetGroup.DisplayName)" -ForegroundColor White
Write-Host " 🆔 ID: $($targetGroup.Id)" -ForegroundColor White
Write-Host " 📧 Email: $($targetGroup.Mail ?? 'N/A')" -ForegroundColor White
Write-Host " 🏷️ Type: $($targetGroup.GroupTypes -join ', ' ?? 'Security Group')" -ForegroundColor White
Write-Host " 🔒 Security Enabled: $($targetGroup.SecurityEnabled)" -ForegroundColor White
Write-Host " 📨 Mail Enabled: $($targetGroup.MailEnabled)" -ForegroundColor White
if ($targetGroup.Description) {
Write-Host " 📄 Description: $($targetGroup.Description)" -ForegroundColor White
}
Write-Host ""
}
catch {
Write-Error "❌ Failed to validate group '$GroupId': $($_.Exception.Message)"
$Global:ErrorCount++
exit 1
}
# Function to get groups that this group is a member of (reverse membership discovery)
function Get-GroupMembershipRecursive {
<#
.SYNOPSIS
Recursively discovers all parent groups that the specified group belongs to.
.DESCRIPTION
This function performs reverse membership analysis by finding all groups that contain
the specified group as a member. It handles nested group structures and prevents
infinite loops through circular reference detection.
.PARAMETER GroupId
The Group ID to analyze for parent group memberships.
.PARAMETER Level
Current recursion level (used internally for depth tracking).
.PARAMETER ProcessedMemberships
Hashtable tracking processed groups to prevent circular references.
.RETURNS
Array of custom objects representing parent group relationships.
#>
param(
[Parameter(Mandatory=$true)]
[string]$GroupId,
[Parameter(Mandatory=$false)]
[int]$Level = 0,
[Parameter(Mandatory=$false)]
[hashtable]$ProcessedMemberships = @{}
)
# Check for circular reference in membership chain
# Circular reference protection
if ($ProcessedMemberships.ContainsKey($GroupId)) {
Write-Warning "Circular membership reference detected for group: $GroupId"
Write-Warning "⚠️ Circular membership reference detected for group: $GroupId (Level: $Level)"
$Global:WarningCount++
return @()
}
# Check for maximum depth
# Maximum recursion depth protection
if ($Level -gt 50) {
Write-Warning "Maximum membership recursion depth reached for group: $GroupId"
Write-Warning "⚠️ Maximum membership recursion depth (50) reached for group: $GroupId"
$Global:WarningCount++
return @()
}
# Mark group as being processed for membership
# Mark group as being processed for membership discovery
$ProcessedMemberships[$GroupId] = $true
$membershipResults = @()
try {
Write-Verbose "Discovering parent groups for GroupId: $GroupId (Level: $Level)"
# Get groups that this group is a member of
$memberOfGroups = Get-MgGroupMemberOf -GroupId $GroupId -All
$memberOfGroups = Get-MgGroupMemberOf -GroupId $GroupId -All -ErrorAction Stop
foreach ($parentGroup in $memberOfGroups) {
# Only process actual groups (not other object types)
if ($parentGroup.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.group') {
$parentGroupDetails = Get-MgGroup -GroupId $parentGroup.Id -Select "DisplayName,Mail,GroupTypes"
try {
# Get detailed information about the parent group
$parentGroupDetails = Get-MgGroup -GroupId $parentGroup.Id -Select "DisplayName,Mail,GroupTypes,SecurityEnabled,MailEnabled" -ErrorAction Stop
# Create membership relationship object
$membershipObject = [PSCustomObject]@{
ChildGroupId = $GroupId
ParentGroupId = $parentGroup.Id
ParentGroupName = $parentGroupDetails.DisplayName
ParentGroupType = $parentGroupDetails.GroupTypes -join ","
ParentGroupEmail = $parentGroupDetails.Mail
ParentGroupType = if ($parentGroupDetails.GroupTypes) { $parentGroupDetails.GroupTypes -join "," } else { "Security" }
ParentGroupEmail = $parentGroupDetails.Mail ?? ""
MembershipLevel = $Level
SecurityEnabled = $parentGroupDetails.SecurityEnabled
MailEnabled = $parentGroupDetails.MailEnabled
DiscoveredAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
}
$membershipResults += $membershipObject
Write-Verbose "Found parent group: $($parentGroupDetails.DisplayName) at level $Level"
# Recursively get parent groups of this parent group
# Recursively discover parent groups of this parent group
if ($parentGroup.Id -ne $GroupId) {
$nestedMemberships = Get-GroupMembershipRecursive -GroupId $parentGroup.Id -Level ($Level + 1) -ProcessedMemberships $ProcessedMemberships
$membershipResults += $nestedMemberships
}
}
catch {
Write-Warning "⚠️ Failed to get details for parent group $($parentGroup.Id): $($_.Exception.Message)"
$Global:WarningCount++
}
}
}
}
catch {
Write-Error "Error getting group memberships for $GroupId`: $($_.Exception.Message)"
Write-Error "Error getting group memberships for $GroupId`: $($_.Exception.Message)"
$Global:ErrorCount++
}
finally {
# Remove from processed memberships when done
# Clean up: remove from processed memberships when done
$ProcessedMemberships.Remove($GroupId)
}
@@ -103,123 +331,322 @@ function Get-GroupMembershipRecursive {
}
# Initialize collections
$groupMembers = @()
$processedGroups = @{}
$groupStack = @()
# Initialize script-level collections for member enumeration
$script:groupMembers = @()
$script:processedGroups = @{}
$script:groupStack = @()
$script:memberCount = 0
$script:groupCount = 0
# Function to get group members recursively
# This function will handle circular references and maximum recursion depth
# Function to recursively enumerate all group members
function Get-GroupMembersRecursive {
<#
.SYNOPSIS
Recursively enumerates all members of a group, including nested group members.
.DESCRIPTION
This function performs deep enumeration of group membership by recursively processing
nested groups. It includes comprehensive protection against circular references and
infinite recursion, while providing detailed member information.
.PARAMETER GroupId
The Group ID to enumerate members for.
.PARAMETER Level
Current recursion level (used internally for depth tracking).
.PARAMETER ParentGroupName
Name of the parent group (used for audit trail).
.NOTES
Uses script-level variables to maintain state across recursive calls:
- $script:groupMembers: Collection of all discovered members
- $script:processedGroups: Tracks processed groups for circular reference protection
- $script:groupStack: Current processing stack for immediate loop detection
#>
param(
[Parameter(Mandatory=$true)]
[string]$GroupId,
[Parameter(Mandatory=$false)]
[int]$Level = 0,
[Parameter(Mandatory=$false)]
[string]$ParentGroupName = ""
)
# Check for circular reference
if ($processedGroups.ContainsKey($GroupId)) {
Write-Warning "Circular reference detected for group: $GroupId"
# Circular reference protection
if ($script:processedGroups.ContainsKey($GroupId)) {
Write-Warning "⚠️ Circular reference detected for group: $GroupId (Level: $Level)"
$Global:WarningCount++
return
}
# Check for stack overflow (max depth)
# Maximum recursion depth protection
if ($Level -gt 50) {
Write-Warning "Maximum recursion depth reached for group: $GroupId"
Write-Warning "⚠️ Maximum recursion depth (50) reached for group: $GroupId"
$Global:WarningCount++
return
}
# Mark group as being processed
$processedGroups[$GroupId] = $true
$groupStack += $GroupId
# Mark group as being processed and add to processing stack
$script:processedGroups[$GroupId] = $true
$script:groupStack += $GroupId
$script:groupCount++
try {
# Get the group information
$group = Get-MgGroup -GroupId $GroupId -ErrorAction Stop
# Get the group information with required properties
$group = Get-MgGroup -GroupId $GroupId -Select "Id,DisplayName,GroupTypes,Mail,SecurityEnabled,MailEnabled" -ErrorAction Stop
Write-Verbose "Processing group: $($group.DisplayName) (Level: $Level)"
# Get group members
$members = Get-MgGroupMember -GroupId $GroupId -All
# Get all group members
$members = Get-MgGroupMember -GroupId $GroupId -All -ErrorAction Stop
Write-Host " 📋 Processing $($members.Count) members in group: $($group.DisplayName) (Level: $Level)" -ForegroundColor Gray
foreach ($member in $members) {
# Create custom object for the result
$script:memberCount++
# Create comprehensive member object
$memberObject = [PSCustomObject]@{
ParentGroupId = $GroupId
ParentGroupName = $group.DisplayName
ParentGroupType = $group.GroupTypes -join ","
ParentGroupType = if ($group.GroupTypes) { $group.GroupTypes -join "," } else { "Security" }
ParentGroupEmail = $group.Mail ?? ""
MemberId = $member.Id
MemberType = $member.AdditionalProperties['@odata.type'] -replace '#microsoft.graph.', ''
Level = $Level
Path = ($groupStack -join " -> ") + " -> " + $member.Id
Path = ($script:groupStack -join " -> ") + " -> " + $member.Id
ProcessedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
SecurityEnabled = $group.SecurityEnabled
MailEnabled = $group.MailEnabled
}
# Get member details based on type
# Get detailed member information based on object type
try {
switch ($member.AdditionalProperties['@odata.type']) {
'#microsoft.graph.user' {
$user = Get-MgUser -UserId $member.Id -Select "DisplayName,UserPrincipalName,Mail"
$user = Get-MgUser -UserId $member.Id -Select "DisplayName,UserPrincipalName,Mail,AccountEnabled,UserType" -ErrorAction Stop
$memberObject | Add-Member -NotePropertyName "MemberName" -NotePropertyValue $user.DisplayName
$memberObject | Add-Member -NotePropertyName "MemberUPN" -NotePropertyValue $user.UserPrincipalName
$memberObject | Add-Member -NotePropertyName "MemberEmail" -NotePropertyValue $user.Mail
$memberObject | Add-Member -NotePropertyName "MemberEmail" -NotePropertyValue ($user.Mail ?? "")
$memberObject | Add-Member -NotePropertyName "MemberGroupTypes" -NotePropertyValue ""
$memberObject | Add-Member -NotePropertyName "AccountEnabled" -NotePropertyValue $user.AccountEnabled
$memberObject | Add-Member -NotePropertyName "UserType" -NotePropertyValue ($user.UserType ?? "Member")
}
'#microsoft.graph.group' {
$memberGroup = Get-MgGroup -GroupId $member.Id -Select "DisplayName,Mail,GroupTypes"
$memberGroup = Get-MgGroup -GroupId $member.Id -Select "DisplayName,Mail,GroupTypes,SecurityEnabled,MailEnabled" -ErrorAction Stop
$memberObject | Add-Member -NotePropertyName "MemberName" -NotePropertyValue $memberGroup.DisplayName
$memberObject | Add-Member -NotePropertyName "MemberUPN" -NotePropertyValue ""
$memberObject | Add-Member -NotePropertyName "MemberEmail" -NotePropertyValue $memberGroup.Mail
$memberObject | Add-Member -NotePropertyName "MemberGroupTypes" -NotePropertyValue ($memberGroup.GroupTypes -join ",")
$memberObject | Add-Member -NotePropertyName "MemberEmail" -NotePropertyValue ($memberGroup.Mail ?? "")
$memberObject | Add-Member -NotePropertyName "MemberGroupTypes" -NotePropertyValue (if ($memberGroup.GroupTypes) { $memberGroup.GroupTypes -join "," } else { "Security" })
$memberObject | Add-Member -NotePropertyName "AccountEnabled" -NotePropertyValue $memberGroup.SecurityEnabled
$memberObject | Add-Member -NotePropertyName "UserType" -NotePropertyValue "Group"
}
default {
$memberObject | Add-Member -NotePropertyName "MemberName" -NotePropertyValue "Unknown"
'#microsoft.graph.servicePrincipal' {
$servicePrincipal = Get-MgServicePrincipal -ServicePrincipalId $member.Id -Select "DisplayName,AppId,ServicePrincipalType" -ErrorAction Stop
$memberObject | Add-Member -NotePropertyName "MemberName" -NotePropertyValue $servicePrincipal.DisplayName
$memberObject | Add-Member -NotePropertyName "MemberUPN" -NotePropertyValue ""
$memberObject | Add-Member -NotePropertyName "MemberEmail" -NotePropertyValue ""
$memberObject | Add-Member -NotePropertyName "MemberGroupTypes" -NotePropertyValue ""
$memberObject | Add-Member -NotePropertyName "AccountEnabled" -NotePropertyValue $true
$memberObject | Add-Member -NotePropertyName "UserType" -NotePropertyValue "ServicePrincipal"
}
default {
$memberObject | Add-Member -NotePropertyName "MemberName" -NotePropertyValue "Unknown Object Type"
$memberObject | Add-Member -NotePropertyName "MemberUPN" -NotePropertyValue ""
$memberObject | Add-Member -NotePropertyName "MemberEmail" -NotePropertyValue ""
$memberObject | Add-Member -NotePropertyName "MemberGroupTypes" -NotePropertyValue ""
$memberObject | Add-Member -NotePropertyName "AccountEnabled" -NotePropertyValue $null
$memberObject | Add-Member -NotePropertyName "UserType" -NotePropertyValue "Unknown"
}
}
}
catch {
Write-Warning "⚠️ Failed to get details for member $($member.Id): $($_.Exception.Message)"
$Global:WarningCount++
$memberObject | Add-Member -NotePropertyName "MemberName" -NotePropertyValue "Error retrieving details"
$memberObject | Add-Member -NotePropertyName "MemberUPN" -NotePropertyValue ""
$memberObject | Add-Member -NotePropertyName "MemberEmail" -NotePropertyValue ""
$memberObject | Add-Member -NotePropertyName "MemberGroupTypes" -NotePropertyValue ""
$memberObject | Add-Member -NotePropertyName "AccountEnabled" -NotePropertyValue $null
$memberObject | Add-Member -NotePropertyName "UserType" -NotePropertyValue "Error"
}
# Add member to results collection
$script:groupMembers += $memberObject
# If member is a group, recurse into it
# If member is a group, recursively process its members
if ($member.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.group') {
# Check if this group is already in the current path to prevent immediate loops
if ($member.Id -notin $groupStack) {
# Check if this group is already in the current processing path to prevent immediate loops
if ($member.Id -notin $script:groupStack) {
Write-Verbose "Recursing into nested group: $($memberObject.MemberName)"
Get-GroupMembersRecursive -GroupId $member.Id -Level ($Level + 1) -ParentGroupName $group.DisplayName
} else {
Write-Warning "Immediate circular reference detected. Skipping group: $($memberGroup.DisplayName)"
Write-Warning "⚠️ Immediate circular reference detected. Skipping group: $($memberObject.MemberName)"
$Global:WarningCount++
}
}
}
}
catch {
Write-Error "Error processing group $GroupId`: $($_.Exception.Message)"
Write-Error "Error processing group $GroupId`: $($_.Exception.Message)"
$Global:ErrorCount++
}
finally {
# Remove from stack and processed groups when done
$groupStack = $groupStack[0..($groupStack.Length-2)]
$processedGroups.Remove($GroupId)
# Clean up: remove from processing stack and processed groups when done
if ($script:groupStack.Length -gt 0) {
$script:groupStack = $script:groupStack[0..($script:groupStack.Length-2)]
}
$script:processedGroups.Remove($GroupId)
}
}
# Start the recursive process
Write-Host "Starting recursive group membership scan for group: $GroupId"
Get-GroupMembersRecursive -GroupId $GroupId
# Execute recursive member enumeration
Write-Host "🔍 Starting recursive group membership analysis..." -ForegroundColor Cyan
Write-Host "📊 Progress will be shown for each processed group..." -ForegroundColor Gray
Write-Host ""
# Export results to CSV
if ($groupMembers.Count -gt 0) {
$groupMembers | Export-Csv -Path $OutputPath -NoTypeInformation
Write-Host "Results exported to: $OutputPath"
Write-Host "Total members found: $($groupMembers.Count)"
} else {
Write-Host "No members found in the specified group."
$startTime = Get-Date
try {
Get-GroupMembersRecursive -GroupId $GroupId
$processingTime = (Get-Date) - $startTime
Write-Host ""
Write-Host "✅ Member enumeration completed!" -ForegroundColor Green
Write-Host "⏱️ Processing time: $($processingTime.TotalSeconds.ToString('F2')) seconds" -ForegroundColor Gray
Write-Host "📈 Performance metrics:" -ForegroundColor Gray
Write-Host " • Groups processed: $script:groupCount" -ForegroundColor Gray
Write-Host " • Members discovered: $script:memberCount" -ForegroundColor Gray
Write-Host " • Processing rate: $([math]::Round($script:memberCount / $processingTime.TotalSeconds, 2)) members/second" -ForegroundColor Gray
}
catch {
Write-Error "❌ Fatal error during member enumeration: $($_.Exception.Message)"
$Global:ErrorCount++
}
# Get group memberships (groups this group belongs to)
Write-Host "Getting group memberships for group: $GroupId"
$groupMemberships = Get-GroupMembershipRecursive -GroupId $GroupId
# Export member results to CSV with error handling
Write-Host ""
Write-Host "📄 Exporting member analysis results..." -ForegroundColor Yellow
if ($script:groupMembers.Count -gt 0) {
try {
$script:groupMembers | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 -ErrorAction Stop
Write-Host "✅ Member results exported successfully!" -ForegroundColor Green
Write-Host " 📁 File: $OutputPath" -ForegroundColor White
Write-Host " 📊 Total members: $($script:groupMembers.Count)" -ForegroundColor White
# Provide member type breakdown
$memberTypes = $script:groupMembers | Group-Object MemberType
Write-Host " 📋 Member types:" -ForegroundColor White
foreach ($type in $memberTypes) {
Write-Host "$($type.Name): $($type.Count)" -ForegroundColor Gray
}
# Show nesting level statistics
$levels = $script:groupMembers | Group-Object Level
Write-Host " 🏗️ Nesting levels:" -ForegroundColor White
foreach ($level in ($levels | Sort-Object Name)) {
Write-Host " • Level $($level.Name): $($level.Count) members" -ForegroundColor Gray
}
}
catch {
Write-Error "❌ Failed to export member results: $($_.Exception.Message)"
$Global:ErrorCount++
}
} else {
Write-Host " No members found in the specified group or its nested groups." -ForegroundColor Yellow
}
# Execute parent group membership discovery
Write-Host ""
Write-Host "🔍 Discovering parent group memberships..." -ForegroundColor Cyan
$membershipStartTime = Get-Date
try {
$groupMemberships = Get-GroupMembershipRecursive -GroupId $GroupId
$membershipProcessingTime = (Get-Date) - $membershipStartTime
Write-Host "✅ Parent group discovery completed!" -ForegroundColor Green
Write-Host "⏱️ Processing time: $($membershipProcessingTime.TotalSeconds.ToString('F2')) seconds" -ForegroundColor Gray
}
catch {
Write-Error "❌ Error during parent group discovery: $($_.Exception.Message)"
$Global:ErrorCount++
$groupMemberships = @()
}
# Export parent group membership results with error handling
Write-Host ""
Write-Host "📄 Exporting parent group membership results..." -ForegroundColor Yellow
# Export group memberships to separate CSV if any found
if ($groupMemberships.Count -gt 0) {
$groupMemberships | Export-Csv -Path $membershipOutputPath -NoTypeInformation
Write-Host "Group memberships exported to: $membershipOutputPath"
Write-Host "Total parent groups found: $($groupMemberships.Count)"
try {
$groupMemberships | Export-Csv -Path $membershipOutputPath -NoTypeInformation -Encoding UTF8 -ErrorAction Stop
Write-Host "✅ Parent group memberships exported successfully!" -ForegroundColor Green
Write-Host " 📁 File: $membershipOutputPath" -ForegroundColor White
Write-Host " 📊 Total parent groups: $($groupMemberships.Count)" -ForegroundColor White
# Show membership level breakdown
$membershipLevels = $groupMemberships | Group-Object MembershipLevel
Write-Host " 🏗️ Membership levels:" -ForegroundColor White
foreach ($level in ($membershipLevels | Sort-Object Name)) {
Write-Host " • Level $($level.Name): $($level.Count) parent groups" -ForegroundColor Gray
}
# Show parent group types
$parentGroupTypes = $groupMemberships | Group-Object ParentGroupType
Write-Host " 📋 Parent group types:" -ForegroundColor White
foreach ($type in $parentGroupTypes) {
Write-Host "$($type.Name): $($type.Count) groups" -ForegroundColor Gray
}
}
catch {
Write-Error "❌ Failed to export parent group memberships: $($_.Exception.Message)"
$Global:ErrorCount++
}
} else {
Write-Host "No group memberships found for the specified group."
Write-Host " Target group is not a member of any other groups." -ForegroundColor Yellow
}
# Generate comprehensive execution summary
Write-Host ""
Write-Host "📊 EXECUTION SUMMARY" -ForegroundColor Cyan
Write-Host "=====================" -ForegroundColor Cyan
$totalTime = (Get-Date) - $startTime
Write-Host "🎯 Target Group: $($targetGroup.DisplayName)" -ForegroundColor White
Write-Host "⏱️ Total Execution Time: $($totalTime.TotalMinutes.ToString('F2')) minutes" -ForegroundColor White
Write-Host "📈 Performance Metrics:" -ForegroundColor White
Write-Host " • Groups Processed: $script:groupCount" -ForegroundColor Gray
Write-Host " • Total Members Found: $script:memberCount" -ForegroundColor Gray
Write-Host " • Parent Groups Found: $($groupMemberships.Count)" -ForegroundColor Gray
Write-Host " • Processing Rate: $([math]::Round($script:memberCount / $totalTime.TotalSeconds, 2)) members/second" -ForegroundColor Gray
# Display error and warning summary
if ($Global:ErrorCount -gt 0 -or $Global:WarningCount -gt 0) {
Write-Host ""
Write-Host "⚠️ ISSUES ENCOUNTERED:" -ForegroundColor Yellow
if ($Global:ErrorCount -gt 0) {
Write-Host " ❌ Errors: $Global:ErrorCount" -ForegroundColor Red
}
if ($Global:WarningCount -gt 0) {
Write-Host " ⚠️ Warnings: $Global:WarningCount" -ForegroundColor Yellow
}
Write-Host " 💡 Review output above for details" -ForegroundColor Cyan
} else {
Write-Host "✅ No errors or warnings encountered!" -ForegroundColor Green
}
# Display output file locations
Write-Host ""
Write-Host "📁 OUTPUT FILES:" -ForegroundColor Cyan
if ($script:groupMembers.Count -gt 0) {
Write-Host " 📄 Member Analysis: $OutputPath" -ForegroundColor White
}
if ($groupMemberships.Count -gt 0) {
Write-Host " 📄 Parent Groups: $membershipOutputPath" -ForegroundColor White
}
Write-Host ""
Write-Host "✅ Group membership analysis completed successfully!" -ForegroundColor Green
Write-Host "📅 Finished: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Green

View File

@@ -1,15 +1,98 @@
Import-Module Microsoft.Graph
<#
.SYNOPSIS
Retrieves user login information from an Entra ID (Azure AD) group with recursive nested group support.
# .\UserLastLoginList.ps1 -GroupName "# Developer ADM"
.DESCRIPTION
This script connects to Microsoft Graph and retrieves all users from a specified Entra ID group,
including users in nested groups. For each user, it displays their display name, user principal name,
and last sign-in date/time information.
Features:
• Recursive group membership enumeration (handles nested groups)
• Circular reference protection to prevent infinite loops
• Last sign-in activity tracking for security and compliance
• Comprehensive error handling and diagnostic output
• Automatic Microsoft Graph authentication with required scopes
.PARAMETER GroupName
The display name of the Entra ID group to analyze. This parameter is mandatory.
The script will search for an exact match of the group display name.
.EXAMPLE
.\UserLastLoginList.ps1 -GroupName "# Developer ADM"
Retrieves all users from the "# Developer ADM" group, including any nested group memberships.
.EXAMPLE
.\UserLastLoginList.ps1 -GroupName "IT Security Team" -Debug
Retrieves users with detailed debug output showing group processing and user enumeration steps.
.INPUTS
None. You cannot pipe objects to this script.
.OUTPUTS
System.Management.Automation.PSCustomObject
Returns a formatted table with the following properties for each user:
- UserPrincipalName: The user's UPN (email-like identifier)
- DisplayName: The user's display name
- LastSignInDateTime: The user's last sign-in date/time, or "No sign-in data available" if unavailable
.NOTES
Requires PowerShell 5.1 or later
Requires Microsoft.Graph PowerShell module
Required Microsoft Graph Permissions:
- User.Read.All: To read user profiles and sign-in activity
- Group.Read.All: To read group memberships and nested groups
The script will automatically prompt for authentication if not already connected to Microsoft Graph.
Sign-in activity data may not be available for all users depending on license and retention policies.
.LINK
https://docs.microsoft.com/en-us/graph/api/user-get
https://docs.microsoft.com/en-us/graph/api/group-list-members
.COMPONENT
Microsoft Graph PowerShell SDK
.ROLE
Identity and Access Management, Security Reporting
.FUNCTIONALITY
Entra ID group analysis, user activity reporting, nested group enumeration
#>
param(
[Parameter(Mandatory = $true)]
[Parameter(
Mandatory = $true,
HelpMessage = "Enter the display name of the Entra ID group to analyze"
)]
[ValidateNotNullOrEmpty()]
[string]$GroupName
)
<#
.SYNOPSIS
Retrieves an Entra ID group by its display name.
.DESCRIPTION
This function searches for an Entra ID group using its display name and returns the group's unique identifier.
Uses Microsoft Graph API to perform an exact match search on the displayName property.
.PARAMETER EntraGroupName
The exact display name of the Entra ID group to search for.
.OUTPUTS
System.String
Returns the group's unique identifier (GUID) if found, or $null if not found or error occurs.
.EXAMPLE
$groupId = Get-EntraGroupByName -EntraGroupName "Developers"
Retrieves the group ID for the "Developers" group.
#>
function Get-EntraGroupByName {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$EntraGroupName
)
@@ -33,18 +116,54 @@ function Get-EntraGroupByName {
}
}
# Initialize Microsoft Graph connection
Write-Host "🔐 Initializing Microsoft Graph connection..." -ForegroundColor Cyan
Write-Debug "Connecting to Microsoft Graph..."
# Connect to Microsoft Graph if not already connected
if (-not (Get-MgContext)) {
Write-Host "🔑 Authenticating to Microsoft Graph with required scopes..." -ForegroundColor Yellow
Connect-MgGraph -Scopes "User.Read.All", "Group.Read.All"
Write-Host "✅ Successfully connected to Microsoft Graph" -ForegroundColor Green
} else {
Write-Host "✅ Already connected to Microsoft Graph" -ForegroundColor Green
}
<#
.SYNOPSIS
Recursively retrieves all users from an Entra ID group, including nested groups.
.DESCRIPTION
This function performs a recursive search through an Entra ID group structure to find all users,
including those in nested groups. It includes circular reference protection to prevent infinite
loops when groups contain circular memberships.
.PARAMETER GroupId
The unique identifier (GUID) of the Entra ID group to process.
.PARAMETER ProcessedGroups
Internal hashtable used to track processed groups and prevent circular reference loops.
This parameter is used internally for recursion and should not be specified by callers.
.OUTPUTS
System.Management.Automation.PSCustomObject[]
Returns an array of custom objects with the following properties:
- UserPrincipalName: The user's UPN
- DisplayName: The user's display name
- LastSignInDateTime: The user's last sign-in date/time or status message
.EXAMPLE
$users = Get-EntraGroupUsersRecursive -GroupId "12345678-1234-1234-1234-123456789012"
Recursively retrieves all users from the specified group and its nested groups.
.NOTES
This function is designed to handle complex group hierarchies and prevents infinite recursion
through circular group memberships. It processes both user and group objects within the membership.
#>
function Get-EntraGroupUsersRecursive {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$GroupId,
[hashtable]$ProcessedGroups = @{}
)
@@ -88,20 +207,44 @@ function Get-EntraGroupUsersRecursive {
}
}
# Main execution logic
Write-Host ""
Write-Host "🔍 Searching for Entra ID group: '$GroupName'" -ForegroundColor Cyan
$groupId = Get-EntraGroupByName -EntraGroupName $GroupName
if ($groupId) {
Write-Host "✅ Group found! Group ID: $groupId" -ForegroundColor Green
Write-Host ""
# Get users recursively from the group
Write-Host "Getting users recursively from group..."
Write-Host "👥 Retrieving users recursively from group (including nested groups)..." -ForegroundColor Cyan
$recursiveUsers = Get-EntraGroupUsersRecursive -GroupId $groupId
if ($recursiveUsers) {
Write-Host "Found $($recursiveUsers.Count) users (including nested groups):"
Write-Host ""
Write-Host "📊 Analysis Results:" -ForegroundColor Green
Write-Host "===================" -ForegroundColor Green
Write-Host "Found $($recursiveUsers.Count) users (including nested groups)" -ForegroundColor White
Write-Host ""
# Display results sorted by display name
$recursiveUsers | Sort-Object DisplayName | Format-Table -AutoSize
# Additional statistics
$usersWithSignInData = ($recursiveUsers | Where-Object { $_.LastSignInDateTime -ne "No sign-in data available" }).Count
$usersWithoutSignInData = $recursiveUsers.Count - $usersWithSignInData
Write-Host ""
Write-Host "📈 Sign-in Data Summary:" -ForegroundColor Yellow
Write-Host "========================" -ForegroundColor Yellow
Write-Host "Users with sign-in data: $usersWithSignInData" -ForegroundColor White
Write-Host "Users without sign-in data: $usersWithoutSignInData" -ForegroundColor White
}
else {
Write-Warning "No users found in the group hierarchy or an error occurred."
Write-Warning "No users found in the group hierarchy or an error occurred."
}
}
else {
Write-Warning "Group not found."
Write-Warning "Group '$GroupName' not found. Please verify the group name and try again."
}

View File

@@ -1,28 +1,63 @@
Import-Module AzureAD
# Import required modules for PowerShell Core compatibility
Import-Module Microsoft.Graph.Groups
Import-Module Microsoft.Graph.Users
Import-Module Microsoft.Graph.Authentication
Import-Module SqlServer
#Connect-AzureAD
# Authentication - uncomment as needed
#Connect-MgGraph -Scopes "Group.Read.All", "User.Read.All", "GroupMember.Read.All"
#Connect-AzAccount
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$filename = "c:\tmp\$date User group mappings.csv"
$filename = ".\$date User group mappings.csv"
Function Get-RecursiveAzureAdGroupMemberUsers{
Function Get-RecursiveMgGroupMemberUsers{
[cmdletbinding()]
param(
[parameter(Mandatory=$True,ValueFromPipeline=$true)]
$AzureGroup
$MgGroup
)
Begin{
If(-not(Get-AzureADCurrentSessionInfo)){Connect-AzureAD}
# Check if Microsoft Graph is connected
$context = Get-MgContext
If(-not($context)){
Write-Warning "Microsoft Graph not connected. Please run Connect-MgGraph first."
throw "Microsoft Graph connection required"
}
}
Process {
Write-Verbose -Message "Enumerating $($AzureGroup.DisplayName)"
$Members = Get-AzureADGroupMember -ObjectId $AzureGroup.ObjectId -All $true
$UserMembers = $Members | Where-Object{$_.ObjectType -eq 'User'}
If($Members | Where-Object{$_.ObjectType -eq 'Group'}){
$UserMembers += $Members | Where-Object{$_.ObjectType -eq 'Group'} | ForEach-Object{ Get-RecursiveAzureAdGroupMemberUsers -AzureGroup $_}
Write-Verbose -Message "Enumerating $($MgGroup.DisplayName)"
# Get group members using Microsoft Graph
$Members = Get-MgGroupMember -GroupId $MgGroup.Id -All
# Filter for user members and get full user details
$UserMembers = @()
$UserMemberIds = $Members | Where-Object {$_.AdditionalProperties["@odata.type"] -eq "#microsoft.graph.user"}
foreach ($userMember in $UserMemberIds) {
try {
$userDetails = Get-MgUser -UserId $userMember.Id -ErrorAction Stop
$UserMembers += $userDetails
}
catch {
Write-Warning "Could not retrieve user details for ID: $($userMember.Id)"
}
}
# Process nested groups recursively
$GroupMembers = $Members | Where-Object {$_.AdditionalProperties["@odata.type"] -eq "#microsoft.graph.group"}
If($GroupMembers){
foreach ($groupMember in $GroupMembers) {
try {
$nestedGroup = Get-MgGroup -GroupId $groupMember.Id -ErrorAction Stop
$UserMembers += Get-RecursiveMgGroupMemberUsers -MgGroup $nestedGroup
}
catch {
Write-Warning "Could not process nested group ID: $($groupMember.Id)"
}
}
}
}
end {
@@ -33,7 +68,9 @@ Function Get-RecursiveAzureAdGroupMemberUsers{
# Get SQL records
Write-Host ("Get SQL records") -foreground Yellow
$access_token = (Get-AzAccessToken -ResourceUrl https://database.windows.net).Token
$access_token_secure = (Get-AzAccessToken -ResourceUrl https://database.windows.net).Token
$access_token = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($access_token_secure))
$signInConnectionString = "Data Source=signin-effectory.database.windows.net;Initial Catalog=SignIn;Persist Security Info=False;Encrypt=True;TrustServerCertificate=False;Application Name=CloudEngineering";
$eceConnectionString = "Data Source=c0m7f8nybr.database.windows.net;Initial Catalog='Effectory Extranet';Persist Security Info=False;Encrypt=True;TrustServerCertificate=False;Application Name=CloudEngineering";
@@ -171,7 +208,14 @@ foreach($mapping in $mappings) {
Write-Host ("[$itemDate] [$a/$noMappings] - Mapping '$mappingName'") -foreground Green
#get users in mapping
$usersInMapping = Get-AzureADGroup -ObjectId $mapping.GroupId | Get-RecursiveAzureAdGroupMemberUsers
try {
$group = Get-MgGroup -GroupId $mapping.GroupId -ErrorAction Stop
$usersInMapping = Get-RecursiveMgGroupMemberUsers -MgGroup $group
}
catch {
Write-Warning "Could not retrieve group with ID: $($mapping.GroupId). Error: $($_.Exception.Message)"
continue
}
#get mapping claims
$mappingItemsInMapping = $mappingItems | Where-Object GroupId -eq $mapping.GroupId
@@ -189,7 +233,8 @@ foreach($mapping in $mappings) {
$userMappingItem.GroupId = $mappingItem.GroupId
$userMappingItem.GroupMappingName = $mappingItem.GroupMappingName
$userMappingItem.UserObjectId = $user.ObjectId
# Microsoft Graph user properties (property names are the same)
$userMappingItem.UserObjectId = $user.Id
$userMappingItem.UserDisplayName = $user.DisplayName
$userMappingItem.UserMail = $user.Mail
$userMappingItem.UserUserPrincipalName = $user.UserPrincipalName

250
Powershell/Lists/README.md Normal file
View File

@@ -0,0 +1,250 @@
# PowerShell List Scripts Collection
This directory contains a comprehensive collection of PowerShell scripts for generating inventory and reporting data across various platforms and services. Each script produces timestamped CSV exports with detailed information for analysis, compliance, and governance purposes.
## 📋 Table of Contents
- [Azure Scripts](#-azure-scripts)
- [Azure DevOps Scripts](#-azure-devops-scripts)
- [Entra ID (Azure AD) Scripts](#-entra-id-azure-ad-scripts)
- [Security & Vulnerability Scripts](#-security--vulnerability-scripts)
- [SQL Database Scripts](#-sql-database-scripts)
- [Application-Specific Scripts](#-application-specific-scripts)
- [Prerequisites](#-prerequisites)
- [Usage Guidelines](#-usage-guidelines)
---
## 🔵 Azure Scripts
### Resource Inventory & Management
| Script | Description | Output |
|--------|-------------|---------|
| **Resources.ps1** | Comprehensive Azure resource inventory across all subscriptions | CSV with resource metadata, tags, managed identities |
| **AzureRBAC.ps1** | RBAC assignment analysis with PIM detection across Azure hierarchy | CSV with assignment details, PIM status, scope analysis |
| **ManagementGroups.ps1** | Management group hierarchy and subscription mapping | CSV with organizational structure |
### Storage & Data
| Script | Description | Output |
|--------|-------------|---------|
| **AzureStorageBlobList.ps1** | Blob storage inventory across storage accounts | CSV with blob details, metadata, access tiers |
| **AzureStorageTableListEntities.ps1** | Table storage entity enumeration | CSV with table entities and properties |
### Security & Access
| Script | Description | Output |
|--------|-------------|---------|
| **KeyVaults.ps1** | Key Vault inventory with configuration details | CSV with vault properties, access policies |
| **KeyVaultAccessPolicies.ps1** | Detailed Key Vault access policy analysis | CSV with permission mappings |
| **KeyVaultNonRBACSecrets.ps1** | Non-RBAC managed Key Vault secrets inventory | CSV with legacy access policy secrets |
| **Certificates.ps1** | Certificate inventory across Key Vaults | CSV with certificate details, expiration dates |
| **AzurePIM.ps1** | Privileged Identity Management assignments | CSV with PIM role assignments and status |
### Networking & Applications
| Script | Description | Output |
|--------|-------------|---------|
| **WebApps.ps1** | App Service and Web App inventory | CSV with app configurations, settings |
| **FrontDoorRoutes.ps1** | Azure Front Door routing configuration | CSV with route mappings and rules |
| **ServiceBus.ps1** | Service Bus namespaces and entity inventory | CSV with queues, topics, subscriptions |
### Monitoring & Alerts
| Script | Description | Output |
|--------|-------------|---------|
| **AlertRules.ps1** | Azure Monitor alert rules inventory | CSV with alert configurations |
| **AppInsightsWorkspace.ps1** | Application Insights workspace details | CSV with workspace configurations |
---
## 🔵 Azure DevOps Scripts
| Script | Description | Output |
|--------|-------------|---------|
| **Repositories.ps1** | Repository inventory with last pull request details | CSV with repo metadata, recent PR info |
| **PullRequests.ps1** | Pull request history and statistics | CSV with PR details, reviewers, completion data |
| **Pipelines.ps1** | Build and release pipeline inventory | CSV with pipeline configurations |
| **ServiceConnections.ps1** | Service connection inventory and status | CSV with connection details, permissions |
| **RepositoriesWithTestAccept.ps1** | Repositories with specific testing configurations | CSV with test acceptance criteria |
| **renovate-stats.ps1** | Renovate bot statistics and dependency updates | CSV with update metrics |
---
## 🟢 Entra ID (Azure AD) Scripts
| Script | Description | Output |
|--------|-------------|---------|
| **GroupMemberships.ps1** | Recursive group membership analysis with circular reference detection | CSV with complete membership hierarchy |
| **UserLastLoginList.ps1** | User last login analysis for group members | CSV with login activity and user status |
---
## 🔴 Security & Vulnerability Scripts
### Snyk Integration
| Script | Description | Output |
|--------|-------------|---------|
| **SnykOverview.ps1** | Comprehensive Snyk organization and project inventory | CSV with project metadata, vulnerability counts |
| **SBOM.ps1** | Software Bill of Materials generation with enhanced package metadata | CSV with dependency details, vulnerability data, deprecation status |
---
## 🟡 SQL Database Scripts
| Script | Description | Output |
|--------|-------------|---------|
| **SQLUserCheck.ps1** | Multi-server SQL database user audit with authentication analysis | CSV with user accounts, permissions, authentication types |
---
## 🟣 Application-Specific Scripts
### MyEffectory
| Script | Description | Output |
|--------|-------------|---------|
| **GroupMappingsCheck.ps1** | Application-specific group mapping validation | CSV with mapping configurations |
---
## 📋 Prerequisites
### Required PowerShell Modules
```powershell
# Azure modules
Install-Module Az.Accounts, Az.Resources, Az.Storage, Az.KeyVault, Az.Monitor
Install-Module Microsoft.Graph.Identity.Governance
# Azure DevOps
Install-Module VSTeam
# SQL Server
Install-Module SqlServer
# Microsoft Graph
Install-Module Microsoft.Graph.Users, Microsoft.Graph.Groups
```
### Authentication Requirements
- **Azure**: `Connect-AzAccount` with appropriate RBAC permissions
- **Microsoft Graph**: `Connect-MgGraph` with required scopes
- **Azure DevOps**: Personal Access Token or OAuth authentication
- **SQL Server**: Azure AD authentication or SQL authentication
### Permission Requirements
| Platform | Required Permissions |
|----------|---------------------|
| **Azure** | Reader or higher on target resources, PIM Admin for PIM detection |
| **Entra ID** | Directory Reader, Group Member Read permissions |
| **Azure DevOps** | Project Reader, Repository Read permissions |
| **SQL Server** | Database Reader, View Server State permissions |
| **Snyk** | API token with Organization Read permissions |
---
## 🚀 Usage Guidelines
### Basic Execution
```powershell
# Run any script directly
.\Azure\Resources.ps1
.\DevOps\Repositories.ps1
.\Entra\GroupMemberships.ps1
```
### With Parameters (where supported)
```powershell
# Single subscription analysis
.\Azure\AzureRBAC.ps1 -SubscriptionId "your-subscription-id"
# Enable detailed debugging
.\Azure\AzureRBAC.ps1 -DetailedDebug
# Custom organization/project
.\DevOps\Repositories.ps1 -Organization "myorg" -Project "myproject"
```
### Output Management
All scripts generate timestamped CSV files in the format:
```
YYYY-MM-DD HHMM script_description.csv
```
### Best Practices
1. **Pre-Authentication**: Ensure proper authentication before running scripts
2. **Permissions**: Verify required permissions for target resources
3. **Network Connectivity**: Ensure access to required APIs and services
4. **Output Storage**: Consider output file locations and security
5. **Scheduling**: Many scripts are suitable for scheduled execution
6. **Error Handling**: Review script output for any errors or warnings
### Troubleshooting
- **Authentication Issues**: Verify token expiration and scope permissions
- **API Throttling**: Some scripts may encounter rate limits with large datasets
- **Permission Errors**: Ensure service principals or user accounts have sufficient privileges
- **Network Connectivity**: Verify access to required endpoints and APIs
---
## 📊 Output Analysis
### Common CSV Columns
Most scripts include standardized columns for:
- **Timestamps**: Creation and modification dates
- **Identifiers**: Unique IDs, names, and references
- **Governance**: Tags, ownership, environment classification
- **Security**: RBAC assignments, permissions, authentication types
- **Metadata**: Configuration details, status information
### Integration Options
- **Power BI**: Direct CSV import for dashboard creation
- **Excel**: Advanced filtering and pivot table analysis
- **Database**: Bulk import for historical trending
- **Automation**: Scheduled execution with result processing
---
## 🔄 Maintenance
### Regular Updates
- **Module Versions**: Keep PowerShell modules updated
- **API Changes**: Monitor for service API modifications
- **Permission Changes**: Verify continued access to required resources
- **Script Enhancements**: Check for new features and improvements
### Version Control
All scripts are maintained under version control with:
- Change tracking and history
- Documentation updates
- Testing and validation
- Community contributions
---
## 📞 Support
For issues, questions, or contributions:
- Review script help documentation (`Get-Help .\ScriptName.ps1 -Full`)
- Check error messages and troubleshooting sections
- Verify prerequisites and permissions
- Consult platform-specific documentation
---
*Last Updated: October 31, 2025*
*Script Collection Version: 2.0*

View File

@@ -1,76 +1,213 @@
<#
.SYNOPSIS
Comprehensive SQL Server database user audit across multiple Azure SQL servers.
.DESCRIPTION
This script connects to multiple Azure SQL servers and databases to generate a comprehensive audit
report of all database users, including their authentication types, creation dates, and permissions.
It provides essential information for security auditing, compliance reporting, and user access management.
Features:
• Multi-server database user enumeration across Azure SQL instances
• Authentication type detection (SQL, Windows, Azure AD)
• User creation and modification date tracking
• Comprehensive CSV reporting with timestamped output files
• Azure AD authentication using access tokens
• Automatic database discovery per server
.PARAMETER ServerList
Array of Azure SQL server FQDNs to audit. If not specified, uses a default list of common servers.
Each server should be provided as a fully qualified domain name (e.g., 'servername.database.windows.net').
.EXAMPLE
.\SQLUserCheck.ps1
Executes a complete user audit across all default Azure SQL servers and databases.
.EXAMPLE
.\SQLUserCheck.ps1 -ServerList @('signin-effectory.database.windows.net', 'c0m7f8nybr.database.windows.net')
Audits users on specific Azure SQL servers instead of using the default server list.
.EXAMPLE
Connect-AzAccount
.\SQLUserCheck.ps1
Ensures Azure authentication is established before running the SQL user audit.
.INPUTS
None. This script does not accept pipeline input.
.OUTPUTS
System.IO.FileInfo
Generates a timestamped CSV file containing user audit results with the following columns:
- ServerName: Azure SQL server name
- DatabaseName: Database name within the server
- UserName: Database user account name
- CreateDate: User account creation timestamp
- ModifyDate: Last modification timestamp
- Type: User principal type (User, Role, etc.)
- AuthenticationType: Authentication method (SQL_USER, WINDOWS_USER, EXTERNAL_USER)
.NOTES
Requires PowerShell 5.1 or later
Requires SqlServer PowerShell module
Requires Az.Accounts PowerShell module for Azure authentication
Prerequisites:
- Must be connected to Azure (Connect-AzAccount)
- Requires appropriate SQL Server permissions on target databases
- Network connectivity to Azure SQL servers
Security Considerations:
- Uses Azure AD authentication with access tokens
- Does not store or transmit SQL credentials
- Audit trail is maintained in timestamped CSV files
.LINK
https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-principals-transact-sql
https://docs.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-overview
.COMPONENT
SqlServer PowerShell Module, Azure PowerShell Module
.ROLE
Database Administration, Security Auditing, Compliance Reporting
.FUNCTIONALITY
Azure SQL Server user auditing, database security assessment, access management reporting
#>
param(
[Parameter(Mandatory = $false, HelpMessage = "Array of Azure SQL server FQDNs to audit")]
[string[]]$ServerList = @(
'c0m7f8nybr.database.windows.net',
'calculations.database.windows.net',
'effectory.database.windows.net',
'effectorycore.database.windows.net',
'logit-backup.database.windows.net',
'mhpfktialk.database.windows.net',
'participants.database.windows.net',
'signin-effectory.database.windows.net',
'sqlserver01prod.6a1f4aa9f43a.database.windows.net'
)
)
Import-Module SqlServer
# Ensure Azure authentication is available
# Uncomment the following lines if not already authenticated to Azure
#Clear-AzContext
#Connect-AzAccount
Write-Host "======================================================================================================================================================================"
Write-Host "Creating SQL user list."
Write-Host "======================================================================================================================================================================"
Write-Host "Creating comprehensive SQL user audit across Azure SQL servers."
Write-Host "===================================================================================================================================================================="
# Generate timestamped output filename
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$filename = ".\$date SQL User check.csv"
Write-Host "📄 Output will be saved to: $filename" -ForegroundColor Cyan
<#
.SYNOPSIS
Data structure for SQL Server database user information.
.DESCRIPTION
This class represents a database user record containing essential information
for security auditing and access management. Each instance captures user
details from a specific database on a specific server.
.NOTES
Properties align with sys.database_principals system catalog view columns
for consistent data representation across different SQL Server versions.
#>
class UserItem {
[string] $ServerName = ""
[string] $DatabaseName = ""
[string] $UserName = ""
[string] $CreateDate = ""
[string] $ModifyDate = ""
[string] $Type = ""
[string] $AuthenticationType = ""
[string] $ServerName = "" # Azure SQL server name
[string] $DatabaseName = "" # Database name where user exists
[string] $UserName = "" # Database user principal name
[string] $CreateDate = "" # User creation timestamp
[string] $ModifyDate = "" # Last modification timestamp
[string] $Type = "" # Principal type (SQL_USER, WINDOWS_USER, etc.)
[string] $AuthenticationType = "" # Authentication method (SQL, Windows, Azure AD)
}
$serverList= @('c0m7f8nybr.database.windows.net','calculations.database.windows.net','effectory.database.windows.net','effectorycore.database.windows.net',
'logit-backup.database.windows.net', 'mhpfktialk.database.windows.net', 'participants.database.windows.net', 'signin-effectory.database.windows.net',
'sqlserver01prod.6a1f4aa9f43a.database.windows.net')
Write-Host "🎯 Configured to audit $($ServerList.Count) Azure SQL servers" -ForegroundColor Green
# SQL query to discover all databases on each server
# Excludes system databases that are not relevant for user auditing
$databaseListQuery = @'
SELECT name, database_id, create_date
FROM sys.databases
order by name;
WHERE name NOT IN ('master', 'tempdb', 'model', 'msdb')
ORDER BY name;
'@
# SQL query to retrieve database user information
# Filters out roles ('R' type) and focuses on actual user principals
# Excludes 'guest' user which exists by default in all databases
$userListQuery = @'
select @@SERVERNAME as serverName,
SELECT @@SERVERNAME as serverName,
DB_NAME() as databaseName,
name as username,
create_date,
modify_date,
type_desc as type,
authentication_type_desc as authentication_type
from sys.database_principals
where type not in ('R')
and sid is not null
and name != 'guest'
order by name;
FROM sys.database_principals
WHERE type NOT IN ('R') -- Exclude database roles
AND sid IS NOT NULL -- Exclude built-in principals without SIDs
AND name NOT IN ('dbo', 'guest') -- Exclude default dbo and guest user
AND name NOT LIKE '##%' -- Exclude system-generated users
ORDER BY name;
'@
foreach ($server in $serverlist) {
# Initialize audit counters
$totalServersProcessed = 0
$totalDatabasesProcessed = 0
$totalUsersFound = 0
# Main server processing loop
foreach ($server in $ServerList) {
$totalServersProcessed++
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "Server [$server)]"
Write-Host "🖥️ Processing Server [$server] ($totalServersProcessed of $($ServerList.Count))" -ForegroundColor Cyan
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
try {
# Get Azure AD access token for SQL Database authentication
# This provides secure, passwordless authentication to Azure SQL
Write-Host "🔐 Obtaining Azure AD access token for SQL authentication..." -ForegroundColor Yellow
$access_token_secure = (Get-AzAccessToken -ResourceUrl https://database.windows.net).Token
$access_token = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($access_token_secure))
$connectionString = "Data Source=$server;Initial Catalog=master;Persist Security Info=False;Encrypt=True;TrustServerCertificate=False;Application Name=CloudEngineering";
# Configure secure connection string for Azure SQL
$connectionString = "Data Source=$server;Initial Catalog=master;Persist Security Info=False;Encrypt=True;TrustServerCertificate=False;Application Name=CloudEngineering-UserAudit"
$databases = Invoke-Sqlcmd -Query $databaseListQuery -ConnectionString $connectionString -AccessToken $access_token
# Discover all user databases on the current server
Write-Host "📋 Discovering databases on server..." -ForegroundColor Gray
$databases = Invoke-Sqlcmd -Query $databaseListQuery -ConnectionString $connectionString -AccessToken $access_token -ErrorAction Stop
Write-Host "✅ Found $($databases.Count) user databases to audit" -ForegroundColor Green
# Process each database on the current server
foreach ($database in $databases) {
$totalDatabasesProcessed++
Write-Host "Database [$($database.name)]"
Write-Host " 📊 Auditing Database [$($database.name)]" -ForegroundColor White
try {
[UserItem[]]$Result = @()
# Configure database-specific connection string
$databaseName = $database.name
$databaseConnectionString = "Data Source=$server;Initial Catalog=$databaseName;Persist Security Info=False;Encrypt=True;TrustServerCertificate=False;Application Name=CloudEngineering";
$databaseConnectionString = "Data Source=$server;Initial Catalog=$databaseName;Persist Security Info=False;Encrypt=True;TrustServerCertificate=False;Application Name=CloudEngineering-UserAudit"
$users = Invoke-Sqlcmd -Query $userListQuery -ConnectionString $databaseConnectionString -AccessToken $access_token
# Query database users and their authentication details
$users = Invoke-Sqlcmd -Query $userListQuery -ConnectionString $databaseConnectionString -AccessToken $access_token -ErrorAction Stop
# Process each user found in the database
foreach ($user in $users) {
$totalUsersFound++
[UserItem] $userItem = [UserItem]::new()
$userItem.ServerName = $server
$userItem.DatabaseName = $database.name
@@ -82,9 +219,100 @@ foreach ($server in $serverlist) {
$Result += $userItem
}
# Export results for this database to CSV
if ($Result.Count -gt 0) {
$Result | Export-Csv -Path $fileName -Append -NoTypeInformation
Write-Host " ✅ Found $($Result.Count) users in database [$($database.name)]" -ForegroundColor Green
} else {
Write-Host " No users found in database [$($database.name)]" -ForegroundColor Gray
}
}
catch {
Write-Warning " ❌ Failed to audit database [$($database.name)]: $($_.Exception.Message)"
}
}
}
catch {
Write-Warning "❌ Failed to process server [$server]: $($_.Exception.Message)"
Write-Warning " This may be due to connectivity issues or insufficient permissions"
}
}
# Generate comprehensive audit summary
Write-Host "======================================================================================================================================================================"
Write-Host "Done."
Write-Host "📊 SQL User Audit Summary" -ForegroundColor Green
Write-Host "======================================================================================================================================================================"
Write-Host ""
Write-Host "Audit Results:" -ForegroundColor Cyan
Write-Host "=============="
Write-Host "• Servers Processed: $totalServersProcessed of $($ServerList.Count)" -ForegroundColor White
Write-Host "• Databases Audited: $totalDatabasesProcessed" -ForegroundColor White
Write-Host "• Total Users Found: $totalUsersFound" -ForegroundColor White
# Analyze authentication types if CSV file exists
if (Test-Path $fileName) {
try {
$csvData = Import-Csv $fileName
$sqlAuthUsers = ($csvData | Where-Object { $_.type -eq "SQL_USER" }).Count
$azureADUsers = ($csvData | Where-Object { $_.type -eq "EXTERNAL_USER" }).Count
$windowsUsers = ($csvData | Where-Object { $_.type -eq "WINDOWS_USER" }).Count
$otherUsers = $csvData.Count - $sqlAuthUsers - $azureADUsers - $windowsUsers
Write-Host ""
Write-Host "Authentication Type Breakdown:" -ForegroundColor Yellow
Write-Host "=============================="
Write-Host "• SQL Authentication Users: $sqlAuthUsers" -ForegroundColor $(if($sqlAuthUsers -gt 0) {'Red'} else {'Green'})
Write-Host "• Azure AD Users: $azureADUsers" -ForegroundColor Green
Write-Host "• Windows Authentication Users: $windowsUsers" -ForegroundColor White
if ($otherUsers -gt 0) {
Write-Host "• Other Authentication Types: $otherUsers" -ForegroundColor Gray
}
# Security recommendation if SQL users found
if ($sqlAuthUsers -gt 0) {
Write-Host ""
Write-Host "⚠️ Security Notice:" -ForegroundColor Red
Write-Host " $sqlAuthUsers SQL Authentication users detected." -ForegroundColor Yellow
Write-Host " Consider migrating to Azure AD authentication for enhanced security." -ForegroundColor Yellow
}
}
catch {
Write-Warning "Could not analyze authentication types: $($_.Exception.Message)"
}
}
Write-Host ""
if (Test-Path $fileName) {
$fileSize = (Get-Item $fileName).Length
Write-Host "📄 Results Export:" -ForegroundColor Cyan
Write-Host "=================="
Write-Host "• Output File: $fileName" -ForegroundColor White
Write-Host "• File Size: $([math]::Round($fileSize/1KB, 2)) KB" -ForegroundColor White
Write-Host ""
# Display sample of results if file exists and has content
try {
$sampleData = Import-Csv $fileName | Select-Object -First 5
if ($sampleData) {
Write-Host "📋 Sample Results (First 5 Users):" -ForegroundColor Cyan
Write-Host "===================================="
$sampleData | Format-Table -AutoSize
}
}
catch {
Write-Warning "Could not display sample results: $($_.Exception.Message)"
}
}
Write-Host "✅ SQL User audit completed successfully!" -ForegroundColor Green
Write-Host ""
Write-Host "📝 Next Steps:" -ForegroundColor Yellow
Write-Host "=============="
Write-Host "• Review the CSV file for user access patterns" -ForegroundColor White
Write-Host "• Identify users with inappropriate authentication types" -ForegroundColor White
Write-Host "• Validate user access against business requirements" -ForegroundColor White
Write-Host "• Consider implementing Azure AD authentication where applicable" -ForegroundColor White
Write-Host "======================================================================================================================================================================"
Write-Host "🏁 Audit process completed." -ForegroundColor Green

View File

@@ -1,7 +1,102 @@
Write-Host "================================================================================================="
Write-Host "Creating Software Bill Of Materials."
Write-Host "================================================================================================="
<#
.SYNOPSIS
Generates a comprehensive Software Bill of Materials (SBOM) by consolidating Snyk dependency exports with enhanced package metadata.
.DESCRIPTION
This script processes multiple Snyk CSV dependency exports to create a unified Software Bill of Materials
with enriched package information. It combines vulnerability data from Snyk with additional metadata
from package repositories (NuGet) to provide comprehensive dependency insights.
Features:
• Multi-file CSV processing from Snyk dependency exports
• Enhanced NuGet package metadata enrichment (version history, deprecation status)
• Vulnerability aggregation across all projects and dependencies
• License information consolidation and analysis
• Deprecation detection for NuGet packages
• Comprehensive SBOM generation with timestamped output
• Support for npm, NuGet, and other package types
• Latest version tracking and publication date analysis
.PARAMETER None
This script does not accept parameters. Input files are processed from a predefined directory path.
.EXAMPLE
.\SBOM.ps1
Processes all Snyk CSV exports in c:\temp\snyk\ and generates a consolidated SBOM.
.EXAMPLE
# Prepare Snyk CSV exports first
# Export dependencies from Snyk UI or CLI to c:\temp\snyk\
.\SBOM.ps1
Creates enhanced SBOM with NuGet metadata enrichment.
.INPUTS
System.IO.FileInfo[]
Requires Snyk CSV dependency export files in c:\temp\snyk\ directory.
Expected CSV columns: id, name, version, type, issuesCritical, issuesHigh, issuesMedium,
issuesLow, dependenciesWithIssues, licenses, projects, license urls
.OUTPUTS
System.IO.FileInfo
Generates a timestamped CSV file containing enriched SBOM data with the following columns:
- FileName: Source CSV file name for traceability
- id: Package unique identifier from Snyk
- name: Package name
- version: Package version
- type: Package type (npm, nuget, maven, etc.)
- issuesCritical/High/Medium/Low: Vulnerability counts by severity
- dependenciesWithIssues: Count of vulnerable dependencies
- licenses: License information from Snyk
- projects: Projects using this dependency
- license_urls: URLs to license information
- latestVersion: Most recent available version (NuGet packages)
- latestVersionUrl: URL to latest version (NuGet packages)
- latestVersionPublishedDate: Publication date of latest version
- firstPublishedDate: Initial publication date of current version
- versionUrl: URL to current version information
- isDeprecated: Boolean indicating deprecation status
.NOTES
Requires PowerShell 5.1 or later
Requires PackageManagement module for NuGet package queries
Prerequisites:
- Snyk CSV dependency exports must be placed in c:\temp\snyk\ directory
- Network connectivity to nuget.org for package metadata enrichment
- PowerShell execution policy must allow script execution
Performance Considerations:
- Processing time depends on number of unique NuGet packages
- Each NuGet package requires API calls to nuget.org
- Large SBOM files may take several minutes to process
- Progress indicators show current processing status
Input File Requirements:
- Files must be CSV format with standard Snyk dependency export structure
- All CSV files in the source directory will be processed
- Files should contain complete dependency information from Snyk scans
.LINK
https://docs.snyk.io/products/snyk-open-source/dependency-management
https://docs.microsoft.com/en-us/nuget/api/overview
.COMPONENT
PackageManagement PowerShell Module, Snyk Dependency Exports
.ROLE
Software Composition Analysis, Security Governance, Compliance Reporting
.FUNCTIONALITY
SBOM generation, dependency analysis, vulnerability aggregation, package metadata enrichment
#>
Write-Host "========================================================================================================================================================================"
Write-Host "🔍 Software Bill of Materials (SBOM) Generator" -ForegroundColor Green
Write-Host "========================================================================================================================================================================"
Write-Host "📋 Processing Snyk dependency exports and enriching with package metadata..." -ForegroundColor Cyan
Write-Host ""
# Data structure class for enhanced SBOM entries
class CSVItem {
[string] $FileName = ""
[string] $id = ""
@@ -24,8 +119,36 @@ class CSVItem {
[string] $isDeprecated = ""
}
function PropagatePackage {
<#
.SYNOPSIS
Enriches package entries with additional metadata from package repositories.
.DESCRIPTION
This function queries package repositories (currently NuGet) to gather additional metadata
such as publication dates, latest versions, deprecation status, and repository URLs.
This enrichment provides comprehensive package lifecycle information for SBOM analysis.
.PARAMETER allItems
Array of CSVItem objects representing all packages in the SBOM.
.PARAMETER name
Name of the package to enrich with metadata.
.PARAMETER version
Version of the package to enrich.
.PARAMETER type
Package type (npm, nuget, maven, etc.). Only NuGet packages are currently enriched.
.PARAMETER progress
Progress indicator string showing current processing status.
.NOTES
Currently supports NuGet package enrichment only.
Makes API calls to NuGet.org which may impact performance for large SBOMs.
Handles deprecated packages by checking multiple metadata fields.
#>
function PropagatePackage {
param (
[CSVItem[]] $allItems,
[string] $name,
@@ -34,73 +157,138 @@ function PropagatePackage {
[string] $progress
)
# Find all SBOM entries matching this package
$foundItems = $allItems | Where-Object { ($_.name -eq $name) -and ($_.version -eq $version) -and ($_.type -eq $type)}
write-Host "[$progress] - Find $type package info for $name ($version) [$($foundItems.Length)]"
Write-Host " [$progress] 📦 Enriching $type package: $name ($version) - Found $($foundItems.Length) entries" -ForegroundColor Gray
# Currently only supports NuGet package enrichment
if ($type -ne "nuget") {
Write-Host " ⏭️ Skipping $type package (enrichment not supported)" -ForegroundColor DarkGray
return
}
$nuget = Find-Package $name -RequiredVersion $version -ProviderName Nuget
if ($null -eq $nuget) {
return
}
# Query NuGet repository for specific version metadata
try {
$lastNuget = Find-Package $name -ProviderName Nuget
$nuget = Find-Package $name -RequiredVersion $version -ProviderName Nuget -ErrorAction Stop
Write-Host " ✅ Found NuGet package metadata for $name $version" -ForegroundColor DarkGreen
}
catch {
Write-Host " ❌ Failed to find NuGet package: $name $version - $($_.Exception.Message)" -ForegroundColor DarkRed
return
}
catch {}
# Query for latest version information
$lastNuget = $null
try {
$lastNuget = Find-Package $name -ProviderName Nuget -ErrorAction Stop
Write-Host " 📈 Latest version found: $($lastNuget.Version)" -ForegroundColor DarkGreen
}
catch {
Write-Host " ⚠️ Could not determine latest version for $name" -ForegroundColor DarkYellow
}
# Enrich all matching SBOM entries with NuGet metadata
foreach ($propagateItem in $foundItems) {
# Set publication date for current version
$propagateItem.firstPublishedDate = $nuget.metadata["published"]
# Generate NuGet.org URL for current version
$propagateItem.versionUrl = "https://www.nuget.org/packages/$name/$version"
# Add latest version information if available
if ($null -ne $lastNuget) {
$propagateItem.latestVersion = $lastNuget.Version;
$propagateItem.latestVersion = $lastNuget.Version
$propagateItem.latestVersionPublishedDate = $lastNuget.metadata["published"]
$propagateItem.latestVersionUrl = "https://www.nuget.org/packages/$name/$($lastNuget.Version)"
}
$propagateItem.isDeprecated = ($null -eq $lastNuget) -or ($nuget.metadata["summary"] -like "*Deprecated*") -or ($nuget.metadata["title"] -like "*Deprecated*") -or ($nuget.metadata["tags"] -like "*Deprecated*")-or ($nuget.metadata["description"] -like "*Deprecated*")
# Determine deprecation status by checking multiple metadata fields
$isDeprecated = ($null -eq $lastNuget) -or
($nuget.metadata["summary"] -like "*Deprecated*") -or
($nuget.metadata["title"] -like "*Deprecated*") -or
($nuget.metadata["tags"] -like "*Deprecated*") -or
($nuget.metadata["description"] -like "*Deprecated*")
$propagateItem.isDeprecated = $isDeprecated
if ($isDeprecated) {
Write-Host " ⚠️ Package marked as deprecated: $name" -ForegroundColor Yellow
}
}
Write-Host " ✅ Successfully enriched $($foundItems.Length) SBOM entries" -ForegroundColor DarkGreen
return
}
# Generate timestamped output filename
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date snyk_npm_nuget_sbom.csv"
Write-Host "-------------------------------------------------------------------------------------------------"
Write-Host "Parsing CSV Files.."
Write-Host "-------------------------------------------------------------------------------------------------"
Write-Host "📁 Output file: $fileName" -ForegroundColor Gray
Write-Host ""
Write-Host "========================================================================================================================================================================"
Write-Host "📋 Phase 1: Processing Snyk CSV Dependencies" -ForegroundColor Cyan
Write-Host "========================================================================================================================================================================"
# Define source directory for Snyk CSV exports
$csvDependenciesExportPath = "c:\temp\snyk\*.csv"
$files = Get-ChildItem $csvDependenciesExportPath
# Locate all CSV files in the source directory
try {
$files = Get-ChildItem $csvDependenciesExportPath -ErrorAction Stop
Write-Host "✅ Found $($files.Count) Snyk CSV file(s) to process" -ForegroundColor Green
}
catch {
Write-Host "❌ No CSV files found in $csvDependenciesExportPath" -ForegroundColor Red
Write-Host " Please ensure Snyk dependency exports are placed in the directory" -ForegroundColor Yellow
exit 1
}
# Initialize SBOM collection
[CSVItem[]]$CSVItems = @()
$totalEntries = 0
# Process each CSV file
foreach($file in $files) {
Write-Host $file.FullName
Write-Host ""
Write-Host "📄 Processing file: $($file.Name)" -ForegroundColor Yellow
Write-Host " 📍 Path: $($file.FullName)" -ForegroundColor Gray
$csv = Import-Csv -Path $file.FullName
try {
$csv = Import-Csv -Path $file.FullName -ErrorAction Stop
Write-Host " 📊 Found $($csv.Count) dependency entries" -ForegroundColor White
# Process each dependency entry in the CSV file
$entryCount = 0
foreach ($csvLine in $csv) {
$entryCount++
$totalEntries++
# Create new SBOM entry
[CSVItem] $CSVItem = [CSVItem]::new()
$CSVItem.FileName = $file.Name
# Map Snyk CSV data to SBOM structure
$CSVItem.id = $csvLine.id
$CSVItem.name = $csvLine.name
$CSVItem.version = $csvLine.version
$CSVItem.type = $csvLine.type
# Vulnerability information
$CSVItem.issuesCritical = $csvLine.issuesCritical
$CSVItem.issuesHigh = $csvLine.issuesHigh
$CSVItem.issuesMedium = $csvLine.issuesMedium
$CSVItem.issuesLow = $csvLine.issuesLow
$CSVItem.dependenciesWithIssues = $csvLine.dependenciesWithIssues
# License and project information
$CSVItem.licenses = $csvLine.licenses
$CSVItem.projects = $csvLine.projects
$CSVItem.license_urls = $csvLine."license urls"
# Version and metadata (will be enriched later for NuGet packages)
$CSVItem.latestVersion = $csvLine.latestVersion
$CSVItem.latestVersionPublishedDate = $csvLine.latestVersionPublishedDate
$CSVItem.firstPublishedDate = $csvLine.firstPublishedDate
@@ -108,30 +296,114 @@ foreach($file in $files) {
$CSVItems += $CSVItem
}
}
Write-Host "-------------------------------------------------------------------------------------------------"
Write-Host "Determine objects.."
Write-Host "-------------------------------------------------------------------------------------------------"
$toDo = $CSVItems | Where-Object { $_.type -eq "nuget" } | Sort-Object -Property version| Sort-Object -Property name
$counter = 0
$length = $toDo.Length
foreach ($package in $toDo) {
$counter = $counter + 1
if ($package.latestVersion -eq "") {
PropagatePackage -allItems $CSVItems -name $package.name -type $package.type -version $package.version -progress ("{0:D4}/{1:D4}" -f $counter, $length)
Write-Host " ✅ Successfully processed $entryCount entries from $($file.Name)" -ForegroundColor Green
}
catch {
Write-Host " ❌ Error processing $($file.Name): $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Skipping this file and continuing..." -ForegroundColor Yellow
continue
}
}
Write-Host "-------------------------------------------------------------------------------------------------"
Write-Host "Saving overview.."
Write-Host "-------------------------------------------------------------------------------------------------"
Write-Host ""
Write-Host "📊 CSV Processing Summary:" -ForegroundColor Cyan
Write-Host "=========================="
Write-Host "• Files Processed: $($files.Count)" -ForegroundColor White
Write-Host "• Total Dependencies: $totalEntries" -ForegroundColor White
Write-Host ""
$CSVItems | Export-Csv -Path $fileName -NoTypeInformation
# Analyze package types
$packageTypes = $CSVItems | Group-Object type | Sort-Object Count -Descending
Write-Host "🔍 Package Type Distribution:" -ForegroundColor Cyan
foreach ($type in $packageTypes) {
Write-Host " $($type.Name): $($type.Count) packages" -ForegroundColor White
}
Write-Host "Done."
Write-Host ""
Write-Host "========================================================================================================================================================================"
Write-Host "🔧 Phase 2: NuGet Package Metadata Enrichment" -ForegroundColor Cyan
Write-Host "========================================================================================================================================================================"
# Identify unique NuGet packages that need enrichment
$nugetPackages = $CSVItems | Where-Object { $_.type -eq "nuget" -and $_.latestVersion -eq "" } |
Sort-Object -Property name, version -Unique
$nugetCount = $nugetPackages.Count
if ($nugetCount -eq 0) {
Write-Host " No NuGet packages require enrichment (all already have metadata)" -ForegroundColor Blue
} else {
Write-Host "📦 Found $nugetCount unique NuGet packages requiring metadata enrichment" -ForegroundColor White
Write-Host "⏱️ This process may take several minutes depending on network connectivity..." -ForegroundColor Yellow
Write-Host ""
$counter = 0
foreach ($package in $nugetPackages) {
$counter++
$progressPercent = [math]::Round(($counter / $nugetCount) * 100, 1)
Write-Host "🔄 [$counter/$nugetCount - $progressPercent%] Processing NuGet package: $($package.name) v$($package.version)" -ForegroundColor Cyan
PropagatePackage -allItems $CSVItems -name $package.name -type $package.type -version $package.version -progress ("{0:D4}/{1:D4}" -f $counter, $nugetCount)
}
}
Write-Host ""
Write-Host "========================================================================================================================================================================"
Write-Host "💾 Phase 3: SBOM Export and Analysis" -ForegroundColor Cyan
Write-Host "========================================================================================================================================================================"
Write-Host "📄 Exporting enhanced SBOM to CSV..." -ForegroundColor White
try {
$CSVItems | Export-Csv -Path $fileName -NoTypeInformation -ErrorAction Stop
if (Test-Path $fileName) {
$fileSize = [math]::Round((Get-Item $fileName).Length / 1KB, 2)
Write-Host "✅ SBOM export completed successfully!" -ForegroundColor Green
Write-Host " 📁 File: $fileName" -ForegroundColor Gray
Write-Host " 📏 Size: $fileSize KB" -ForegroundColor Gray
Write-Host " 📊 Records: $($CSVItems.Count)" -ForegroundColor Gray
}
}
catch {
Write-Host "❌ Failed to export SBOM: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "📈 SBOM Analysis Summary:" -ForegroundColor Cyan
Write-Host "========================"
# Vulnerability summary
$criticalCount = ($CSVItems | Where-Object { [int]$_.issuesCritical -gt 0 }).Count
$highCount = ($CSVItems | Where-Object { [int]$_.issuesHigh -gt 0 }).Count
$mediumCount = ($CSVItems | Where-Object { [int]$_.issuesMedium -gt 0 }).Count
$lowCount = ($CSVItems | Where-Object { [int]$_.issuesLow -gt 0 }).Count
Write-Host "🚨 Vulnerability Summary:" -ForegroundColor Yellow
Write-Host " Critical Issues: $criticalCount packages" -ForegroundColor $(if($criticalCount -gt 0) {'Red'} else {'Green'})
Write-Host " High Issues: $highCount packages" -ForegroundColor $(if($highCount -gt 0) {'Red'} else {'Green'})
Write-Host " Medium Issues: $mediumCount packages" -ForegroundColor $(if($mediumCount -gt 0) {'Yellow'} else {'Green'})
Write-Host " Low Issues: $lowCount packages" -ForegroundColor $(if($lowCount -gt 0) {'Yellow'} else {'Green'})
# Deprecation summary
$deprecatedCount = ($CSVItems | Where-Object { $_.isDeprecated -eq $true -or $_.isDeprecated -eq "True" }).Count
Write-Host ""
Write-Host "⚠️ Deprecation Summary:" -ForegroundColor Yellow
Write-Host " Deprecated Packages: $deprecatedCount" -ForegroundColor $(if($deprecatedCount -gt 0) {'Yellow'} else {'Green'})
# License summary
$unlicensedCount = ($CSVItems | Where-Object { $_.licenses -eq "" -or $_.licenses -eq $null }).Count
Write-Host ""
Write-Host "📋 License Summary:" -ForegroundColor Cyan
Write-Host " Packages with License Info: $($CSVItems.Count - $unlicensedCount)" -ForegroundColor Green
Write-Host " Packages without License Info: $unlicensedCount" -ForegroundColor $(if($unlicensedCount -gt 0) {'Yellow'} else {'Green'})
Write-Host ""
Write-Host "========================================================================================================================================================================"
Write-Host "✅ Software Bill of Materials generation completed successfully!" -ForegroundColor Green
Write-Host "========================================================================================================================================================================"

View File

@@ -1,11 +1,103 @@
<#
.SYNOPSIS
Comprehensive Snyk organization and project inventory across all accessible Snyk organizations.
.DESCRIPTION
This script connects to the Snyk API to retrieve a complete inventory of all organizations and their
associated projects. It provides essential information for security governance, project oversight,
and vulnerability management across your Snyk ecosystem.
Features:
• Multi-organization project enumeration across Snyk groups
• Project metadata collection including repository, type, and runtime information
• Comprehensive CSV reporting with timestamped output files
• Automatic project name parsing for repository identification
• Support for all Snyk project types (npm, Maven, Docker, etc.)
• Secure API key management via Azure Key Vault
.PARAMETER None
This script does not accept parameters. Configuration is managed through Azure Key Vault integration.
.EXAMPLE
.\SnykOverview.ps1
Executes a complete inventory of all Snyk organizations and projects with CSV export.
.EXAMPLE
Connect-AzAccount
Set-AzContext -SubscriptionId "your-subscription-id"
.\SnykOverview.ps1
Ensures proper Azure authentication before accessing Key Vault for Snyk API credentials.
.INPUTS
None. This script does not accept pipeline input.
.OUTPUTS
System.IO.FileInfo
Generates a timestamped CSV file containing Snyk project inventory with the following columns:
- OrganisationId: Snyk organization unique identifier
- OrganisationName: Human-readable organization name
- GroupId: Snyk group identifier for organization grouping
- OrganisationSlug: URL-friendly organization identifier
- ProjectId: Unique project identifier within Snyk
- ProjectRepo: Repository name extracted from project name
- ProjectName: Specific project/component name within repository
- ProjectType: Project technology type (npm, maven, docker, etc.)
- ProjectCreateDate: Project creation timestamp in Snyk
- ProjectTargetFile: Target manifest file (package.json, pom.xml, etc.)
- ProjectTargetRuntime: Runtime environment or version information
.NOTES
Requires PowerShell 5.1 or later
Requires Az.KeyVault PowerShell module for secure API key retrieval
Prerequisites:
- Must be connected to Azure (Connect-AzAccount)
- Requires access to the 'consoleapp' Key Vault containing 'SnykKey' secret
- Snyk API token must have appropriate organization and project read permissions
- Network connectivity to api.snyk.io (HTTPS outbound on port 443)
API Information:
- Uses Snyk REST API v2023-08-29~beta
- Rate limiting: Respects Snyk API rate limits (varies by plan)
- Pagination: Handles up to 100 projects per organization (adjust limit if needed)
Security Considerations:
- API tokens are securely retrieved from Azure Key Vault
- No credentials are stored in script or output files
- Uses encrypted HTTPS connections to Snyk API
- Audit trail is maintained in timestamped CSV files
.LINK
https://docs.snyk.io/snyk-api-info/snyk-rest-api
https://docs.snyk.io/snyk-api-info/authentication-for-api
.COMPONENT
Az.KeyVault PowerShell Module, Snyk REST API
.ROLE
Security Administration, DevSecOps, Vulnerability Management
.FUNCTIONALITY
Snyk project inventory, security governance, vulnerability management reporting
#>
$access_token = Get-AzKeyVaultSecret -VaultName "consoleapp" -Name "SnykKey" -AsPlainText
$head = @{ Authorization ="$access_token" }
$version = "2023-08-29%7Ebeta"
$ofs = ', '
# Generate timestamped output filename
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date snyk projects.csv"
Write-Host "========================================================================================================================================================================"
Write-Host "🔍 Snyk Organization and Project Inventory" -ForegroundColor Green
Write-Host "========================================================================================================================================================================"
Write-Host "🔐 Retrieving Snyk API credentials from Azure Key Vault..." -ForegroundColor Cyan
Write-Host "📊 Output file: $fileName" -ForegroundColor Gray
Write-Host ""
# Data structure class for Snyk project information
class SnykOverview {
[string] $OrganisationId = ""
[string] $OrganisationName = ""
@@ -20,29 +112,54 @@ class SnykOverview {
[string] $ProjectTargetRunTime = ""
}
# Initialize results collection
[SnykOverview[]]$Result = @()
$totalOrganizations = 0
$totalProjects = 0
Write-Host "📋 Retrieving Snyk organizations..." -ForegroundColor Cyan
# Retrieve all accessible Snyk organizations
$organisationUrl = "https://api.snyk.io/rest/orgs?version=$version"
$organisationResponse = Invoke-RestMethod -Uri $organisationUrl -Method GET -Headers $head
Write-Host "✅ Found $($organisationResponse.data.Count) Snyk organization(s)" -ForegroundColor Green
Write-Host ""
# Process each organization to retrieve its projects
foreach ($organisation in $organisationResponse.data)
{
$totalOrganizations++
$organisationId = $organisation.id
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "🏢 Processing Organization [$($organisation.attributes.name)] (ID: $organisationId)" -ForegroundColor Yellow
# Retrieve all projects for the current organization
$projectUrl = "https://api.snyk.io/rest/orgs/$organisationId/projects?version=$version&limit=100"
$projectResponse = Invoke-RestMethod -Uri $projectUrl -Method GET -Headers $head
$orgProjectCount = $projectResponse.data.Count
Write-Host " 📦 Found $orgProjectCount project(s) in this organization" -ForegroundColor White
# Process each project within the organization
foreach ($project in $projectResponse.data)
{
$totalProjects++
$projectName = $project.attributes.name
# Create new project record with comprehensive metadata
[SnykOverview] $SnykOverview = [SnykOverview]::new()
# Populate organization-level information
$SnykOverview.OrganisationId = $organisationId
$SnykOverview.OrganisationName = $organisation.attributes.name
$SnykOverview.GroupId = $organisation.attributes.group_id
$SnykOverview.OrganisationSlug = $organisation.attributes.slug
# Populate project-level information
$SnykOverview.ProjectId = $project.id
# Parse project name to extract repository and component names (format: "repo:component")
$SnykOverview.ProjectRepo = $projectName.Split(":")[0]
$SnykOverview.ProjectName = $projectName.Split(":")[1]
$SnykOverview.ProjectType = $project.attributes.type
@@ -54,6 +171,49 @@ foreach ($organisation in $organisationResponse.data)
}
}
Write-Host ""
Write-Host "========================================================================================================================================================================"
Write-Host "📊 Snyk Inventory Summary" -ForegroundColor Green
Write-Host "========================================================================================================================================================================"
Write-Host ""
Write-Host "Inventory Results:" -ForegroundColor Cyan
Write-Host "=================="
Write-Host "• Organizations Processed: $totalOrganizations" -ForegroundColor White
Write-Host "• Total Projects Found: $totalProjects" -ForegroundColor White
Write-Host ""
# Export results to CSV file
Write-Host "💾 Exporting results to CSV..." -ForegroundColor Cyan
$Result | Export-Csv -Path $fileName -NoTypeInformation -Force
$Result | Format-Table
if (Test-Path $fileName) {
$fileSize = [math]::Round((Get-Item $fileName).Length / 1KB, 2)
Write-Host "✅ Export completed successfully!" -ForegroundColor Green
Write-Host " 📁 File: $fileName" -ForegroundColor Gray
Write-Host " 📏 Size: $fileSize KB" -ForegroundColor Gray
} else {
Write-Host "❌ Export failed - file not created" -ForegroundColor Red
}
Write-Host ""
Write-Host "🔍 Project Type Breakdown:" -ForegroundColor Cyan
$projectTypes = $Result | Group-Object ProjectType | Sort-Object Count -Descending
foreach ($type in $projectTypes) {
Write-Host " $($type.Name): $($type.Count) projects" -ForegroundColor White
}
Write-Host ""
Write-Host "🏢 Organization Summary:" -ForegroundColor Cyan
$orgSummary = $Result | Group-Object OrganisationName | Sort-Object Count -Descending
foreach ($org in $orgSummary) {
Write-Host " $($org.Name): $($org.Count) projects" -ForegroundColor White
}
Write-Host ""
Write-Host "📋 Displaying first 10 results..." -ForegroundColor Cyan
$Result | Select-Object -First 10 | Format-Table -AutoSize
Write-Host ""
Write-Host "========================================================================================================================================================================"
Write-Host "✅ Snyk inventory completed successfully!" -ForegroundColor Green
Write-Host "========================================================================================================================================================================"

View File

@@ -1,80 +1,156 @@
/*
================================================================================
SQL SERVER DATABASE SIZE AND BACKUP ANALYSIS REPORT
================================================================================
DESCRIPTION:
Comprehensive analysis of SQL Server database sizes, space utilization,
and backup information. This script provides detailed insights into:
• Database file sizes (data and log files)
• Actual space used vs. allocated space
• Recovery model information
• Latest backup information for both full and log backups
• Backup size metrics including compression details
FEATURES:
• Multi-database analysis across entire SQL Server instance
• Real-time space usage calculation using dynamic SQL
• Backup history analysis from msdb.backupset
• Intelligent backup size reporting (compressed vs. uncompressed)
• Results ordered by total database size (largest first)
OUTPUT COLUMNS:
- database_id: SQL Server internal database identifier
- name: Database name
- state_desc: Database state (ONLINE, OFFLINE, etc.)
- recovery_model_desc: Recovery model (FULL, SIMPLE, BULK_LOGGED)
- total_size: Total allocated space (data + log files) in MB
- data_size: Total allocated data file space in MB
- data_used_size: Actual space used in data files in MB
- log_size: Total allocated log file space in MB
- log_used_size: Actual space used in log files in MB
- full_last_date: Date/time of most recent full backup
- full_size: Size of most recent full backup in MB
- log_last_date: Date/time of most recent log backup
- log_size: Size of most recent log backup in MB
USAGE SCENARIOS:
• Database capacity planning and growth analysis
• Storage optimization and space reclamation projects
• Backup strategy review and optimization
• Performance troubleshooting related to database size
• Compliance reporting for backup procedures
TECHNICAL IMPLEMENTATION:
• Uses temporary table to collect space usage across databases
• Dynamic SQL generation to query each online database
• Joins system catalog views (sys.databases, sys.master_files)
• Integrates backup history from msdb.backupset
• Handles compressed backup size calculations
PERMISSIONS REQUIRED:
• VIEW SERVER STATE permission
• Access to msdb database for backup history
• Database access to calculate space usage (connects to each database)
COMPATIBILITY:
• SQL Server 2008 R2 and later versions
• Works with Azure SQL Database (with limited backup history)
• Compatible with SQL Server on Linux
AUTHOR: Cloud Engineering Team
CREATED: Database administration and monitoring toolkit
UPDATED: Enhanced with comprehensive backup analysis and documentation
================================================================================
*/
IF OBJECT_ID('tempdb.dbo.#space') IS NOT NULL
DROP TABLE #space
-- Create temporary table to store actual space usage from each database
CREATE TABLE #space (
database_id INT PRIMARY KEY
, data_used_size DECIMAL(18,2)
, log_used_size DECIMAL(18,2)
, data_used_size DECIMAL(18,2) -- Actual space used in data files (MB)
, log_used_size DECIMAL(18,2) -- Actual space used in log files (MB)
)
-- Variable to hold dynamically generated SQL for cross-database queries
DECLARE @SQL NVARCHAR(MAX)
-- Generate dynamic SQL to collect actual space usage from each online database
-- This approach is necessary because FILEPROPERTY() must be executed in the context of each database
SELECT @SQL = STUFF((
SELECT '
USE [' + d.name + ']
INSERT INTO #space (database_id, data_used_size, log_used_size)
SELECT
DB_ID()
, SUM(CASE WHEN [type] = 0 THEN space_used END)
, SUM(CASE WHEN [type] = 1 THEN space_used END)
, SUM(CASE WHEN [type] = 0 THEN space_used END) -- Data files (type = 0)
, SUM(CASE WHEN [type] = 1 THEN space_used END) -- Log files (type = 1)
FROM (
SELECT s.[type], space_used = SUM(FILEPROPERTY(s.name, ''SpaceUsed'') * 8. / 1024)
SELECT s.[type], space_used = SUM(FILEPROPERTY(s.name, ''SpaceUsed'') * 8. / 1024) -- Convert pages to MB
FROM sys.database_files s
GROUP BY s.[type]
) t;'
FROM sys.databases d
WHERE d.[state] = 0
WHERE d.[state] = 0 -- Only include ONLINE databases
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '')
-- Execute the dynamic SQL to populate space usage data
EXEC sys.sp_executesql @SQL
-- Main query: Combine database information, file sizes, space usage, and backup data
SELECT
d.database_id
, d.name
, d.state_desc
, d.recovery_model_desc
, t.total_size
, t.data_size
, s.data_used_size
, t.log_size
, s.log_used_size
, bu.full_last_date
, bu.full_size
, bu.log_last_date
, bu.log_size
, d.state_desc -- Database state (ONLINE, OFFLINE, etc.)
, d.recovery_model_desc -- Recovery model (FULL, SIMPLE, BULK_LOGGED)
, t.total_size -- Total allocated space (data + log) in MB
, t.data_size -- Allocated data file space in MB
, s.data_used_size -- Actual space used in data files in MB
, t.log_size -- Allocated log file space in MB
, s.log_used_size -- Actual space used in log files in MB
, bu.full_last_date -- Most recent full backup date
, bu.full_size -- Most recent full backup size in MB
, bu.log_last_date -- Most recent log backup date
, bu.log_size -- Most recent log backup size in MB
FROM (
-- Calculate allocated file sizes from sys.master_files (covers all databases from master)
SELECT
database_id
, log_size = CAST(SUM(CASE WHEN [type] = 1 THEN size END) * 8. / 1024 AS DECIMAL(18,2))
, data_size = CAST(SUM(CASE WHEN [type] = 0 THEN size END) * 8. / 1024 AS DECIMAL(18,2))
, total_size = CAST(SUM(size) * 8. / 1024 AS DECIMAL(18,2))
FROM sys.master_files
, log_size = CAST(SUM(CASE WHEN [type] = 1 THEN size END) * 8. / 1024 AS DECIMAL(18,2)) -- Log files
, data_size = CAST(SUM(CASE WHEN [type] = 0 THEN size END) * 8. / 1024 AS DECIMAL(18,2)) -- Data files
, total_size = CAST(SUM(size) * 8. / 1024 AS DECIMAL(18,2)) -- Total allocated
FROM sys.master_files -- System view with all database files across instance
GROUP BY database_id
) t
JOIN sys.databases d ON d.database_id = t.database_id
LEFT JOIN #space s ON d.database_id = s.database_id
JOIN sys.databases d ON d.database_id = t.database_id -- Join with database catalog
LEFT JOIN #space s ON d.database_id = s.database_id -- Join with actual space usage data
LEFT JOIN (
-- Subquery to get latest backup information for each database
SELECT
database_name
, full_last_date = MAX(CASE WHEN [type] = 'D' THEN backup_finish_date END)
, full_size = MAX(CASE WHEN [type] = 'D' THEN backup_size END)
, log_last_date = MAX(CASE WHEN [type] = 'L' THEN backup_finish_date END)
, log_size = MAX(CASE WHEN [type] = 'L' THEN backup_size END)
, full_last_date = MAX(CASE WHEN [type] = 'D' THEN backup_finish_date END) -- Latest full backup date
, full_size = MAX(CASE WHEN [type] = 'D' THEN backup_size END) -- Latest full backup size
, log_last_date = MAX(CASE WHEN [type] = 'L' THEN backup_finish_date END) -- Latest log backup date
, log_size = MAX(CASE WHEN [type] = 'L' THEN backup_size END) -- Latest log backup size
FROM (
-- Inner query to get most recent backup of each type per database
SELECT
s.database_name
, s.[type]
, s.[type] -- 'D' = Full backup, 'L' = Log backup
, s.backup_finish_date
, backup_size =
, backup_size = -- Smart backup size calculation
CAST(CASE WHEN s.backup_size = s.compressed_backup_size
THEN s.backup_size
ELSE s.compressed_backup_size
END / 1048576.0 AS DECIMAL(18,2))
THEN s.backup_size -- No compression used
ELSE s.compressed_backup_size -- Use compressed size
END / 1048576.0 AS DECIMAL(18,2)) -- Convert bytes to MB
, RowNum = ROW_NUMBER() OVER (PARTITION BY s.database_name, s.[type] ORDER BY s.backup_finish_date DESC)
FROM msdb.dbo.backupset s
WHERE s.[type] IN ('D', 'L')
FROM msdb.dbo.backupset s -- SQL Server backup history
WHERE s.[type] IN ('D', 'L') -- Full and Log backups only
) f
WHERE f.RowNum = 1
WHERE f.RowNum = 1 -- Only most recent backup of each type
GROUP BY f.database_name
) bu ON d.name = bu.database_name
ORDER BY t.total_size DESC
) bu ON d.name = bu.database_name -- Join backup data with database info
ORDER BY t.total_size DESC -- Order by largest databases first

View File

@@ -1,28 +1,119 @@
/*
================================================================================
SQL SERVER TABLE SIZE AND SPACE UTILIZATION ANALYSIS
================================================================================
DESCRIPTION:
Comprehensive analysis of table sizes and space utilization within a SQL Server database.
This script provides detailed insights into storage consumption, helping identify:
• Tables consuming the most storage space
• Space efficiency and potential optimization opportunities
• Row counts and storage allocation patterns
• Unused space that could be reclaimed through maintenance
FEATURES:
• Per-table storage analysis with schema information
• Detailed space breakdown (total, used, unused)
• Multiple unit reporting (KB and MB for flexibility)
• Row count correlation with storage size
• Excludes system tables and diagnostic tables
• Results ordered by storage consumption (largest first)
OUTPUT COLUMNS:
- TableName: Name of the table
- SchemaName: Schema containing the table
- rows: Number of rows in the table (from partition statistics)
- TotalSpaceKB: Total allocated space in kilobytes
- TotalSpaceMB: Total allocated space in megabytes (rounded to 2 decimals)
- UsedSpaceKB: Actually used space in kilobytes
- UsedSpaceMB: Actually used space in megabytes (rounded to 2 decimals)
- UnusedSpaceKB: Allocated but unused space in kilobytes
- UnusedSpaceMB: Allocated but unused space in megabytes (rounded to 2 decimals)
USAGE SCENARIOS:
• Database capacity planning and growth forecasting
• Storage optimization and space reclamation projects
• Performance troubleshooting (large table identification)
• Archive and purging strategy development
• Index maintenance priority assessment
• Storage cost analysis and optimization
TECHNICAL IMPLEMENTATION:
• Queries system catalog views for accurate space calculations
• Aggregates data across all indexes and partitions for each table
• Joins allocation units to get precise storage metrics
• Filters out system tables and development artifacts
• Uses 8KB page size for accurate space calculations
DATA SOURCES:
• sys.tables: Table metadata and properties
• sys.indexes: Index information for space aggregation
• sys.partitions: Partition-level row counts and object relationships
• sys.allocation_units: Physical storage allocation details
• sys.schemas: Schema ownership and organization
FILTERS APPLIED:
• Excludes tables with names starting with 'dt%' (development/diagnostic tables)
• Excludes Microsoft shipped system tables (is_ms_shipped = 0)
• Excludes system objects (object_id > 255)
• Only includes user-created tables in user schemas
PERFORMANCE CONSIDERATIONS:
• Efficient query using system catalog views
• Minimal performance impact on production systems
• Results cached by SQL Server's metadata caching
• Suitable for regular monitoring and reporting
PERMISSIONS REQUIRED:
• VIEW DEFINITION permission on target database
• Membership in db_datareader role (recommended)
• Access to system catalog views (granted by default to most users)
COMPATIBILITY:
• SQL Server 2005 and later versions
• Azure SQL Database compatible
• SQL Server on Linux compatible
• Works with all SQL Server editions
AUTHOR: Cloud Engineering Team
CREATED: Database administration and monitoring toolkit
UPDATED: Enhanced with comprehensive space analysis and documentation
================================================================================
*/
SELECT
t.name AS TableName,
s.name AS SchemaName,
p.rows,
t.name AS TableName, -- Table name for identification
s.name AS SchemaName, -- Schema name for organization context
p.rows, -- Row count from partition statistics
-- Total allocated space calculations (pages * 8KB per page)
SUM(a.total_pages) * 8 AS TotalSpaceKB,
CAST(ROUND(((SUM(a.total_pages) * 8) / 1024.00), 2) AS NUMERIC(36, 2)) AS TotalSpaceMB,
-- Actually used space calculations
SUM(a.used_pages) * 8 AS UsedSpaceKB,
CAST(ROUND(((SUM(a.used_pages) * 8) / 1024.00), 2) AS NUMERIC(36, 2)) AS UsedSpaceMB,
-- Unused (allocated but not used) space calculations
(SUM(a.total_pages) - SUM(a.used_pages)) * 8 AS UnusedSpaceKB,
CAST(ROUND(((SUM(a.total_pages) - SUM(a.used_pages)) * 8) / 1024.00, 2) AS NUMERIC(36, 2)) AS UnusedSpaceMB
FROM
sys.tables t
sys.tables t -- Base table metadata
INNER JOIN
sys.indexes i ON t.object_id = i.object_id
sys.indexes i ON t.object_id = i.object_id -- All indexes on each table
INNER JOIN
sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id -- Partition information and row counts
INNER JOIN
sys.allocation_units a ON p.partition_id = a.container_id
sys.allocation_units a ON p.partition_id = a.container_id -- Physical storage allocation details
LEFT OUTER JOIN
sys.schemas s ON t.schema_id = s.schema_id
sys.schemas s ON t.schema_id = s.schema_id -- Schema information (LEFT JOIN for safety)
WHERE
t.name NOT LIKE 'dt%'
AND t.is_ms_shipped = 0
AND i.object_id > 255
t.name NOT LIKE 'dt%' -- Exclude diagnostic/development tables (common naming pattern)
AND t.is_ms_shipped = 0 -- Exclude Microsoft system tables
AND i.object_id > 255 -- Exclude system objects (object_id <= 255 are system objects)
GROUP BY
t.name, s.name, p.rows
t.name, s.name, p.rows -- Group by table, schema, and row count for aggregation
ORDER BY
TotalSpaceMB DESC, t.name
TotalSpaceMB DESC, -- Order by largest tables first
t.name -- Secondary sort by table name for consistency

View File

@@ -1,32 +1,158 @@
-- Query to list all objects in a database with their types
/*
================================================================================
SQL SERVER DATABASE OBJECT INVENTORY AND CATALOG ANALYSIS
================================================================================
DESCRIPTION:
Comprehensive inventory of all user-created database objects within a SQL Server database.
This script provides a complete catalog of database objects with type classification,
helping database administrators and developers understand the database structure and
identify objects for maintenance, documentation, or migration activities.
FEATURES:
• Complete database object inventory with schema context
• Human-readable object type descriptions
• Modification date tracking for change management
• Focused on user-created objects (excludes system objects)
• Filtered results to show primary development objects
• Organized output by schema, type, and name for easy navigation
OUTPUT COLUMNS:
- ObjectName: Fully qualified object name (Schema.ObjectName format)
- ObjectType: Human-readable description of the object type
- ModifiedDate: Last modification timestamp for change tracking
OBJECT TYPES INCLUDED:
• User Tables: Data storage tables created by users
• Views: Virtual tables and data abstractions
• Stored Procedures: Executable SQL code blocks
• Scalar Functions: Functions returning single values
• Inline Table Functions: Functions returning table results inline
• Table Functions: Functions returning table structures
• Extended Stored Procedures: System-level executable procedures
• CLR Objects: .NET Common Language Runtime integrated objects
- CLR Stored Procedures
- CLR Scalar Functions
- CLR Table Functions
- CLR Aggregate Functions
OBJECT TYPES EXCLUDED:
• Triggers (TR): Database triggers
• Primary Keys (PK): Primary key constraints
• Foreign Keys (F): Foreign key constraints
• Check Constraints (C): Data validation constraints
• Default Constraints (D): Default value constraints
• Unique Constraints (UQ): Unique value constraints
• System Tables (S): SQL Server system tables
• Service Queues (SQ): Service Broker queues
• Internal Tables (IT): SQL Server internal structures
USAGE SCENARIOS:
• Database documentation and inventory management
• Migration planning and object dependency analysis
• Code review and development lifecycle management
• Security auditing and permission analysis
• Database schema comparison and synchronization
• Object naming convention compliance checking
• Development team onboarding and knowledge transfer
TECHNICAL IMPLEMENTATION:
• Queries sys.objects catalog view for object metadata
• Joins with sys.schemas for complete naming context
• Uses CASE statement for user-friendly type descriptions
• Filters system objects using is_ms_shipped flag
• Additional filtering to focus on primary development objects
• Results ordered for logical grouping and easy scanning
DATA SOURCES:
• sys.objects: Core object metadata and properties
• sys.schemas: Schema ownership and organization information
FILTERS APPLIED:
• is_ms_shipped = 0: Excludes Microsoft system objects
• Object type filtering: Excludes constraints, triggers, and internal objects
• Focus on tables, views, procedures, and functions
PERFORMANCE CONSIDERATIONS:
• Efficient query using system catalog views
• Minimal performance impact (metadata-only query)
• Results typically cached by SQL Server
• Suitable for regular inventory and monitoring
PERMISSIONS REQUIRED:
• VIEW DEFINITION permission on target database
• Access to system catalog views (standard user access)
• Membership in db_datareader role (recommended)
COMPATIBILITY:
• SQL Server 2005 and later versions
• Azure SQL Database compatible
• SQL Server on Linux compatible
• Works with all SQL Server editions
CUSTOMIZATION OPTIONS:
• Modify object type filters to include/exclude specific types
• Add additional object properties (create_date, principal_id, etc.)
• Filter by specific schemas or naming patterns
• Add object size or usage statistics
AUTHOR: Cloud Engineering Team
CREATED: Database administration and development toolkit
UPDATED: Enhanced with comprehensive object type mapping and documentation
================================================================================
*/
-- Main query: Database object inventory with type classification and metadata
SELECT
s.name + '.' + o.name AS ObjectName,
CASE o.type
WHEN 'U' THEN 'User Table'
WHEN 'V' THEN 'View'
WHEN 'P' THEN 'Stored Procedure'
WHEN 'FN' THEN 'Scalar Function'
WHEN 'IF' THEN 'Inline Table Function'
WHEN 'TF' THEN 'Table Function'
WHEN 'TR' THEN 'Trigger'
WHEN 'PK' THEN 'Primary Key'
WHEN 'F' THEN 'Foreign Key'
WHEN 'C' THEN 'Check Constraint'
WHEN 'D' THEN 'Default Constraint'
WHEN 'UQ' THEN 'Unique Constraint'
WHEN 'S' THEN 'System Table'
WHEN 'SQ' THEN 'Service Queue'
WHEN 'IT' THEN 'Internal Table'
WHEN 'X' THEN 'Extended Stored Procedure'
WHEN 'PC' THEN 'CLR Stored Procedure'
WHEN 'FS' THEN 'CLR Scalar Function'
WHEN 'FT' THEN 'CLR Table Function'
WHEN 'AF' THEN 'CLR Aggregate Function'
ELSE 'Other'
s.name + '.' + o.name AS ObjectName, -- Fully qualified name (Schema.Object)
CASE o.type -- Convert system type codes to readable descriptions
-- Primary database objects (included in results)
WHEN 'U' THEN 'User Table' -- User-defined tables
WHEN 'V' THEN 'View' -- Views and indexed views
WHEN 'P' THEN 'Stored Procedure' -- T-SQL stored procedures
WHEN 'FN' THEN 'Scalar Function' -- Scalar user-defined functions
WHEN 'IF' THEN 'Inline Table Function' -- Inline table-valued functions
WHEN 'TF' THEN 'Table Function' -- Multi-statement table-valued functions
WHEN 'X' THEN 'Extended Stored Procedure' -- System extended procedures
-- CLR (Common Language Runtime) objects
WHEN 'PC' THEN 'CLR Stored Procedure' -- .NET CLR stored procedures
WHEN 'FS' THEN 'CLR Scalar Function' -- .NET CLR scalar functions
WHEN 'FT' THEN 'CLR Table Function' -- .NET CLR table-valued functions
WHEN 'AF' THEN 'CLR Aggregate Function' -- .NET CLR aggregate functions
-- Constraint and system objects (filtered out but documented)
WHEN 'TR' THEN 'Trigger' -- Database triggers
WHEN 'PK' THEN 'Primary Key' -- Primary key constraints
WHEN 'F' THEN 'Foreign Key' -- Foreign key constraints
WHEN 'C' THEN 'Check Constraint' -- Check constraints
WHEN 'D' THEN 'Default Constraint' -- Default constraints
WHEN 'UQ' THEN 'Unique Constraint' -- Unique constraints
WHEN 'S' THEN 'System Table' -- System tables
WHEN 'SQ' THEN 'Service Queue' -- Service Broker queues
WHEN 'IT' THEN 'Internal Table' -- Internal system tables
ELSE 'Other' -- Catch-all for unrecognized types
END AS ObjectType,
o.modify_date AS ModifiedDate
FROM sys.objects o
INNER JOIN sys.schemas s ON o.schema_id = s.schema_id
WHERE o.is_ms_shipped = 0 -- Exclude system objects
and not (o.type in ('TR','PK','F','C','D','UQ','S','SQ','IT','',''))
ORDER BY s.name, ObjectType, o.name;
o.modify_date AS ModifiedDate -- Last modification timestamp
FROM sys.objects o -- Core object metadata catalog
INNER JOIN sys.schemas s ON o.schema_id = s.schema_id -- Schema information for full naming context
WHERE
o.is_ms_shipped = 0 -- Exclude Microsoft system objects
AND NOT (o.type IN ( -- Filter out constraint and system object types
'TR', -- Triggers
'PK', -- Primary Keys
'F', -- Foreign Keys
'C', -- Check Constraints
'D', -- Default Constraints
'UQ', -- Unique Constraints
'S', -- System Tables
'SQ', -- Service Queues
'IT', -- Internal Tables
'' -- Empty/null types
))
ORDER BY
s.name, -- Primary sort: Schema name
ObjectType, -- Secondary sort: Object type for grouping
o.name -- Tertiary sort: Object name alphabetically