Tag Archives: MS Graph API

Windows 365 Cloud Apps – Provisioning Policy – PowerShell (Graph Rest API) – Part 1

26 Sep

This is part one of a two-part series on Windows 365 Cloud Apps. In this post, we’ll walk through what Cloud Apps are and how to create the provisioning policy with PowerShell. In part two, we’ll publish the apps themselves. I’ll also include the PowerShell script that uses Azure/Graph REST APIs.

What is Windows 365 Cloud Apps?

Windows 365 Cloud Apps let you give users access to specific apps streamed from a Cloud PC—without handing out a full desktop to everyone. Under the hood, Cloud Apps run on Windows 365 Frontline Cloud PCs in Shared mode. That licensing model is designed for shift or part-time staff: many users can be assigned, but only one active session per license at a time.

Think of it as “just-the-apps” VDI: Outlook, Word, your line-of-business app—delivered from the cloud—with the management simplicity of Windows 365 and Intune.

Why customers care: You streamline app delivery, lower overhead, and modernize VDI without building and babysitting a big remote desktop estate.

Cloud Apps vs AVD Published Apps vs “Traditional” VDI Published Apps

TopicWindows 365 Cloud AppsAzure Virtual Desktop Published AppsTraditional VDI Published Apps
What users seeIndividual apps streamed from a Cloud PC; no full desktopIndividual apps from session hosts in Azure Virtual DesktopIndividual apps from on-prem or hosted RDS/Horizon/Citrix farms
Infra you manageCloud PC lifecycle via Intune; Microsoft operates the fabricYou design & operate host pools, scaling, FSLogix, imagesYou run the farm: brokers, gateways, hypervisors, storage
Licensing / sessionsFrontline: many users per license, 1 active session per licensePer-user/per-device or CALs + Azure consumption; multiple sessions per hostPer-user/device + on-prem infra costs
Admin planeIntune + Windows 365Azure Portal + ARM + Host pool automationVendor consoles + on-prem change management
App packagingStart-menu discovered apps from the image (MSIX/Appx discovery expanding)MSI/MSIX; MSIX App Attach; image-basedMSI/MST/App-V/Citrix packages, etc.
Who it’s great forTask/shift workers; predictable, lightweight app accessBroad use cases; granular scale & controlHeavily customized legacy estates, on-prem constraints

Mental model:

If you need elastic host pools or platform primitives, choose AVD.

  • If you’re tied to on-prem or specific vendor features, you might keep traditional VDI, but expect more ops work.
  • If you like the “managed Cloud PC” experience and want app-only access, choose Cloud Apps.
  • If you like the “managed Cloud PC” experience and want app-only access, choose Cloud Apps.

PowerShell: create the policy via REST/Graph

  • Auth: Provide $TenantId, $ClientId, $ClientSecret from your app registration. Grant/admin-consent the scopes listed above. If you are not aware of how to create the app registration, you can follow here – How to register an app in Microsoft Entra ID – Microsoft identity platform | Microsoft Learn
  • Image: Set $ImageType (e.g., "gallery") and $ImageId for your chosen image.
  • Region: $RegionName (e.g., australiaeast or "automatic").
  • Assignment:
    • $GroupId: Entra group whose members should see the Cloud Apps.
    • $ServicePlanId: the Frontline size (e.g., FL 2vCPU/8GB/128GB in the example).
    • $AllotmentCount: how many concurrent sessions you want available for this policy.
    • $AllotmentDisplayName: a friendly label that shows up with the assignment.
  • Verification/Polling: The script dumps the policy with assignments and can optionally poll for provisioned Cloud PCs tied to the policy.
  • Get-or-Create a Cloud Apps provisioning policy (userExperienceType = cloudApp, provisioningType = sharedByEntraGroup, Azure AD Join in a specified region).
  • Assigns the policy to an Entra group with service plan, capacity (allotment), and a friendly label

Required permissions (app registration – admin consent):

  • CloudPC.ReadWrite.All (and CloudPC.Read.All)
  • DeviceManagementServiceConfig.ReadWrite.All (for policy config)
<#
Create (or reuse) a Windows 365 "Cloud Apps" provisioning policy, assign an Entra group
with size + capacity + label, then verify assignment (via $expand=assignments) and optionally
poll for provisioned Cloud PCs. Uses Microsoft Graph beta.

Key note: /assignments endpoint returns 404 by design; use $expand=assignments. See MS docs.
#>

# ==========================
# 0) CONFIG — EDIT THESE
# ==========================
$TenantId     = "<Copy/Paste Tenant ID>"
$ClientId     = "<Copy/Paste Client ID>"
$ClientSecret = "<Copy/Paste ClientSecret ID>"

# Policy
$DisplayName  = "Cloud-Apps-Prov-4"
$Description  = "Cloud Apps Prov Policy - Frontline"
$EnableSSO    = $true
$RegionName   = "australiaeast"   # or "automatic"
$Locale       = "en-AU"
$Language     = "en-AU"

# Image (gallery)
$ImageType    = "gallery"
$ImageId      = "microsoftwindowsdesktop_windows-ent-cpc_win11-24H2-ent-cpc-m365"

# Assignment
$GroupId              = "b582705d-48be-4e4b-baac-90e5b50ebdf2"   # Entra ID Group
$ServicePlanId        = "057efbfe-a95d-4263-acb0-12b4a31fed8d"   # FL 2vCPU/8GB/128GB
$AllotmentCount       = 1
$AllotmentDisplayName = "CP-FL-Shared-CloudApp-1"

# Optional provisioning poll
$VerifyDesiredCount   = $AllotmentCount
$VerifyMaxTries       = 30
$VerifyDelaySec       = 30

# ==========================
# Helpers
# ==========================
function New-GraphUri {
  param(
    [Parameter(Mandatory)][string]$Path,        # e.g. "/beta/deviceManagement/virtualEndpoint/provisioningPolicies"
    [hashtable]$Query
  )
  $cleanPath = '/' + ($Path -replace '^\s*/+','' -replace '\s+$','')
  $b = [System.UriBuilder]::new("https","graph.microsoft.com")
  $b.Path = $cleanPath
  if ($Query -and $Query.Count -gt 0) {
    Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue | Out-Null
    $nvc = [System.Web.HttpUtility]::ParseQueryString([string]::Empty)
    foreach ($k in $Query.Keys) { $nvc.Add($k, [string]$Query[$k]) }
    $b.Query = $nvc.ToString()
  } else { $b.Query = "" }
  return $b.Uri.AbsoluteUri
}

function Invoke-Graph {
  param(
    [Parameter(Mandatory)][ValidateSet('GET','POST','PATCH','DELETE','PUT')][string]$Method,
    [Parameter(Mandatory)][string]$Uri,
    [hashtable]$Headers,
    $Body
  )
  try {
    if ($PSBoundParameters.ContainsKey('Body')) {
      return Invoke-RestMethod -Method $Method -Uri $Uri -Headers $Headers -Body ($Body | ConvertTo-Json -Depth 20)
    } else {
      return Invoke-RestMethod -Method $Method -Uri $Uri -Headers $Headers
    }
  } catch {
    Write-Warning "HTTP $Method $Uri failed: $($_.Exception.Message)"
    try {
      $resp = $_.Exception.Response
      if ($resp -and $resp.GetResponseStream()) {
        $reader = New-Object IO.StreamReader($resp.GetResponseStream())
        $text   = $reader.ReadToEnd()
        Write-Host "Response body:" -ForegroundColor DarkYellow
        Write-Host $text
      }
    } catch {}
    throw
  }
}

# ==========================
# 1) AUTH — Client Credentials
# ==========================
$TokenEndpoint = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
$tokenForm = @{
  client_id     = $ClientId
  client_secret = $ClientSecret
  scope         = "https://graph.microsoft.com/.default"
  grant_type    = "client_credentials"
}
$auth = Invoke-RestMethod -Method Post -Uri $TokenEndpoint -Body $tokenForm -ContentType 'application/x-www-form-urlencoded'
if (-not $auth.access_token) { throw "No access_token returned. Ensure CloudPC.ReadWrite.All (+ CloudPC.Read.All) are admin-consented." }

$Headers = @{
  Authorization = "Bearer $($auth.access_token)"
  "Content-Type" = "application/json"
  "Prefer"       = "include-unknown-enum-members"
}

# Base paths
$PoliciesPath = "/beta/deviceManagement/virtualEndpoint/provisioningPolicies"
$CloudPcsPath = "/beta/deviceManagement/virtualEndpoint/cloudPCs"

# ==========================
# 2) Ensure (Get-or-Create) the policy
# ==========================
$escapedName = $DisplayName.Replace("'","''")
$findUri     = New-GraphUri -Path $PoliciesPath -Query @{ '$filter' = "displayName eq '$escapedName'" }

Write-Host "Finding policy by name: $DisplayName" -ForegroundColor Cyan
try { $existing = Invoke-Graph -Method GET -Uri $findUri -Headers $Headers }
catch { Write-Warning "Name lookup failed; proceeding to create."; $existing = $null }

if ($existing -and $existing.value -and $existing.value.Count -gt 0) {
  $policy   = $existing.value | Select-Object -First 1
  $policyId = $policy.id
  Write-Host "Using existing policy '$DisplayName' (id: $policyId)" -ForegroundColor Yellow
} else {
  $createBody = @{
    "@odata.type"           = "#microsoft.graph.cloudPcProvisioningPolicy"
    displayName             = $DisplayName
    description             = $Description
    enableSingleSignOn      = $EnableSSO
    imageType               = $ImageType
    imageId                 = $ImageId
    managedBy               = "windows365"
    microsoftManagedDesktop = @{
      "@odata.type" = "microsoft.graph.microsoftManagedDesktop"
      managedType   = "notManaged"
    }
    windowsSetting          = @{
      "@odata.type" = "microsoft.graph.cloudPcWindowsSetting"
      locale        = $Locale
    }
    windowsSettings         = @{
      "@odata.type" = "microsoft.graph.cloudPcWindowsSettings"
      language      = $Language
    }
    userExperienceType      = "cloudApp"
    provisioningType        = "sharedByEntraGroup"
    domainJoinConfigurations = @(
      @{
        "@odata.type"  = "microsoft.graph.cloudPcDomainJoinConfiguration"
        domainJoinType = "azureADJoin"
        type           = "azureADJoin"
        regionName     = $RegionName
      }
    )
  }

  $createUri = New-GraphUri -Path $PoliciesPath
  Write-Host "Creating policy '$DisplayName'..." -ForegroundColor Cyan
  $policy   = Invoke-Graph -Method POST -Uri $createUri -Headers $Headers -Body $createBody
  $policyId = $policy.id
  if (-not $policyId) { throw "Create succeeded but no policy id returned." }
  Write-Host "Created policy id: $policyId" -ForegroundColor Green
}

# ==========================
# 3) Assign — group + size + capacity + label
# ==========================
$assignUri = New-GraphUri -Path ($PoliciesPath + "/$policyId/assign")

Write-Host "Clearing existing assignments..." -ForegroundColor DarkGray
Invoke-Graph -Method POST -Uri $assignUri -Headers $Headers -Body @{ assignments = @() } | Out-Null

$assignBody = @{
  assignments = @(
    @{
      target = @{
        "@odata.type"          = "#microsoft.graph.cloudPcManagementGroupAssignmentTarget"
        groupId                = $GroupId
        servicePlanId          = $ServicePlanId
        allotmentLicensesCount = $AllotmentCount
        allotmentDisplayName   = $AllotmentDisplayName
      }
    }
  )
}
Write-Host "Assigning policy to group $GroupId (plan $ServicePlanId, count $AllotmentCount)..." -ForegroundColor Cyan
Invoke-Graph -Method POST -Uri $assignUri -Headers $Headers -Body $assignBody | Out-Null
Write-Host "Assignment submitted." -ForegroundColor Green

# ==========================
# 4) Verify — read policy with $expand=assignments (RETRY)
# ==========================
$expandUri = New-GraphUri -Path ($PoliciesPath + "/$policyId") -Query @{ '$expand' = 'assignments' }
Write-Host "Reading back policy + assignments via $expand..." -ForegroundColor Cyan

$verify = $null
for ($try=1; $try -le 12; $try++) {
  try {
    $verify = Invoke-Graph -Method GET -Uri $expandUri -Headers $Headers
    if ($verify.assignments -and $verify.assignments.Count -gt 0) { break }
    Write-Host "Assignments not materialized yet (attempt $try). Waiting 3s..." -ForegroundColor Yellow
  } catch {
    Write-Warning "Expand read attempt $try failed; retrying in 3s..."
  }
  Start-Sleep -Seconds 3
}
if (-not $verify) { throw "Failed to read policy with assignments after retries." }

$verify | ConvertTo-Json -Depth 20 | Write-Output

# Sanity: confirm the expected assignment
$expected = $verify.assignments | Where-Object {
  $_.target.groupId -eq $GroupId -and
  $_.target.servicePlanId -eq $ServicePlanId -and
  $_.target.allotmentLicensesCount -eq $AllotmentCount -and
  ($_.target.allotmentDisplayName -eq $AllotmentDisplayName -or -not $AllotmentDisplayName)
}
if ($expected) {
  Write-Host "✅ Assignment present with expected group, plan, and count." -ForegroundColor Green
} else {
  Write-Warning "Assignment not found with expected fields. See dump above."
}

# ==========================
# 5) (Optional) Poll for provisioned Cloud PCs for this policy
# ==========================
if ($VerifyDesiredCount -gt 0) {
  $cloudPcsUri = New-GraphUri -Path $CloudPcsPath -Query @{ '$filter' = "provisioningPolicyId eq '$policyId'" }
  for ($i = 1; $i -le $VerifyMaxTries; $i++) {
    try {
      $cloudPcs   = Invoke-Graph -Method GET -Uri $cloudPcsUri -Headers $Headers
      $pcsForPlan = $cloudPcs.value | Where-Object { $_.servicePlanId -eq $ServicePlanId -or -not $_.psobject.Properties.Name.Contains('servicePlanId') }
      $count      = ($pcsForPlan | Measure-Object).Count
      if ($count -ge $VerifyDesiredCount) {
        Write-Host "Provisioned Cloud PCs for policy: $count (target $VerifyDesiredCount) ✅" -ForegroundColor Green
        $pcsForPlan | ConvertTo-Json -Depth 10 | Write-Output
        break
      } else {
        Write-Host "Provisioned Cloud PCs for policy: $count (waiting for $VerifyDesiredCount) … attempt $i/$VerifyMaxTries" -ForegroundColor Yellow
      }
    } catch {
      Write-Warning "Provisioning check failed on attempt ${i}: $($_.Exception.Message)"
    }
    Start-Sleep -Seconds $VerifyDelaySec
  }
}

GitHub Link – avdwin365mem/W365-CloudApp-Prov-Policy at main · askaresh/avdwin365mem

Provisioning Policy Details – UI

Policy

Overview

Tips, gotchas, and troubleshooting

  • App discovery: Ensure the app has a Start menu shortcut on the image. That’s how Cloud Apps gets its list.
  • Security baselines: If your tenant enforces restrictions on PowerShell in the image at discovery time, discovery can fail.
  • MSIX/Appx: Discovery is expanding—classic installers show up first; some Appx/MSIX apps (e.g., newer Teams) may not appear yet.
  • Concurrency math: Active sessions for the policy are capped by assigned Frontline license count on that policy.
  • Schema drift: These are beta endpoints. If you hit a property/enum change, the script’s warnings will surface the response body—update the field names accordingly.

What’s next (Part 2)

We’ll move to All Cloud Apps to publish the discovered apps, tweak display name/description/command line/icon index, confirm they appear in Windows App, and cover unpublish/reset workflows—with your screenshots.

I hope you find this helpful information for creating a Cloud App using PowerShell. If I have missed any steps or details, I will be happy to update the post.

Thanks,
Aresh Sarkari

Windows Intune Settings Catalog Policy to Disable Windows Copilot – Windows 365 Cloud PC & Windows 11 + Bonus PowerShell

28 Feb

In the latest update of Windows Intune, a new method has been introduced to disable Windows Copilot through the settings catalog policy. Previously, the technique revolved around CSP and registry, but now users can conveniently manage this setting directly within the settings catalog. There are plenty of blog posts showing how to do it via CSP and registry, and we are not going into that here.

What is Windows Copilot?

For those unfamiliar, Windows Copilot (formerly Bing Chat) is a built-in AI-powered intelligent assistant that helps you get answers and inspirations from across the web, supports creativity and collaboration, and enables you to focus on the task at hand.

How to Disable Windows Copilot via Settings Catalog Policy

The process to disable Windows Copilot through the settings catalog policy is simple and straightforward. Here’s a step-by-step guide:

  • Log in to the Microsoft Intune admin center.
  • Create a configuration profile for Windows 10 and later devices with the Settings catalog and slect Create
  • Enter the profile Name “DisableCopilot” and Description “Settings to disable Copilot” and select Next
  • Under the Setting Picker select category as Windows AI and select the setting “Turn off Copilot in Windows (user).”
  • Next Within the settings select the silder and “Disable the Copilot settings”
  • Assign the Policy to a Windows 365 or Windows 11 Device group

By following these steps, administrators can effectively manage the Windows Copilot setting for their organization’s devices.

Validation

Now after sometime login to your Windows 365 CloudPC or Windows 11 device and the Copilot Icon will disappear from the TaskBar

Bonus (PowerShell)

If you want to create the above policy using PowerShell and MS Graph you can run the below code:

# Import necessary modules
Import-Module Microsoft.Graph.Beta.Groups
Import-Module Microsoft.Graph.Beta.DeviceManagement

# Define parameters for the new device management configuration policy
$params = @{
name = "DisableCopilot"
description = "Disable AI copilot"
platforms = "windows10"
technologies = "mdm"
roleScopeTagIds = @(
"0"
)
settings = @(
@{
"@odata.type" = "#microsoft.graph.deviceManagementConfigurationSetting"
settingInstance = @{
"@odata.type" = "#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance"
settingDefinitionId = "user_vendor_msft_policy_config_windowsai_turnoffwindowscopilot"
choiceSettingValue = @{
"@odata.type" = "#microsoft.graph.deviceManagementConfigurationChoiceSettingValue"
value = "user_vendor_msft_policy_config_windowsai_turnoffwindowscopilot_1"
children = @()
}
}
}
)
}

# Create a new device management configuration policy with the specified parameters
New-MgBetaDeviceManagementConfigurationPolicy -BodyParameter $params

Check out my other blog post that outlines how to use MS Graph and Powershell to execute the above code.

I hope you’ll find this insightful for easily disabling the Copilot within the Windows 11 physical and Windows 365 Cloud PC fleet of device. Please let me know if I’ve missed any steps or details, and I’ll be happy to update the post.

Thanks,
Aresh Sarkari

Windows 365 Cloud PC Audit Logs with Azure Log Analytics & Graph API using PowerShell

3 Nov

Are you looking to keep a vigilant eye on your Windows 365 environment? Good news! You can now send Windows 365 audit events to Azure Log Analytics, Splunk, or any other SIEM system that supports it.

Understanding the Scope of Windows 365 Audit Logs

When it comes to monitoring your Cloud PC environment, Windows 365 audit logs are an indispensable resource. These logs provide a comprehensive chronicle of significant activities that result in modifications within your Cloud PC setup (https://intune.microsoft.com/). Here’s what gets captured:

  • Creation Events: Every time a Cloud PC is provisioned, it’s meticulously logged.
  • Update Events: Any alterations or configurations changes made to an existing Cloud PC are recorded.
  • Deletion Events: If a Cloud PC is decommissioned, this action is also captured in the logs.
  • Assignment Events: The process of assigning Cloud PCs to users doesn’t go unnoticed; it’s all in the logs.
  • Remote Actions: Activities such as remote sign-outs or restarts are tracked for administrative oversight.

These audit events encompass most actions executed via the Microsoft Graph API, ensuring that administrators have visibility into the operations that affect their Cloud PC infrastructure. It’s important to note that audit logging is an always-on feature for Windows 365 customers. This means that from the moment you start using Cloud PCs, every eligible action is automatically logged without any additional configuration.

Windows 365 and Azure Log Analytics

Windows 365 has made it easier than ever to integrate with Azure Log Analytics. With a few simple PowerShell commands, you can create a diagnostic setting to send your logs directly to your Azure Log Analytics workspace.

  • Sign in to the Microsoft Intune admin center, select Reports > Diagnostic settings (under Azure monitor)> Add Diagnostic settings.
  • Under Logs, select Windows365AuditLogs.
  • Under Destination details, select the Azure Log Analytics and choose the Subscription & Workspace.
  • Select Save.

Query the Azure Log Analytics

Once your logs are safely stored in Azure Log Analytics, retrieving them is a breeze. You can use Kusto Query Language (KQL) to extract and analyze the data. Here’s a basic example of how you might query the logs:

  • Sign in to the Microsoft Intune admin center, select Reports > Log analytics (under Azure monitor)> New Query
  • Paste the below query under and click on Run
  • Optional you may save the Select Save. to use the query in the future.
Windows365AuditLogs
| where TimeGenerated > ago(7d)
| extend ParsedApplicationId = tostring(parse_json(ApplicationId)[0].Identity)
| extend ParsedUserPrincipalName = tostring(parse_json(UserPrincipalName)[0].Identity)
| extend ParsedUserId = tostring(parse_json(UserId)[0].Identity)
| project TenantId, TimeGenerated, OperationName, Result, 
          ParsedApplicationId, 
          ParsedUserPrincipalName, 
          ParsedUserId
| sort by TimeGenerated desc

Leverage Graph API to retrieve Windows 365 audit events

Connect to MS Graph API

Step 1 – Install the MS Graph Powershell Module

#Install Microsoft Graph Beta Module
PS C:WINDOWSsystem32> Install-Module Microsoft.Graph.Beta

Step 2 – Connect to scopes and specify which API you wish to authenticate to. If you are only doing read-only operations, I suggest you connect to “CloudPC.Read.All” in our case, we are creating the policy, so we need to change the scope to “CloudPC.ReadWrite.All”

#Read-only
PS C:WINDOWSsystem32> Connect-MgGraph -Scopes "CloudPC.Read.All" -NoWelcome
Welcome To Microsoft Graph!

OR

#Read-Write
PS C:WINDOWSsystem32> Connect-MgGraph -Scopes "CloudPC.ReadWrite.All" -NoWelcome
Welcome To Microsoft Graph!
Permissions for MS Graph API

Step 3 –  Check the User account by running the following beta command.

#Beta APIs
PS C:WINDOWSsystem32> Get-MgBetaUser -UserId admin@wdomain.com

Get entire list of audit events, including the audit actor

To get the entire list of audit events including the actor (person who performed the action), use the following command:

Get-MgBetaDeviceManagementVirtualEndpointAuditEvent | Select-Object -Property Actor,ActivityDateTime,ActivityType,ActivityResult -ExpandProperty Actor | Format-Table UserId, UserPrincipalName, ActivityType, ActivityDateTime, ActivityResult


Get a list of audit events

To get a list of audit events without the audit actor, use the following command:

Get-MgBetaDeviceManagementVirtualEndpointAuditEvent -All -Top 100

Integrating Windows 365 with Azure Log Analytics is a smart move for any organization looking to bolster its security and compliance posture. With the added flexibility of forwarding to multiple endpoints, you’re well-equipped to handle whatever audit challenges come your way.

I hope you will find this helpful information for enabling and quering Windows 365 Audit Logs in Azure Logs Analytics or using Graph API with PowerShell. Please let me know if I have missed any steps or details, and I will be happy to update the post.

Thanks,
Aresh Sarkari