Skip to content

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.

  • 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.
  • 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 CurrentUser
    • Install-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.

Save the script below as script.ps1 in your working directory, then run:

Terminal window
.\script.ps1 -DisplayName "vScope App Registration" -GenerateClientSecret -RbacScope "/subscriptions/<subscription-id>"
  • Omit -RbacScope if you will assign Reader manually in the portal.
  • Keep the printed ClientSecret safe; it is shown once.
Terminal window
#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).TenantId
if (-not $tenantId) {
throw "Could not determine tenant id from Microsoft Graph context."
}
Write-Step "Ensuring application '$DisplayName' exists..."
$app = Get-SingleMgApplicationByDisplayName -Name $DisplayName
if (-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 = $null
if ($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
}
  • You should see: AppId, ServicePrincipalId, and, when requested, a ClientSecret (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-AzAccount in that case.