#requires -Modules Az.Accounts, Az.Resources, Az.Security <# .SYNOPSIS Grants the Costory Function App's Managed Identity the subscription-scope roles it needs, and (optionally) activates Defender for Cloud Standard tier. .DESCRIPTION Subscription-scope role assignments cannot live inside an Azure Managed Application template — the Appliance Resource Provider that executes the deployment has rights only inside the managed RG. This script performs the out-of-band grants that the template used to do, and must be run by a user who is Owner (or has User Access Administrator + Security Admin if -EnableDefender) on each target subscription. Roles assigned per target subscription: - Cost Management Reader (72fafb9e-0641-4937-9268-a91bfd8191a3) - Reader (acdd72a7-3385-48ef-bd42-f606fba81ae7) With -EnableDefender (Costory Premium plan only): - Microsoft.Security/pricings 'VirtualMachines' = Standard (subPlan P1) - Microsoft.Security/pricings 'StorageAccounts' = Standard These activate Defender Standard tier billing on the target subscription. .PARAMETER FunctionAppMsiPrincipalId Object ID (GUID) of the Costory Function App's system-assigned MSI. Read it from the managed application's output 'functionAppPrincipalId', or from the Azure portal: Function App → Identity → Object (principal) ID. .PARAMETER SubscriptionIds One or more subscription IDs to grant roles on. Must include the subscription where Costory itself is deployed (the template no longer assigns roles on it). .PARAMETER EnableDefender Switch — also enable Defender for Cloud Standard on the listed subscriptions. Required for Costory Premium plan's security perimeter scan feature. .EXAMPLE # Minimum case — single subscription, Free or Standard plan .\Set-CostorySubscriptionRoles.ps1 ` -FunctionAppMsiPrincipalId '00000000-0000-0000-0000-000000000000' ` -SubscriptionIds '11111111-1111-1111-1111-111111111111' .EXAMPLE # Premium plan with Defender, multiple subscriptions to scan .\Set-CostorySubscriptionRoles.ps1 ` -FunctionAppMsiPrincipalId '00000000-0000-0000-0000-000000000000' ` -SubscriptionIds '11111111-1111-1111-1111-111111111111','22222222-2222-2222-2222-222222222222' ` -EnableDefender #> param( [Parameter(Mandatory)] [ValidatePattern('^[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}$')] [string]$FunctionAppMsiPrincipalId, [Parameter(Mandatory)] [ValidatePattern('^[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}$')] [string[]]$SubscriptionIds, [switch]$EnableDefender ) $ErrorActionPreference = 'Stop' if (-not (Get-AzContext)) { Write-Host 'Connecting to Azure...' Connect-AzAccount | Out-Null } $roles = @( @{ Name = 'Cost Management Reader'; Id = '72fafb9e-0641-4937-9268-a91bfd8191a3' } @{ Name = 'Reader'; Id = 'acdd72a7-3385-48ef-bd42-f606fba81ae7' } ) foreach ($subId in $SubscriptionIds) { Write-Host '' Write-Host "=== Subscription $subId ===" -ForegroundColor Cyan Set-AzContext -SubscriptionId $subId | Out-Null foreach ($role in $roles) { $scope = "/subscriptions/$subId" $existing = Get-AzRoleAssignment -ObjectId $FunctionAppMsiPrincipalId -RoleDefinitionId $role.Id -Scope $scope -ErrorAction SilentlyContinue if ($existing) { Write-Host " [skip] $($role.Name) already assigned to $FunctionAppMsiPrincipalId" -ForegroundColor DarkGray } else { Write-Host " [add ] $($role.Name) -> $FunctionAppMsiPrincipalId" New-AzRoleAssignment -ObjectId $FunctionAppMsiPrincipalId -RoleDefinitionId $role.Id -Scope $scope | Out-Null } } if ($EnableDefender) { $defenderPlans = @( @{ Name = 'VirtualMachines'; PricingTier = 'Standard'; SubPlan = 'P1' } @{ Name = 'StorageAccounts'; PricingTier = 'Standard'; SubPlan = $null } ) foreach ($plan in $defenderPlans) { $args = @{ Name = $plan.Name PricingTier = $plan.PricingTier } if ($plan.SubPlan) { $args.SubPlan = $plan.SubPlan } Write-Host " [defender] $($plan.Name) -> $($plan.PricingTier)$($plan.SubPlan ? " ($($plan.SubPlan))" : '')" Set-AzSecurityPricing @args | Out-Null } } } Write-Host '' Write-Host 'Done. The Costory MSI now has cost-read access on the targeted subscriptions.' -ForegroundColor Green if ($EnableDefender) { Write-Host 'Defender for Cloud Standard tier billing is now active on the targeted subscriptions.' -ForegroundColor Yellow }