Automate Azure setup with PowerShell (how-to)
Prefer code over portal clicks? This how-to lets you build the Azure RM integration in no time.
Result: vScope app registration with Microsoft Graph + Defender for Endpoint application permissions, admin consent granted, and (optionally) Azure RBAC Reader on your chosen scope.
What the script does
Section titled “What the script does”- Creates an app registration + service principal for vScope.
- Grants required Microsoft Graph and Defender for Endpoint application permissions.
- Grants admin consent.
- Optionally assigns Azure RBAC Reader at a scope you provide.
Requirements (and what you change)
Section titled “Requirements (and what you change)”- PowerShell 7+
- Role: Global Admin, Privileged Role Admin, or Cloud App Admin (plus rights to assign RBAC if using
-RbacScope). - Modules:
Install-Module Microsoft.Graph -Scope CurrentUserInstall-Module Az.Accounts,Az.Resources -Scope CurrentUser(only when using-RbacScope)
- The script creates a tenant-wide app registration + service principal, grants admin consent to the listed Graph and Defender application permissions, and (optionally) assigns Azure RBAC at the scope you pass.
Quick start
Section titled “Quick start”Save the script below as script.ps1 in your working directory, then run:
.\script.ps1 -DisplayName "vScope App Registration" -GenerateClientSecret -RbacScope "/subscriptions/<subscription-id>"- Omit
-RbacScopeif you will assign Reader manually in the portal. - Keep the printed ClientSecret safe; it is shown once.
Script
Section titled “Script”#Requires -Modules Microsoft.Graph.Authentication, Microsoft.Graph.Applications
[CmdletBinding(SupportsShouldProcess = $true)]param( [Parameter(Mandatory = $true)] [string]$DisplayName,
[switch]$GenerateClientSecret,
[ValidateRange(1, 5)] [int]$SecretYears = 1,
[string]$SecretDisplayName = $null,
[switch]$AssignSubscriptionRole,
[string[]]$SubscriptionId = @(),
[string[]]$RbacScope = @(),
[string]$AzRoleDefinitionName = "Reader",
[switch]$SkipDefenderPermissions,
[switch]$OutputJson)
$ErrorActionPreference = "Stop"
if ($SubscriptionId.Count -gt 0 -and -not $AssignSubscriptionRole.IsPresent) { throw "Use -AssignSubscriptionRole together with -SubscriptionId, or omit both to skip Azure subscription RBAC setup."}
if ($AssignSubscriptionRole.IsPresent -and $SubscriptionId.Count -eq 0 -and $RbacScope.Count -eq 0) { throw "Use -SubscriptionId or -RbacScope when -AssignSubscriptionRole is set."}
foreach ($id in $SubscriptionId) { if ($id -notmatch "^[0-9a-fA-F-]{36}$") { throw "SubscriptionId '$id' is not a valid GUID." }
$RbacScope += "/subscriptions/$id"}
$GraphAppPermissions = @( "AuditLog.Read.All", "DeviceManagementConfiguration.Read.All", "DeviceManagementManagedDevices.Read.All", "Directory.Read.All", "Group.Read.All", "ProfilePhoto.Read.All", "Reports.Read.All", "Sites.Read.All", "Policy.Read.All", "Team.ReadBasic.All", "DeviceManagementApps.Read.All")
$DefenderAppPermissions = @( "AdvancedQuery.Read.All", "Machine.Read.All")
function Write-Step { param([string]$Message) Write-Host $Message -ForegroundColor Cyan}
function ConvertTo-ODataEscapedString { param([Parameter(Mandatory = $true)][string]$Value) return $Value.Replace("'", "''")}
function Invoke-WithRetry { param( [Parameter(Mandatory = $true)] [scriptblock]$ScriptBlock,
[int]$MaxAttempts = 6,
[int]$InitialDelaySeconds = 2,
[string]$OperationName = "operation" )
$delay = $InitialDelaySeconds for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { try { return & $ScriptBlock } catch { if ($attempt -eq $MaxAttempts) { throw }
Write-Warning "$OperationName failed on attempt $attempt/$MaxAttempts. Retrying in $delay seconds. Error: $($_.Exception.Message)" Start-Sleep -Seconds $delay $delay = [Math]::Min($delay * 2, 30) } }}
function Get-SingleMgServicePrincipalByDisplayName { param([Parameter(Mandatory = $true)][string]$Name)
$escapedName = ConvertTo-ODataEscapedString -Value $Name $servicePrincipalMatches = @(Get-MgServicePrincipal -Filter "displayName eq '$escapedName'" -All)
if ($servicePrincipalMatches.Count -gt 1) { throw "Multiple service principals found with displayName '$Name'. Cannot safely choose one." }
if ($servicePrincipalMatches.Count -eq 0) { return $null }
return $servicePrincipalMatches[0]}
function Get-SingleMgApplicationByDisplayName { param([Parameter(Mandatory = $true)][string]$Name)
$escapedName = ConvertTo-ODataEscapedString -Value $Name $applicationMatches = @(Get-MgApplication -Filter "displayName eq '$escapedName'" -All)
if ($applicationMatches.Count -gt 1) { throw "Multiple applications found with displayName '$Name'. Rename or remove duplicates before running this script." }
if ($applicationMatches.Count -eq 0) { return $null }
return $applicationMatches[0]}
function Get-MgServicePrincipalByAppIdWithRetry { param([Parameter(Mandatory = $true)][string]$AppId)
return Invoke-WithRetry -OperationName "Lookup service principal for appId '$AppId'" -ScriptBlock { $servicePrincipalMatches = @(Get-MgServicePrincipal -Filter "appId eq '$AppId'" -All) if ($servicePrincipalMatches.Count -gt 1) { throw "Multiple service principals found with appId '$AppId'. Cannot safely choose one." } if ($servicePrincipalMatches.Count -eq 0) { throw "Service principal for appId '$AppId' was not available yet." } return $servicePrincipalMatches[0] }}
function Get-AppRoleByValue { param( [Parameter(Mandatory = $true)]$ResourceSp, [Parameter(Mandatory = $true)][string]$RoleValue )
$role = $ResourceSp.AppRoles | Where-Object { $_.Value -eq $RoleValue -and $_.AllowedMemberTypes -contains "Application" -and $_.IsEnabled }
if (-not $role) { throw "App role '$RoleValue' not found on resource '$($ResourceSp.DisplayName)'." }
return $role}
function Set-RequiredResourceAccess { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)]$Application, [Parameter(Mandatory = $true)]$ResourceSp, [Parameter(Mandatory = $true)][array]$Roles )
$existingAccess = @() if ($Application.RequiredResourceAccess) { $existingAccess = @($Application.RequiredResourceAccess) }
$resourceAccessByAppId = @{} foreach ($entry in $existingAccess) { $resourceAccessByAppId[$entry.ResourceAppId] = @($entry.ResourceAccess) }
$currentResourceAccess = @() if ($resourceAccessByAppId.ContainsKey($ResourceSp.AppId)) { $currentResourceAccess = @($resourceAccessByAppId[$ResourceSp.AppId]) }
foreach ($role in $Roles) { $alreadyDeclared = $currentResourceAccess | Where-Object { $_.Id -eq $role.Id -and $_.Type -eq "Role" } if (-not $alreadyDeclared) { $currentResourceAccess += @{ id = $role.Id type = "Role" } } }
$resourceAccessByAppId[$ResourceSp.AppId] = $currentResourceAccess
$requiredResourceAccess = @() foreach ($resourceAppId in $resourceAccessByAppId.Keys) { $requiredResourceAccess += @{ resourceAppId = $resourceAppId resourceAccess = @($resourceAccessByAppId[$resourceAppId]) } }
if ($PSCmdlet.ShouldProcess($Application.DisplayName, "Update declared API permissions for '$($ResourceSp.DisplayName)'")) { Update-MgApplication -ApplicationId $Application.Id -RequiredResourceAccess $requiredResourceAccess }}
function Grant-AppRoleIfMissing { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)]$ClientSp, [Parameter(Mandatory = $true)]$ResourceSp, [Parameter(Mandatory = $true)]$Role )
$existingAssignments = @(Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $ClientSp.Id -All) $existing = $existingAssignments | Where-Object { $_.ResourceId -eq $ResourceSp.Id -and $_.AppRoleId -eq $Role.Id }
if ($existing) { Write-Host "Application permission '$($Role.Value)' on '$($ResourceSp.DisplayName)' already granted." -ForegroundColor DarkGreen return "AlreadyGranted" }
if ($PSCmdlet.ShouldProcess($ClientSp.DisplayName, "Grant application permission '$($Role.Value)' on '$($ResourceSp.DisplayName)'")) { Invoke-WithRetry -OperationName "Grant '$($Role.Value)' on '$($ResourceSp.DisplayName)'" -ScriptBlock { New-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $ClientSp.Id -BodyParameter @{ principalId = $ClientSp.Id resourceId = $ResourceSp.Id appRoleId = $Role.Id } | Out-Null }
Write-Host "Granted application permission '$($Role.Value)' on '$($ResourceSp.DisplayName)'." -ForegroundColor Green return "Granted" }
return "WhatIf"}
function Set-AzRoleAssignmentIfMissing { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)]$ServicePrincipal, [Parameter(Mandatory = $true)][string]$Scope, [Parameter(Mandatory = $true)][string]$RoleDefinitionName )
if ($Scope -notmatch "^/subscriptions/[0-9a-fA-F-]{36}(/.*)?$") { Write-Warning "RBAC scope '$Scope' does not look like an Azure Resource Manager scope. Continuing anyway." }
$existing = @(Get-AzRoleAssignment -ObjectId $ServicePrincipal.Id -RoleDefinitionName $RoleDefinitionName -Scope $Scope -ErrorAction SilentlyContinue) if ($existing.Count -gt 0) { Write-Host "Azure RBAC role '$RoleDefinitionName' already assigned at scope '$Scope'." -ForegroundColor DarkGreen return "AlreadyAssigned" }
if ($PSCmdlet.ShouldProcess($ServicePrincipal.DisplayName, "Assign Azure RBAC role '$RoleDefinitionName' at scope '$Scope'")) { Invoke-WithRetry -OperationName "Assign Azure RBAC role '$RoleDefinitionName' at '$Scope'" -ScriptBlock { New-AzRoleAssignment -ObjectId $ServicePrincipal.Id -RoleDefinitionName $RoleDefinitionName -Scope $Scope | Out-Null }
Write-Host "Azure RBAC role '$RoleDefinitionName' assigned at scope '$Scope'." -ForegroundColor Green return "Assigned" }
return "WhatIf"}
Write-Step "Connecting to Microsoft Graph..."$graphScopes = @( "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All", "Directory.Read.All")Connect-MgGraph -Scopes $graphScopes | Out-Null
$tenantId = (Get-MgContext).TenantIdif (-not $tenantId) { throw "Could not determine tenant id from Microsoft Graph context."}
Write-Step "Ensuring application '$DisplayName' exists..."$app = Get-SingleMgApplicationByDisplayName -Name $DisplayNameif (-not $app) { if ($PSCmdlet.ShouldProcess($DisplayName, "Create Microsoft Entra application")) { $app = New-MgApplication -DisplayName $DisplayName Write-Host "Application created. AppId: $($app.AppId)" -ForegroundColor Green }} else { Write-Host "Application already exists. AppId: $($app.AppId)" -ForegroundColor DarkGreen}
if (-not $app) { throw "Application is not available. If you used -WhatIf, rerun without -WhatIf to create it."}
Write-Step "Ensuring service principal exists..."$spMatches = @(Get-MgServicePrincipal -Filter "appId eq '$($app.AppId)'" -All)if ($spMatches.Count -gt 1) { throw "Multiple service principals found for appId '$($app.AppId)'. Cannot safely choose one."}
if ($spMatches.Count -eq 0) { if ($PSCmdlet.ShouldProcess($DisplayName, "Create service principal")) { New-MgServicePrincipal -AppId $app.AppId | Out-Null $sp = Get-MgServicePrincipalByAppIdWithRetry -AppId $app.AppId Write-Host "Service principal created. ObjectId: $($sp.Id)" -ForegroundColor Green }} else { $sp = $spMatches[0] Write-Host "Service principal already exists. ObjectId: $($sp.Id)" -ForegroundColor DarkGreen}
if (-not $sp) { throw "Service principal is not available. If you used -WhatIf, rerun without -WhatIf to create it."}
$secretValue = $null$secretExpires = $nullif ($GenerateClientSecret.IsPresent) { if (-not $SecretDisplayName) { $SecretDisplayName = "vScope-$((Get-Date).ToString('yyyy-MM-dd'))" }
$secretExpires = (Get-Date).ToUniversalTime().AddYears($SecretYears) Write-Host "Creating client secret '$SecretDisplayName'. The secret value is shown once." -ForegroundColor Yellow
if ($PSCmdlet.ShouldProcess($DisplayName, "Create client secret '$SecretDisplayName' expiring $($secretExpires.ToString('u'))")) { $passwordCredential = Add-MgApplicationPassword -ApplicationId $app.Id -PasswordCredential @{ displayName = $SecretDisplayName endDateTime = $secretExpires } $secretValue = $passwordCredential.SecretText }}
Write-Step "Loading Microsoft Graph resource service principal..."$graphSp = Get-MgServicePrincipalByAppIdWithRetry -AppId "00000003-0000-0000-c000-000000000000"
$permissionResults = @()
Write-Step "Ensuring Microsoft Graph application permissions..."$graphRoles = foreach ($permission in $GraphAppPermissions) { Get-AppRoleByValue -ResourceSp $graphSp -RoleValue $permission}
Set-RequiredResourceAccess -Application $app -ResourceSp $graphSp -Roles $graphRoles
foreach ($role in $graphRoles) { $status = Grant-AppRoleIfMissing -ClientSp $sp -ResourceSp $graphSp -Role $role $permissionResults += [pscustomobject]@{ Resource = $graphSp.DisplayName Permission = $role.Value Status = $status }}
if (-not $SkipDefenderPermissions.IsPresent) { Write-Step "Loading WindowsDefenderATP resource service principal..."
# vScope expects Defender permissions on the WindowsDefenderATP resource service principal. # Keep this displayName lookup aligned with product-side validation. $defenderSp = Get-SingleMgServicePrincipalByDisplayName -Name "WindowsDefenderATP"
if (-not $defenderSp) { Write-Warning "WindowsDefenderATP service principal not found. Defender permissions will be skipped." } else { Write-Step "Ensuring Defender for Endpoint application permissions..." $defenderRoles = foreach ($permission in $DefenderAppPermissions) { Get-AppRoleByValue -ResourceSp $defenderSp -RoleValue $permission }
Set-RequiredResourceAccess -Application $app -ResourceSp $defenderSp -Roles $defenderRoles
foreach ($role in $defenderRoles) { $status = Grant-AppRoleIfMissing -ClientSp $sp -ResourceSp $defenderSp -Role $role $permissionResults += [pscustomobject]@{ Resource = $defenderSp.DisplayName Permission = $role.Value Status = $status } } }}
$rbacResults = @()if ($AssignSubscriptionRole.IsPresent -or $RbacScope.Count -gt 0) { Write-Step "Ensuring Azure RBAC role assignments..."
if (-not (Get-Module -ListAvailable -Name Az.Accounts)) { throw "Az.Accounts is required when -RbacScope is used. Install the Az module or rerun without -RbacScope." } if (-not (Get-Module -ListAvailable -Name Az.Resources)) { throw "Az.Resources is required when -RbacScope is used. Install the Az module or rerun without -RbacScope." }
Import-Module Az.Accounts Import-Module Az.Resources
try { Get-AzContext | Out-Null } catch { Connect-AzAccount | Out-Null }
foreach ($scope in $RbacScope) { try { $status = Set-AzRoleAssignmentIfMissing -ServicePrincipal $sp -Scope $scope -RoleDefinitionName $AzRoleDefinitionName } catch { $status = "Failed" Write-Warning "Failed to assign Azure RBAC role '$AzRoleDefinitionName' at scope '$scope'. Assign later in Portal > Access control (IAM). Error: $($_.Exception.Message)" }
$rbacResults += [pscustomobject]@{ Scope = $scope RoleDefinitionName = $AzRoleDefinitionName Status = $status } }} else { Write-Host "Skipping Azure subscription RBAC setup. Use -AssignSubscriptionRole -SubscriptionId <id> to assign '$AzRoleDefinitionName'." -ForegroundColor Yellow}
$summary = [pscustomobject]@{ TenantId = $tenantId ApplicationName = $app.DisplayName ApplicationId = $app.AppId ApplicationObjectId = $app.Id ServicePrincipalId = $sp.Id ClientSecret = $secretValue ClientSecretExpires = $secretExpires Permissions = $permissionResults RbacAssignments = $rbacResults NextSteps = "Use TenantId, ApplicationId (ClientId) and ClientSecret (or certificate) in vScope's Azure RM credential."}
if ($OutputJson.IsPresent) { $summary | ConvertTo-Json -Depth 6} else { $summary | Format-List}After running
Section titled “After running”- You should see:
AppId,ServicePrincipalId, and, when requested, aClientSecret(shown once). Keep the secret secure. - Verify creation:
Get-MgServicePrincipal -AppId <AppId> - Verify optional RBAC:
Get-AzRoleAssignment -ObjectId <ServicePrincipalId> -Scope <scope> - Copy ApplicationId (ClientId) and ClientSecret into the Azure RM credential in vScope, then run Test Credential.
- If you skipped
-RbacScope, assign Reader manually in the portal and retest. - Common issue: missing Graph consent scopes or not being logged into the right Azure context will block RBAC; re-run
Connect-AzAccountin that case.