diff --git a/AdminConsent/GA_AdminConsent_Set.ps1 b/AdminConsent/GA_AdminConsent_Set.ps1 index 2f14f57..1444c83 100644 --- a/AdminConsent/GA_AdminConsent_Set.ps1 +++ b/AdminConsent/GA_AdminConsent_Set.ps1 @@ -7,195 +7,209 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> - -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" - -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" - -$resourceAppIdURI = "https://graph.microsoft.com" - -$authority = "https://login.microsoftonline.com/$Tenant" + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" - - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId,"prompt=admin_consent").Result - - if($authResult.AccessToken){ - - # Creating header for Authorization token - - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn - } - - return $authHeader + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Failed to establish connection to Microsoft Graph" + return $false } - } - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break - + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } - } -#################################################### +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -#region Authentication + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -write-host + [Parameter(Mandatory = $false)] + [object]$Body = $null, -# Checking if authToken exists before running authentication -if($global:authToken){ + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() + try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - if($TokenExpires -le 0){ + $results = @() + $nextLink = $Uri - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host + do { + Write-Verbose "Making request to: $nextLink" - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($User -eq $null -or $User -eq ""){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host + $response = Invoke-MgGraphRequest @requestParams + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - $global:authToken = Get-AuthToken -User $User + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } + else { + Write-Error "Graph API request failed: $errorMessage" + } + throw + } } -# Authentication doesn't exist, calling Get-AuthToken function - -else { +#################################################### - if($User -eq $null -or $User -eq ""){ +#################################################### - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - } +#################################################### -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User +#region Authentication +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -203,3 +217,4 @@ $global:authToken = Get-AuthToken -User $User #################################################### Write-Host + diff --git a/AndroidEnterprise/Get-AndroidDedicatedDeviceProfiles.ps1 b/AndroidEnterprise/Get-AndroidDedicatedDeviceProfiles.ps1 index d404be5..b149698 100644 --- a/AndroidEnterprise/Get-AndroidDedicatedDeviceProfiles.ps1 +++ b/AndroidEnterprise/Get-AndroidDedicatedDeviceProfiles.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,148 +6,200 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "DeviceManagementServiceConfig.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### @@ -166,15 +218,15 @@ NAME: Get-AndroidEnrollmentProfile $graphApiVersion = "Beta" $Resource = "deviceManagement/androidDeviceOwnerEnrollmentProfiles" - + try { - - $now = (Get-Date -Format s) - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)?`$filter=tokenExpirationDateTime gt $($now)z" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).value - + + $now = (Get-Date -Format s) + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)?`$filter=tokenExpirationDateTime gt $($now)z" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + } - + catch { $ex = $_.Exception @@ -196,50 +248,10 @@ $Resource = "deviceManagement/androidDeviceOwnerEnrollmentProfiles" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -267,3 +279,4 @@ Write-Host "No Corporate-owned dedicated device Profiles found..." -ForegroundCo } Write-Host + diff --git a/AndroidEnterprise/Get-AndroidDedicatedDeviceQRCode.ps1 b/AndroidEnterprise/Get-AndroidDedicatedDeviceQRCode.ps1 index 4c74394..ff78181 100644 --- a/AndroidEnterprise/Get-AndroidDedicatedDeviceQRCode.ps1 +++ b/AndroidEnterprise/Get-AndroidDedicatedDeviceQRCode.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,148 +7,200 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "DeviceManagementServiceConfig.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } - -} #################################################### @@ -167,15 +219,15 @@ NAME: Get-AndroidEnrollmentProfile $graphApiVersion = "Beta" $Resource = "deviceManagement/androidDeviceOwnerEnrollmentProfiles" - + try { - - $now = (Get-Date -Format s) - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)?`$filter=tokenExpirationDateTime gt $($now)z" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).value - + + $now = (Get-Date -Format s) + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)?`$filter=tokenExpirationDateTime gt $($now)z" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + } - + catch { $ex = $_.Exception @@ -218,13 +270,13 @@ Param( $graphApiVersion = "Beta" try { - + $Resource = "deviceManagement/androidDeviceOwnerEnrollmentProfiles/$($Profileid)?`$select=qrCodeImage" - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get) - + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) + } - + catch { $ex = $_.Exception @@ -246,50 +298,10 @@ $graphApiVersion = "Beta" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -318,14 +330,14 @@ $profilecount = @($profiles).count $menu = @{} - for ($i=1;$i -le $COSUprofiles.count; $i++) - { Write-Host "$i. $($COSUprofiles[$i-1])" + for ($i=1;$i -le $COSUprofiles.count; $i++) + { Write-Host "$i. $($COSUprofiles[$i-1])" $menu.Add($i,($COSUprofiles[$i-1]))} Write-Host $ans = Read-Host 'Choose a profile (numerical value)' - if($ans -eq "" -or $ans -eq $null){ + if("" -eq $ans -or $null -eq $ans){ Write-Host "Corporate-owned dedicated device profile can't be null, please specify a valid Profile..." -ForegroundColor Red Write-Host @@ -375,7 +387,7 @@ $profilecount = @($profiles).count $Profileid = (Get-AndroidEnrollmentProfile).id $ProfileDisplayName = (Get-AndroidEnrollmentProfile).displayname - + Write-Host "Found a Corporate-owned dedicated devices profile '$ProfileDisplayName'..." Write-Host @@ -399,7 +411,7 @@ Write-Host "Show token? [Y]es, [N]o" $FinalConfirmation = Read-Host if ($FinalConfirmation -ne "y"){ - + Write-Host "Exiting..." Write-Host break @@ -411,12 +423,12 @@ $FinalConfirmation = Read-Host Write-Host $QR = (Get-AndroidQRCode -Profileid $ProfileID) - + $QRType = $QR.qrCodeImage.type $QRValue = $QR.qrCodeImage.value - + $imageType = $QRType.split("/")[1] - + $filename = "$TempDirPath\$ProfileDisplayName.$imageType" $bytes = [Convert]::FromBase64String($QRValue) @@ -432,11 +444,11 @@ $FinalConfirmation = Read-Host } else { - + write-host "Oops! Something went wrong!" -ForegroundColor Red - + } - + } } @@ -447,3 +459,4 @@ else { Write-Host } + diff --git a/AndroidEnterprise/Get-AndroidWorkProfileConfiguration.ps1 b/AndroidEnterprise/Get-AndroidWorkProfileConfiguration.ps1 index fbae098..f5c1bf2 100644 --- a/AndroidEnterprise/Get-AndroidWorkProfileConfiguration.ps1 +++ b/AndroidEnterprise/Get-AndroidWorkProfileConfiguration.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,153 +6,205 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "DeviceManagementServiceConfig.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-DeviceEnrollmentConfigurations(){ - +function Get-DeviceEnrollmentConfigurations { + <# .SYNOPSIS This function is used to get Deivce Enrollment Configurations from the Graph API REST interface @@ -164,21 +216,21 @@ Returns Device Enrollment Configurations configured in Intune .NOTES NAME: Get-DeviceEnrollmentConfigurations #> - + [cmdletbinding()] - + $graphApiVersion = "Beta" $Resource = "deviceManagement/deviceEnrollmentConfigurations" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -189,14 +241,14 @@ NAME: Get-DeviceEnrollmentConfigurations Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -222,48 +274,48 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + if($Group){ $GID = $Group.id - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } } - + } } @@ -289,50 +341,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -366,7 +378,7 @@ if($AndroidEnterpriseConfig){ else { - $uri = "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations/$ConfigurationId/assignments" + $uri = "beta/deviceManagement/deviceEnrollmentConfigurations/$ConfigurationId/assignments" $Assignments = (Invoke-RestMethod -Method Get -Uri $uri -Headers $authToken).value @@ -402,3 +414,4 @@ else { Write-Host } + diff --git a/AppConfigurationPolicy/AppConfigurationPolicy_Export.ps1 b/AppConfigurationPolicy/AppConfigurationPolicy_Export.ps1 index 39167ed..d202407 100644 --- a/AppConfigurationPolicy/AppConfigurationPolicy_Export.ps1 +++ b/AppConfigurationPolicy/AppConfigurationPolicy_Export.ps1 @@ -1,158 +1,210 @@ <# -  + .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - } + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null + [Parameter(Mandatory = $false)] + [object]$Body = $null, -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) -$clientId = "" -  -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" -  -$resourceAppIdURI = "https://graph.microsoft.com" -  -$authority = "https://login.microsoftonline.com/$Tenant" -  try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-ManagedAppAppConfigPolicy(){ +function Get-ManagedAppAppConfigPolicy { <# .SYNOPSIS @@ -168,14 +220,14 @@ NAME: Get-ManagedAppAppConfigPolicy $graphApiVersion = "Beta" $Resource = "deviceAppManagement/targetedManagedAppConfigurations?`$expand=apps" - + try{ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { $ex = $_.Exception @@ -195,7 +247,7 @@ $Resource = "deviceAppManagement/targetedManagedAppConfigurations?`$expand=apps" #################################################### -Function Get-ManagedDeviceAppConfigPolicy(){ +function Get-ManagedDeviceAppConfigPolicy { <# .SYNOPSIS @@ -213,12 +265,12 @@ $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileAppConfigurations" try{ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { $ex = $_.Exception @@ -238,7 +290,7 @@ $Resource = "deviceAppManagement/mobileAppConfigurations" #################################################### -Function Get-AppBundleID(){ +function Get-AppBundleID { <# .SYNOPSIS @@ -246,7 +298,7 @@ This function is used to get an app bundle ID from the Graph API REST interface .DESCRIPTION The function connects to the Graph API Interface and gets the app bundle ID for the specified app GUID .EXAMPLE -Get-AppBundleID -guid +Get-AppBundleID -guid Returns the bundle ID for the specified app GUID in Intune .NOTES NAME: Get-AppBundleID @@ -262,12 +314,12 @@ $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps?`$filter=id eq '$GUID'" try{ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value + } - + catch { $ex = $_.Exception @@ -287,7 +339,7 @@ $Resource = "deviceAppManagement/mobileApps?`$filter=id eq '$GUID'" #################################################### -Function Export-JSONData(){ +function Export-JSONData { <# .SYNOPSIS @@ -312,7 +364,7 @@ $bundleID try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON..." -f Red @@ -343,7 +395,7 @@ $bundleID $Properties = ($JSON_Convert | Get-Member | ? { $_.MemberType -eq "NoteProperty" }).Name - + $FileName_JSON = "$DisplayName" + "_" + $(get-date -f dd-MM-yyyy-H-mm-ss) + "1.json" $Object = New-Object System.Object @@ -362,10 +414,10 @@ $bundleID } write-host "Export Path:" "$ExportPath" - + $object | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath "$ExportPath\$FileName_JSON" write-host "JSON created in $ExportPath\$FileName_JSON..." -f cyan - + } } @@ -382,55 +434,15 @@ $bundleID #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion - + #################################################### $ExportPath = Read-Host -Prompt "Please specify a path to export the policy data to e.g. C:\IntuneOutput" @@ -485,7 +497,7 @@ $managedDeviceAppConfigPolicies = Get-ManagedDeviceAppConfigPolicy foreach($policy in $managedDeviceAppConfigPolicies){ write-host "(Managed Device) App Configuration Policy:"$policy.displayName -f Yellow - + #If this is an Managed Device App Config for iOS, lookup the bundleID to support importing to a different tenant If($policy.'@odata.type' -eq "#microsoft.graph.iosMobileAppConfiguration"){ @@ -494,7 +506,7 @@ $managedDeviceAppConfigPolicies = Get-ManagedDeviceAppConfigPolicy Write-Host } - + Else{ @@ -505,3 +517,4 @@ $managedDeviceAppConfigPolicies = Get-ManagedDeviceAppConfigPolicy } + diff --git a/AppConfigurationPolicy/AppConfigurationPolicy_ImportFromJSON.ps1 b/AppConfigurationPolicy/AppConfigurationPolicy_ImportFromJSON.ps1 index 7049740..d1be91f 100644 --- a/AppConfigurationPolicy/AppConfigurationPolicy_ImportFromJSON.ps1 +++ b/AppConfigurationPolicy/AppConfigurationPolicy_ImportFromJSON.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - Write-Host - Write-Host "AzureAD Powershell module not installed..." -f Red - Write-Host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - Write-Host "Script can't continue..." -f Red - Write-Host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - Write-Host $_.Exception.Message -f Red - Write-Host $_.Exception.ItemName -f Red - Write-Host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -187,7 +239,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -197,7 +249,7 @@ $JSON #################################################### -Function Test-AppBundleId(){ +function Test-AppBundleId { <# .SYNOPSIS @@ -205,7 +257,7 @@ This function is used to test whether an app bundle ID is present in the client .DESCRIPTION The function connects to the Graph API Interface and checks whether the app bundle ID has been added to the client apps .EXAMPLE -Test-AppBundleId -bundleId +Test-AppBundleId -bundleId Returns the targetedMobileApp GUID for the specified app GUID in Intune .NOTES NAME: Test-AppBundleId @@ -221,12 +273,12 @@ $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps?`$filter=(microsoft.graph.managedApp/appAvailability eq null or microsoft.graph.managedApp/appAvailability eq 'lineOfBusiness' or isAssigned eq true) and (isof('microsoft.graph.iosLobApp') or isof('microsoft.graph.iosStoreApp') or isof('microsoft.graph.iosVppApp') or isof('microsoft.graph.managedIOSStoreApp') or isof('microsoft.graph.managedIOSLobApp'))" try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - $mobileApps = Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + $mobileApps = Invoke-IntuneRestMethod -Uri $uri -Method GET + } - + catch { $ex = $_.Exception @@ -243,24 +295,24 @@ $Resource = "deviceAppManagement/mobileApps?`$filter=(microsoft.graph.managedApp } $app = $mobileApps.value | where {$_.bundleId -eq $bundleId} - + If($app){ - + return $app.id } - + Else{ return $false } - + } #################################################### -Function Test-AppPackageId(){ +function Test-AppPackageId { <# .SYNOPSIS @@ -268,7 +320,7 @@ This function is used to test whether an app package ID is present in the client .DESCRIPTION The function connects to the Graph API Interface and checks whether the app package ID has been added to the client apps .EXAMPLE -Test-AppPackageId -packageId +Test-AppPackageId -packageId Returns the targetedMobileApp GUID for the specified app GUID in Intune .NOTES NAME: Test-AppPackageId @@ -284,12 +336,12 @@ $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps?`$filter=(isof('microsoft.graph.androidForWorkApp') or microsoft.graph.androidManagedStoreApp/supportsOemConfig eq false)" try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - $mobileApps = Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + $mobileApps = Invoke-IntuneRestMethod -Uri $uri -Method GET + } - + catch { $ex = $_.Exception @@ -306,13 +358,13 @@ $Resource = "deviceAppManagement/mobileApps?`$filter=(isof('microsoft.graph.andr } $app = $mobileApps.value | where {$_.packageId -eq $packageId} - + If($app){ - + return $app.id } - + Else{ return $false @@ -323,7 +375,7 @@ $Resource = "deviceAppManagement/mobileApps?`$filter=(isof('microsoft.graph.andr #################################################### -Function Add-ManagedAppAppConfigPolicy(){ +function Add-ManagedAppAppConfigPolicy { <# .SYNOPSIS @@ -345,10 +397,10 @@ param $graphApiVersion = "Beta" $Resource = "deviceAppManagement/targetedManagedAppConfigurations" - + try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ Write-Host "No JSON specified, please specify valid JSON for the App Configuration Policy..." -f Red @@ -358,13 +410,13 @@ $Resource = "deviceAppManagement/targetedManagedAppConfigurations" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -384,7 +436,7 @@ $Resource = "deviceAppManagement/targetedManagedAppConfigurations" #################################################### -Function Add-ManagedDeviceAppConfigPolicy(){ +function Add-ManagedDeviceAppConfigPolicy { <# .SYNOPSIS @@ -406,10 +458,10 @@ param $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileAppConfigurations" - + try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ Write-Host "No JSON specified, please specify valid JSON for the App Configuration Policy..." -f Red @@ -419,13 +471,13 @@ $Resource = "deviceAppManagement/mobileAppConfigurations" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -447,50 +499,10 @@ $Resource = "deviceAppManagement/mobileAppConfigurations" #region Authentication -Write-Host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - Write-Host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - Write-Host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -529,9 +541,9 @@ If(($JSON_Convert.'@odata.type' -eq "#microsoft.graph.iosMobileAppConfiguration" If($JSON_Convert.'@odata.type' -eq "#microsoft.graph.iosMobileAppConfiguration"){ - # Check if the client app is present + # Check if the client app is present $targetedMobileApp = Test-AppBundleId -bundleId $JSON_Convert.bundleId - + If($targetedMobileApp){ Write-Host @@ -568,27 +580,27 @@ If(($JSON_Convert.'@odata.type' -eq "#microsoft.graph.iosMobileAppConfiguration" ElseIf($JSON_Convert.'@odata.type' -eq "#microsoft.graph.androidManagedStoreAppConfiguration"){ - # Check if the client app is present + # Check if the client app is present $targetedMobileApp = Test-AppPackageId -packageId $JSON_Convert.packageId - + If($targetedMobileApp){ Write-Host Write-Host "Targeted app $($JSON_Convert.packageId) has already been added from Managed Google Play" -ForegroundColor Yellow Write-Host "The App Configuration Policy will be created" -ForegroundColor Yellow Write-Host - - # Update the targetedMobileApps GUID if required + + # Update the targetedMobileApps GUID if required If(!($targetedMobileApp -eq $JSON_Convert.targetedMobileApps)){ - + $JSON_Convert.targetedMobileApps.SetValue($targetedMobileApp,0) } $JSON_Output = $JSON_Convert | ConvertTo-Json -Depth 5 $JSON_Output - Write-Host - Write-Host "Adding App Configuration Policy '$DisplayName'" -ForegroundColor Yellow + Write-Host + Write-Host "Adding App Configuration Policy '$DisplayName'" -ForegroundColor Yellow Add-ManagedDeviceAppConfigPolicy -JSON $JSON_Output } @@ -601,7 +613,7 @@ If(($JSON_Convert.'@odata.type' -eq "#microsoft.graph.iosMobileAppConfiguration" Write-Host "The App Configuration Policy can't be created" -ForegroundColor Red } - + } } @@ -614,10 +626,11 @@ Else $JSON_Output Write-Host Write-Host "Adding App Configuration Policy '$DisplayName'" -ForegroundColor Yellow - Add-ManagedAppAppConfigPolicy -JSON $JSON_Output + Add-ManagedAppAppConfigPolicy -JSON $JSON_Output } - + + diff --git a/AppProtectionPolicy/ManagedAppPolicy_Add.ps1 b/AppProtectionPolicy/ManagedAppPolicy_Add.ps1 index 660bbb5..28d0152 100644 --- a/AppProtectionPolicy/ManagedAppPolicy_Add.ps1 +++ b/AppProtectionPolicy/ManagedAppPolicy_Add.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-ManagedAppPolicy(){ +function Add-ManagedAppPolicy { <# .SYNOPSIS @@ -178,7 +230,7 @@ $Resource = "deviceAppManagement/managedAppPolicies" try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for a Managed App Policy..." -f Red @@ -188,8 +240,8 @@ $Resource = "deviceAppManagement/managedAppPolicies" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -215,7 +267,7 @@ $Resource = "deviceAppManagement/managedAppPolicies" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -262,50 +314,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -419,3 +431,4 @@ Add-ManagedAppPolicy -Json $iOS write-host "Adding Android Managed App Policy" -f Yellow Add-ManagedAppPolicy -Json $Android + diff --git a/AppProtectionPolicy/ManagedAppPolicy_Add_Assign.ps1 b/AppProtectionPolicy/ManagedAppPolicy_Add_Assign.ps1 index 69381cf..1bca280 100644 --- a/AppProtectionPolicy/ManagedAppPolicy_Add_Assign.ps1 +++ b/AppProtectionPolicy/ManagedAppPolicy_Add_Assign.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-ManagedAppPolicy(){ +function Add-ManagedAppPolicy { <# .SYNOPSIS @@ -178,7 +230,7 @@ $Resource = "deviceAppManagement/managedAppPolicies" try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for a Managed App Policy..." -f Red @@ -188,8 +240,8 @@ $Resource = "deviceAppManagement/managedAppPolicies" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -215,7 +267,7 @@ $Resource = "deviceAppManagement/managedAppPolicies" #################################################### -Function Assign-ManagedAppPolicy(){ +function Assign-ManagedAppPolicy { <# .SYNOPSIS @@ -242,7 +294,7 @@ param ) $graphApiVersion = "Beta" - + try { if(!$Id){ @@ -275,7 +327,7 @@ $JSON = @" "@ - if($OS -eq "" -or $OS -eq $null){ + if("" -eq $OS -or $null -eq $OS){ write-host "No OS parameter specified, please provide an OS. Supported value Android or iOS..." -f Red Write-Host @@ -285,20 +337,20 @@ $JSON = @" elseif($OS -eq "Android"){ - $uri = "https://graph.microsoft.com/beta/deviceAppManagement/iosManagedAppProtections('$ID')/assign" + $uri = "beta/deviceAppManagement/iosManagedAppProtections('$ID')/assign" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } elseif($OS -eq "iOS"){ - $uri = "https://graph.microsoft.com/$graphApiVersion/deviceAppManagement/iosManagedAppProtections('$ID')/assign" + $uri = "$global:GraphEndpoint/$graphApiVersion/deviceAppManagement/iosManagedAppProtections('$ID')/assign" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } - + } - + catch { $ex = $_.Exception @@ -318,7 +370,7 @@ $JSON = @" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -363,7 +415,7 @@ $JSON #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -389,37 +441,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ $GID = $Group.id @@ -427,13 +479,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } } @@ -459,50 +511,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -515,7 +527,7 @@ $AADGroup = Read-Host -Prompt "Enter the Azure AD Group name where policies will $TargetGroupId = (get-AADGroup -GroupName "$AADGroup").id - if($TargetGroupId -eq $null -or $TargetGroupId -eq ""){ + if($null -eq $TargetGroupId -or "" -eq $TargetGroupId){ Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red Write-Host @@ -653,3 +665,4 @@ $Assign_Policy = Assign-ManagedAppPolicy -Id $MAM_PolicyID -TargetGroupId $Targe Write-Host "Assigned '$AADGroup' to $($CreateResult.displayName)/$($CreateResult.id)" Write-Host + diff --git a/AppProtectionPolicy/ManagedAppPolicy_AppRegistrationSummary.ps1 b/AppProtectionPolicy/ManagedAppPolicy_AppRegistrationSummary.ps1 index 1d0e18c..b1ef8a2 100644 --- a/AppProtectionPolicy/ManagedAppPolicy_AppRegistrationSummary.ps1 +++ b/AppProtectionPolicy/ManagedAppPolicy_AppRegistrationSummary.ps1 @@ -6,153 +6,204 @@ See LICENSE in the project root for license information. #> -#################################################### -function Get-AuthToken { +function Connect-GraphAPI { +<# +.SYNOPSIS +Connects to Microsoft Graph API with appropriate scopes for Intune operations +.DESCRIPTION +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment +.NOTES +Requires Microsoft.Graph.Authentication module +#> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) - <# - .SYNOPSIS - This function is used to authenticate with the Graph API REST interface - .DESCRIPTION - The function authenticate with the Graph API Interface with the tenant name - .EXAMPLE - Get-AuthToken - Authenticates you with the Graph API interface - .NOTES - NAME: Get-AuthToken - #> - - [cmdletbinding()] - - param - ( - [Parameter(Mandatory = $true)] - $User - ) - - $userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - - $tenant = $userUpn.Host - - Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit + + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false } - - # Getting path to ActiveDirectory Assemblies - # If the module count is greater than 1 find the latest version - - if ($AadModule.count -gt 1) { - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if ($AadModule.count -gt 1) { - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true } - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + Write-Error "Failed to establish connection to Microsoft Graph" + return $false } - - [System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - - [System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - - # Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information - # on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - - $clientId = "" - - $redirectUri = "urn:ietf:wg:oauth:2.0:oob" - - $resourceAppIdURI = "https://graph.microsoft.com" - - $authority = "https://login.microsoftonline.com/$Tenant" - - try { - - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" - - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $clientId, $redirectUri, $platformParameters, $userId).Result - - # If the accesstoken is valid then create the authentication header - - if ($authResult.AccessToken) { - - # Creating header for Authorization token - - $authHeader = @{ - 'Content-Type' = 'application/json' - 'Authorization' = "Bearer " + $authResult.AccessToken - 'ExpiresOn' = $authResult.ExpiresOn + } + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false + } +} + +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, + + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', + + [Parameter(Mandatory = $false)] + [object]$Body = $null, + + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) + + try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } + + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } + + $results = @() + $nextLink = $Uri + + do { + Write-Verbose "Making request to: $nextLink" + + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } + + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" } - - return $authHeader - } - + + $response = Invoke-MgGraphRequest @requestParams + + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + $results += $response + $nextLink = $null } - + + } while ($nextLink) + + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break - + else { + Write-Error "Graph API request failed: $errorMessage" } - + throw } - +} + +#################################################### + +#################################################### + + #################################################### - - Function Get-ManagedAppPolicyRegistrationSummary() { - + + function Get-ManagedAppPolicyRegistrationSummary { + <# .SYNOPSIS This function is used to download App Protection Report for iOS and Android. @@ -164,63 +215,63 @@ function Get-AuthToken { .NOTES NAME: Get-ManagedAppPolicyRegistrationSummary #> - + [cmdletbinding()] - + param ( [ValidateSet("Android_iOS", "WIP_WE", "WIP_MDM")] $ReportType, $NextPage ) - + $graphApiVersion = "Beta" $Stoploop = $false [int]$Retrycount = "0" do{ try { - - if ($ReportType -eq "" -or $ReportType -eq $null) { + + if ("" -eq $ReportType -or $null -eq $ReportType) { $ReportType = "Android_iOS" - + } elseif ($ReportType -eq "Android_iOS") { - + $Resource = "/deviceAppManagement/managedAppStatuses('appregistrationsummary')?fetch=6000&policyMode=0&columns=DisplayName,UserEmail,ApplicationName,ApplicationInstanceId,ApplicationVersion,DeviceName,DeviceType,DeviceManufacturer,DeviceModel,AndroidPatchVersion,AzureADDeviceId,MDMDeviceID,Platform,PlatformVersion,ManagementLevel,PolicyName,LastCheckInDate" - if ($NextPage -ne "" -and $NextPage -ne $null) { + if ("" -ne $NextPage -and $null -ne $NextPage) { $Resource += "&seek=$NextPage" } - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET + } - + elseif ($ReportType -eq "WIP_WE") { - + $Resource = "deviceAppManagement/managedAppStatuses('windowsprotectionreport')" - if ($NextPage -ne "" -and $NextPage -ne $null) { + if ("" -ne $NextPage -and $null -ne $NextPage) { $Resource += "&seek=$NextPage" } - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET + } - + elseif ($ReportType -eq "WIP_MDM") { - + $Resource = "deviceAppManagement/mdmWindowsInformationProtectionPolicies" - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET + } $Stoploop = $true } - + catch { - + $ex = $_.Exception - + # Retry 4 times if 503 service time out if($ex.Response.StatusCode.value__ -eq "503") { $Retrycount = $Retrycount + 1 @@ -243,137 +294,138 @@ function Get-AuthToken { } } while ($Stoploop -eq $false) - + } - + #################################################### - - Function Test-AuthToken(){ - + + function Test-AuthToken { + # Checking if authToken exists before running authentication if ($global:authToken) { - + # Setting DateTime to Universal time to work in all timezones $DateTime = (Get-Date).ToUniversalTime() - + # If the authToken exists checking when it expires $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - + if ($TokenExpires -le 0) { - + write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow write-host - + # Defining User Principal Name if not present - - if ($User -eq $null -or $User -eq "") { - + + if ($null -eq $User -or "" -eq $User) { + $global:User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" Write-Host - + } - - $global:authToken = Get-AuthToken -User $User - + + $global:authToken = Connect-GraphAPIr $User + } } - - # Authentication doesn't exist, calling Get-AuthToken function - + + # Authentication doesn't exist, calling Connect-GraphAPInction + else { - - if ($User -eq $null -or $User -eq "") { - + + if ($null -eq $User -or "" -eq $User) { + $global:User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" Write-Host - + } - + # Getting the authorization token - $global:authToken = Get-AuthToken -User $User - + $global:authToken = Connect-GraphAPIr $User + } } - + #################################################### - + Test-AuthToken - + #################################################### - + Write-Host - + $ExportPath = Read-Host -Prompt "Please specify a path to export the policy data to e.g. C:\IntuneOutput" - + # If the directory path doesn't exist prompt user to create the directory - + if (!(Test-Path "$ExportPath")) { - + Write-Host Write-Host "Path '$ExportPath' doesn't exist, do you want to create this directory? Y or N?" -ForegroundColor Yellow - + $Confirm = read-host - + if ($Confirm -eq "y" -or $Confirm -eq "Y") { - + new-item -ItemType Directory -Path "$ExportPath" | Out-Null Write-Host - + } - + else { - + Write-Host "Creation of directory path was cancelled..." -ForegroundColor Red Write-Host break - + } - + } - + Write-Host - + #################################################### - + $AppType = Read-Host -Prompt "Please specify the type of report [Android_iOS, WIP_WE, WIP_MDM]" - + if($AppType -eq "Android_iOS" -or $AppType -eq "WIP_WE" -or $AppType -eq "WIP_MDM") { - + Write-Host write-host "Running query against Microsoft Graph to download App Protection Report for '$AppType'.." -f Yellow - + $ofs = ',' $stream = [System.IO.StreamWriter]::new("$ExportPath\AppRegistrationSummary_$AppType.csv", $false, [System.Text.Encoding]::UTF8) $ManagedAppPolicies = Get-ManagedAppPolicyRegistrationSummary -ReportType $AppType $stream.WriteLine([string]($ManagedAppPolicies.content.header | % {$_.columnName } )) - + do { Test-AuthToken - + write-host "Your data is being downloaded for '$AppType'..." $MoreItem = $ManagedAppPolicies.content.skipToken -ne "" -and $ManagedAppPolicies.content.skipToken -ne $null - + foreach ($SummaryItem in $ManagedAppPolicies.content.body) { - + $stream.WriteLine([string]($SummaryItem.values -replace ",",".")) } - + if ($MoreItem){ - + $ManagedAppPolicies = Get-ManagedAppPolicyRegistrationSummary -ReportType $AppType -NextPage ($ManagedAppPolicies.content.skipToken) } - + } while ($MoreItem) - + $stream.close() - + write-host - + } - + else { - + Write-Host "AppType isn't a valid option..." -ForegroundColor Red Write-Host - + } + diff --git a/AppProtectionPolicy/ManagedAppPolicy_Export.ps1 b/AppProtectionPolicy/ManagedAppPolicy_Export.ps1 index be2b04d..9f77358 100644 --- a/AppProtectionPolicy/ManagedAppPolicy_Export.ps1 +++ b/AppProtectionPolicy/ManagedAppPolicy_Export.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-ManagedAppPolicy(){ +function Get-ManagedAppPolicy { <# .SYNOPSIS @@ -177,25 +228,25 @@ $graphApiVersion = "Beta" $Resource = "deviceAppManagement/managedAppPolicies" try { - + if($Name){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } + } - + else { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("ManagedAppProtection") -or ($_.'@odata.type').contains("InformationProtectionPolicy") } - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("ManagedAppProtection") -or ($_.'@odata.type').contains("InformationProtectionPolicy") } + } - + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -206,14 +257,14 @@ $Resource = "deviceAppManagement/managedAppPolicies" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### -Function Get-ManagedAppProtection(){ +function Get-ManagedAppProtection { <# .SYNOPSIS @@ -239,72 +290,72 @@ param $id, [Parameter(Mandatory=$true)] [ValidateSet("Android","iOS","WIP_WE","WIP_MDM")] - $OS + $OS ) $graphApiVersion = "Beta" try { - - if($id -eq "" -or $id -eq $null){ - + + if("" -eq $id -or $null -eq $id){ + write-host "No Managed App Policy id specified, please provide a policy id..." -f Red break - + } - + else { - - if($OS -eq "" -or $OS -eq $null){ - + + if("" -eq $OS -or $null -eq $OS){ + write-host "No OS parameter specified, please provide an OS. Supported value are Android,iOS,WIP_WE,WIP_MDM..." -f Red Write-Host break - + } - + elseif($OS -eq "Android"){ - + $Resource = "deviceAppManagement/androidManagedAppProtections('$id')/?`$expand=apps" - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET + } - + elseif($OS -eq "iOS"){ - + $Resource = "deviceAppManagement/iosManagedAppProtections('$id')/?`$expand=apps" - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET + } elseif($OS -eq "WIP_WE"){ - + $Resource = "deviceAppManagement/windowsInformationProtectionPolicies('$id')?`$expand=protectedAppLockerFiles,exemptAppLockerFiles,assignments" - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET + } elseif($OS -eq "WIP_MDM"){ - + $Resource = "deviceAppManagement/mdmWindowsInformationProtectionPolicies('$id')?`$expand=protectedAppLockerFiles,exemptAppLockerFiles,assignments" - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET } - + } - + } catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -315,14 +366,14 @@ $graphApiVersion = "Beta" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } } #################################################### -Function Export-JSONData(){ +function Export-JSONData { <# .SYNOPSIS @@ -345,7 +396,7 @@ $ExportPath try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON..." -f Red @@ -382,7 +433,7 @@ $ExportPath $JSON1 | Set-Content -LiteralPath "$ExportPath\$FileName_JSON" write-host "JSON created in $ExportPath\$FileName_JSON..." -f cyan - + } } @@ -399,50 +450,10 @@ $ExportPath #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -522,3 +533,4 @@ if($ManagedAppPolicies){ } } + diff --git a/AppProtectionPolicy/ManagedAppPolicy_Get.ps1 b/AppProtectionPolicy/ManagedAppPolicy_Get.ps1 index d2df32a..bc168fd 100644 --- a/AppProtectionPolicy/ManagedAppPolicy_Get.ps1 +++ b/AppProtectionPolicy/ManagedAppPolicy_Get.ps1 @@ -7,165 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { - - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" - - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $MethodArguments = [Type[]]@("System.String", "System.String", "System.Uri", "Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior", "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier") - $NonAsync = $AuthContext.GetType().GetMethod("AcquireToken", $MethodArguments) - - if ($NonAsync -ne $null){ - - $authResult = $authContext.AcquireToken($resourceAppIdURI, $clientId, [Uri]$redirectUri, [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Auto, $userId) - + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" } - - else { - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $clientId, [Uri]$redirectUri, $platformParameters, $userId).Result - + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" } - # If the accesstoken is valid then create the authentication header - - if($authResult.AccessToken){ + $results = @() + $nextLink = $Uri - # Creating header for Authorization token + do { + Write-Verbose "Making request to: $nextLink" - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType } - return $authHeader - - } + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - else { + $response = Invoke-MgGraphRequest @requestParams - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null + } - } + } while ($nextLink) + return $results } - catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" + } + else { + Write-Error "Graph API request failed: $errorMessage" + } + throw + } +} - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-ManagedAppPolicy(){ +function Get-ManagedAppPolicy { <# .SYNOPSIS @@ -190,25 +228,25 @@ $graphApiVersion = "Beta" $Resource = "deviceAppManagement/managedAppPolicies" try { - + if($Name){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } + } - + else { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("ManagedAppProtection") -or ($_.'@odata.type').contains("InformationProtectionPolicy") } - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("ManagedAppProtection") -or ($_.'@odata.type').contains("InformationProtectionPolicy") } + } - + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -219,14 +257,14 @@ $Resource = "deviceAppManagement/managedAppPolicies" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### -Function Get-ManagedAppProtection(){ +function Get-ManagedAppProtection { <# .SYNOPSIS @@ -249,72 +287,72 @@ NAME: Get-ManagedAppProtection param ( $id, - $OS + $OS ) $graphApiVersion = "Beta" try { - - if($id -eq "" -or $id -eq $null){ - + + if("" -eq $id -or $null -eq $id){ + write-host "No Managed App Policy id specified, please provide a policy id..." -f Red break - + } - + else { - - if($OS -eq "" -or $OS -eq $null){ - + + if("" -eq $OS -or $null -eq $OS){ + write-host "No OS parameter specified, please provide an OS. Supported value are Android,iOS,WIP_WE,WIP_MDM..." -f Red Write-Host break - + } - + elseif($OS -eq "Android"){ - + $Resource = "deviceAppManagement/androidManagedAppProtections('$id')/?`$expand=deploymentSummary,apps,assignments" - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET + } - + elseif($OS -eq "iOS"){ - + $Resource = "deviceAppManagement/iosManagedAppProtections('$id')/?`$expand=deploymentSummary,apps,assignments" - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET + } elseif($OS -eq "WIP_WE"){ - + $Resource = "deviceAppManagement/windowsInformationProtectionPolicies('$id')?`$expand=protectedAppLockerFiles,exemptAppLockerFiles,assignments" - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET + } elseif($OS -eq "WIP_MDM"){ - + $Resource = "deviceAppManagement/mdmWindowsInformationProtectionPolicies('$id')?`$expand=protectedAppLockerFiles,exemptAppLockerFiles,assignments" - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET } - + } - + } catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -325,14 +363,14 @@ $graphApiVersion = "Beta" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } } #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -360,55 +398,55 @@ $graphApiVersion = "v1.0" $Group_resource = "groups" try { - + if($id){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + else { - + if(!$Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ - + $GID = $Group.id - + $Group.displayName write-host - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + } - + } - + } catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -419,7 +457,7 @@ $Group_resource = "groups" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } } @@ -428,50 +466,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -491,227 +489,228 @@ write-host "Managed App Policy:"$ManagedAppPolicy.displayName -f Yellow $ManagedAppPolicy # If Android Managed App Policy - + if($ManagedAppPolicy.'@odata.type' -eq "#microsoft.graph.androidManagedAppProtection"){ - + $AndroidManagedAppProtection = Get-ManagedAppProtection -id $ManagedAppPolicy.id -OS "Android" - + write-host "Managed App Policy - Assignments" -f Cyan - + $AndroidAssignments = ($AndroidManagedAppProtection | select assignments).assignments - + if($AndroidAssignments){ - + foreach($Group in $AndroidAssignments.target.groupId){ - + (Get-AADGroup -id $Group).displayName - + } - + Write-Host - + } - + else { - + Write-Host "No assignments set for this policy..." -ForegroundColor Red Write-Host - + } - + write-host "Managed App Policy - Mobile Apps" -f Cyan - + if($ManagedAppPolicy.deployedAppCount -ge 1){ - + ($AndroidManagedAppProtection | select apps).apps.mobileAppIdentifier - + } - + else { - + Write-Host "No Managed Apps targeted..." -ForegroundColor Red Write-Host - + } - + } # If iOS Managed App Policy - + elseif($ManagedAppPolicy.'@odata.type' -eq "#microsoft.graph.iosManagedAppProtection"){ - + $iOSManagedAppProtection = Get-ManagedAppProtection -id $ManagedAppPolicy.id -OS "iOS" - + write-host "Managed App Policy - Assignments" -f Cyan - + $iOSAssignments = ($iOSManagedAppProtection | select assignments).assignments - + if($iOSAssignments){ - + foreach($Group in $iOSAssignments.target.groupId){ - + (Get-AADGroup -id $Group).displayName - + } - + Write-Host - + } - + else { - + Write-Host "No assignments set for this policy..." -ForegroundColor Red Write-Host - + } - + write-host "Managed App Policy - Mobile Apps" -f Cyan - + if($ManagedAppPolicy.deployedAppCount -ge 1){ - + ($iOSManagedAppProtection | select apps).apps.mobileAppIdentifier - + } - + else { - + Write-Host "No Managed Apps targeted..." -ForegroundColor Red Write-Host - + } - + } # If WIP Without Enrollment Managed App Policy - + elseif($ManagedAppPolicy.'@odata.type' -eq "#microsoft.graph.windowsInformationProtectionPolicy"){ - + $Win10ManagedAppProtection = Get-ManagedAppProtection -id $ManagedAppPolicy.id -OS "WIP_WE" - + write-host "Managed App Policy - Assignments" -f Cyan - + $Win10Assignments = ($Win10ManagedAppProtection | select assignments).assignments - + if($Win10Assignments){ - + foreach($Group in $Win10Assignments.target.groupId){ - + (Get-AADGroup -id $Group).displayName - + } - + Write-Host - + } - + else { - + Write-Host "No assignments set for this policy..." -ForegroundColor Red Write-Host - + } - + write-host "Protected Apps" -f Cyan - + if($Win10ManagedAppProtection.protectedApps){ - + $Win10ManagedAppProtection.protectedApps.displayName - + Write-Host } - + else { - + Write-Host "No Protected Apps targeted..." -ForegroundColor Red Write-Host - + } - + write-host "Protected AppLocker Files" -ForegroundColor Cyan if($Win10ManagedAppProtection.protectedAppLockerFiles){ - + $Win10ManagedAppProtection.protectedAppLockerFiles.displayName Write-Host - + } - + else { - + Write-Host "No Protected Applocker Files targeted..." -ForegroundColor Red Write-Host - + } - + } # If WIP with Enrollment (MDM) Managed App Policy - + elseif($ManagedAppPolicy.'@odata.type' -eq "#microsoft.graph.mdmWindowsInformationProtectionPolicy"){ - + $Win10ManagedAppProtection = Get-ManagedAppProtection -id $ManagedAppPolicy.id -OS "WIP_MDM" - + write-host "Managed App Policy - Assignments" -f Cyan - + $Win10Assignments = ($Win10ManagedAppProtection | select assignments).assignments - + if($Win10Assignments){ - + foreach($Group in $Win10Assignments.target.groupId){ - + (Get-AADGroup -id $Group).displayName - + } - + Write-Host - + } - + else { - + Write-Host "No assignments set for this policy..." -ForegroundColor Red Write-Host - + } - + write-host "Protected Apps" -f Cyan - + if($Win10ManagedAppProtection.protectedApps){ - + $Win10ManagedAppProtection.protectedApps.displayName - + Write-Host } - + else { - + Write-Host "No Protected Apps targeted..." -ForegroundColor Red Write-Host - + } - + write-host "Protected AppLocker Files" -ForegroundColor Cyan if($Win10ManagedAppProtection.protectedAppLockerFiles){ - + $Win10ManagedAppProtection.protectedAppLockerFiles.displayName Write-Host - + } - + else { - + Write-Host "No Protected Applocker Files targeted..." -ForegroundColor Red Write-Host - + } - + } } + diff --git a/AppProtectionPolicy/ManagedAppPolicy_Import_FromJSON.ps1 b/AppProtectionPolicy/ManagedAppPolicy_Import_FromJSON.ps1 index a50b441..00f3969 100644 --- a/AppProtectionPolicy/ManagedAppPolicy_Import_FromJSON.ps1 +++ b/AppProtectionPolicy/ManagedAppPolicy_Import_FromJSON.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -187,7 +238,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -197,7 +248,7 @@ $JSON #################################################### -Function Add-ManagedAppPolicy(){ +function Add-ManagedAppPolicy { <# .SYNOPSIS @@ -223,7 +274,7 @@ $Resource = "deviceAppManagement/managedAppPolicies" try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for a Managed App Policy..." -f Red @@ -233,8 +284,8 @@ $Resource = "deviceAppManagement/managedAppPolicies" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -262,50 +313,10 @@ $Resource = "deviceAppManagement/managedAppPolicies" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -340,7 +351,7 @@ $JSON_Convert | Add-Member -MemberType NoteProperty -Name 'apps' -Value @($JSON_ $DisplayName = $JSON_Convert.displayName $JSON_Output = $JSON_Convert | ConvertTo-Json -Depth 5 - + write-host write-host "App Protection Policy '$DisplayName' Found..." -ForegroundColor Cyan write-host @@ -348,4 +359,5 @@ $JSON_Output Write-Host Write-Host "Adding App Protection Policy '$DisplayName'" -ForegroundColor Yellow Add-ManagedAppPolicy -JSON $JSON_Output - + + diff --git a/AppProtectionPolicy/ManagedAppPolicy_MobileAppIdentifier_Get.ps1 b/AppProtectionPolicy/ManagedAppPolicy_MobileAppIdentifier_Get.ps1 index 32dc1e1..a45064e 100644 --- a/AppProtectionPolicy/ManagedAppPolicy_MobileAppIdentifier_Get.ps1 +++ b/AppProtectionPolicy/ManagedAppPolicy_MobileAppIdentifier_Get.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } -$tenant = $userUpn.Host + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } -Write-Host "Checking for AzureAD module..." + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } - -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null +} + +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$clientId = "" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } -$resourceAppIdURI = "https://graph.microsoft.com" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } -$authority = "https://login.microsoftonline.com/$Tenant" + $results = @() + $nextLink = $Uri - try { - - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" - - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result - - # If the accesstoken is valid then create the authentication header - - if($authResult.AccessToken){ - - # Creating header for Authorization token - - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + do { + Write-Verbose "Making request to: $nextLink" + + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } + + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } + + $response = Invoke-MgGraphRequest @requestParams + + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' } - - return $authHeader - + else { + $results += $response + $nextLink = $null + } + + } while ($nextLink) + + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-IntuneMAMApplication(){ +function Get-IntuneMAMApplication { <# .SYNOPSIS @@ -194,25 +245,25 @@ $Resource = "deviceAppManagement/mobileApps" Write-Host } - + elseif($Android){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | ? { ($_.'@odata.type').Contains("managedAndroidStoreApp") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | ? { ($_.'@odata.type').Contains("managedAndroidStoreApp") } } elseif($iOS){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | ? { ($_.'@odata.type').Contains("managedIOSStoreApp") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | ? { ($_.'@odata.type').Contains("managedIOSStoreApp") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | ? { ($_.'@odata.type').Contains("managed") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | ? { ($_.'@odata.type').Contains("managed") } } @@ -239,50 +290,10 @@ $Resource = "deviceAppManagement/mobileApps" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -320,3 +331,4 @@ Write-Host } Write-Host + diff --git a/AppProtectionPolicy/ManagedAppPolicy_Remove.ps1 b/AppProtectionPolicy/ManagedAppPolicy_Remove.ps1 index 0a47453..560d9ec 100644 --- a/AppProtectionPolicy/ManagedAppPolicy_Remove.ps1 +++ b/AppProtectionPolicy/ManagedAppPolicy_Remove.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "DeviceManagementApps.ReadWrite.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-ManagedAppPolicy(){ +function Get-ManagedAppPolicy { <# .SYNOPSIS @@ -180,15 +232,15 @@ $Resource = "deviceAppManagement/managedAppPolicies" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -213,7 +265,7 @@ $Resource = "deviceAppManagement/managedAppPolicies" #################################################### -Function Remove-ManagedAppPolicy(){ +function Remove-ManagedAppPolicy { <# .SYNOPSIS @@ -239,7 +291,7 @@ $Resource = "deviceAppManagement/managedAppPolicies" try { - if($id -eq "" -or $id -eq $null){ + if("" -eq $id -or $null -eq $id){ write-host "No id specified for managed app policy, can't remove managed app policy..." -f Red write-host "Please specify id for managed app policy..." -f Red @@ -249,7 +301,7 @@ $Resource = "deviceAppManagement/managedAppPolicies" else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)/$id" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)/$id" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Delete } @@ -277,50 +329,10 @@ $Resource = "deviceAppManagement/managedAppPolicies" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -355,3 +367,4 @@ $MAM = Get-ManagedAppPolicy -Name "Graph" Write-Host } + diff --git a/AppProtectionPolicy/ManagedAppPolicy_WIP_Add.ps1 b/AppProtectionPolicy/ManagedAppPolicy_WIP_Add.ps1 index 5774f31..068cf81 100644 --- a/AppProtectionPolicy/ManagedAppPolicy_WIP_Add.ps1 +++ b/AppProtectionPolicy/ManagedAppPolicy_WIP_Add.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,148 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User - -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null -  -# Client ID used for Intune scopes - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + $results = @() + $nextLink = $Uri - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } -} -  #################################################### -Function Add-WindowsInformationProtectionPolicy(){ +function Add-WindowsInformationProtectionPolicy { <# .SYNOPSIS @@ -171,10 +227,10 @@ param $graphApiVersion = "Beta" $Resource = "deviceAppManagement/windowsInformationProtectionPolicies" - + try { - - if($JSON -eq "" -or $JSON -eq $null){ + + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the iOS Policy..." -f Red @@ -184,13 +240,13 @@ $Resource = "deviceAppManagement/windowsInformationProtectionPolicies" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -210,7 +266,7 @@ $Resource = "deviceAppManagement/windowsInformationProtectionPolicies" #################################################### -Function Add-MDMWindowsInformationProtectionPolicy(){ +function Add-MDMWindowsInformationProtectionPolicy { <# .SYNOPSIS @@ -233,10 +289,10 @@ param $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mdmWindowsInformationProtectionPolicies" - + try { - - if($JSON -eq "" -or $JSON -eq $null){ + + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the iOS Policy..." -f Red @@ -246,13 +302,13 @@ $Resource = "deviceAppManagement/mdmWindowsInformationProtectionPolicies" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -272,7 +328,7 @@ $Resource = "deviceAppManagement/mdmWindowsInformationProtectionPolicies" #################################################### -Function Get-EnterpriseDomain(){ +function Get-EnterpriseDomain { <# .SYNOPSIS @@ -288,9 +344,9 @@ NAME: Get-EnterpriseDomain try { - $uri = "https://graph.microsoft.com/v1.0/domains" + $uri = "v1.0/domains" - $domains = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $domains = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value $EnterpriseDomain = ($domains | ? { $_.isInitial -eq $true } | select id).id @@ -318,7 +374,7 @@ NAME: Get-EnterpriseDomain #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -355,7 +411,7 @@ $JSON if (!$validJson){ Write-Host "Provided JSON isn't in valid JSON format" -f Red - + break } @@ -366,50 +422,10 @@ $JSON #region Authentication -Write-Host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - Write-Host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - Write-Host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -904,3 +920,4 @@ Write-Host "Adding Windows Information Protection MDM Policy from JSON..." -Fore Add-MDMWindowsInformationProtectionPolicy -JSON $ManagedAppPolicy_WIP_MDM Write-Host + diff --git a/AppProtectionPolicy/ManagedAppPolicy_WIP_Add_Assign.ps1 b/AppProtectionPolicy/ManagedAppPolicy_WIP_Add_Assign.ps1 index f81364f..ded8817 100644 --- a/AppProtectionPolicy/ManagedAppPolicy_WIP_Add_Assign.ps1 +++ b/AppProtectionPolicy/ManagedAppPolicy_WIP_Add_Assign.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,148 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User - -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null -  -# Client ID used for Intune scopes - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + $results = @() + $nextLink = $Uri - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } -} -  #################################################### -Function Add-WindowsInformationProtectionPolicy(){ +function Add-WindowsInformationProtectionPolicy { <# .SYNOPSIS @@ -171,10 +227,10 @@ param $graphApiVersion = "Beta" $Resource = "deviceAppManagement/windowsInformationProtectionPolicies" - + try { - - if($JSON -eq "" -or $JSON -eq $null){ + + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the iOS Policy..." -f Red @@ -184,13 +240,13 @@ $Resource = "deviceAppManagement/windowsInformationProtectionPolicies" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -210,7 +266,7 @@ $Resource = "deviceAppManagement/windowsInformationProtectionPolicies" #################################################### -Function Add-MDMWindowsInformationProtectionPolicy(){ +function Add-MDMWindowsInformationProtectionPolicy { <# .SYNOPSIS @@ -233,10 +289,10 @@ param $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mdmWindowsInformationProtectionPolicies" - + try { - - if($JSON -eq "" -or $JSON -eq $null){ + + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the iOS Policy..." -f Red @@ -246,13 +302,13 @@ $Resource = "deviceAppManagement/mdmWindowsInformationProtectionPolicies" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -272,7 +328,7 @@ $Resource = "deviceAppManagement/mdmWindowsInformationProtectionPolicies" #################################################### -Function Assign-WindowsInformationProtectionPolicy(){ +function Assign-WindowsInformationProtectionPolicy { <# .SYNOPSIS @@ -295,7 +351,7 @@ param ) $graphApiVersion = "Beta" - + try { if(!$Id){ @@ -328,11 +384,11 @@ $JSON = @" "@ - $uri = "https://graph.microsoft.com/beta/deviceAppManagement/windowsInformationProtectionPolicies('$ID')/assign" + $uri = "beta/deviceAppManagement/windowsInformationProtectionPolicies('$ID')/assign" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken - + } - + catch { $ex = $_.Exception @@ -352,7 +408,7 @@ $JSON = @" #################################################### -Function Assign-MDMWindowsInformationProtectionPolicy(){ +function Assign-MDMWindowsInformationProtectionPolicy { <# .SYNOPSIS @@ -375,7 +431,7 @@ param ) $graphApiVersion = "Beta" - + try { if(!$Id){ @@ -408,11 +464,11 @@ $JSON = @" "@ - $uri = "https://graph.microsoft.com/beta/deviceAppManagement/mdmWindowsInformationProtectionPolicies('$ID')/assign" + $uri = "beta/deviceAppManagement/mdmWindowsInformationProtectionPolicies('$ID')/assign" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken - + } - + catch { $ex = $_.Exception @@ -432,7 +488,7 @@ $JSON = @" #################################################### -Function Get-EnterpriseDomain(){ +function Get-EnterpriseDomain { <# .SYNOPSIS @@ -448,9 +504,9 @@ NAME: Get-EnterpriseDomain try { - $uri = "https://graph.microsoft.com/v1.0/domains" + $uri = "v1.0/domains" - $domains = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $domains = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value $EnterpriseDomain = ($domains | ? { $_.isInitial -eq $true } | select id).id @@ -478,7 +534,7 @@ NAME: Get-EnterpriseDomain #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -504,37 +560,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + if($Group){ $GID = $Group.id @@ -542,13 +598,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } } - + } } @@ -572,7 +628,7 @@ $Group_resource = "groups" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -609,7 +665,7 @@ $JSON if (!$validJson){ Write-Host "Provided JSON isn't in valid JSON format" -f Red - + break } @@ -620,50 +676,10 @@ $JSON #region Authentication -Write-Host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - Write-Host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - Write-Host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -676,7 +692,7 @@ $AADGroup = Read-Host -Prompt "Enter the Azure AD Group name where policies will $TargetGroupId = (get-AADGroup -GroupName "$AADGroup").id - if($TargetGroupId -eq $null -or $TargetGroupId -eq ""){ + if($null -eq $TargetGroupId -or "" -eq $TargetGroupId){ Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red Write-Host @@ -1190,3 +1206,4 @@ $Assign_Policy = Assign-MDMWindowsInformationProtectionPolicy -Id $WIP_PolicyID Write-Host "Assigned '$AADGroup' to $($CreateResult.displayName)/$($CreateResult.id)" Write-Host + diff --git a/AppProtectionPolicy/ManagedAppPolicy_Wipe.ps1 b/AppProtectionPolicy/ManagedAppPolicy_Wipe.ps1 index 041b431..fb2619c 100644 --- a/AppProtectionPolicy/ManagedAppPolicy_Wipe.ps1 +++ b/AppProtectionPolicy/ManagedAppPolicy_Wipe.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-AADUser(){ +function Get-AADUser { <# .SYNOPSIS @@ -180,36 +231,36 @@ param # Defining Variables $graphApiVersion = "v1.0" $User_resource = "users" - + try { - - if($userPrincipalName -eq "" -or $userPrincipalName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + if("" -eq $userPrincipalName -or $null -eq $userPrincipalName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - - if($Property -eq "" -or $Property -eq $null){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName" + if("" -eq $Property -or $null -eq $Property){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName" Write-Verbose $uri - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } catch { @@ -231,7 +282,7 @@ $User_resource = "users" #################################################### -Function Get-AADUserManagedAppRegistrations(){ +function Get-AADUserManagedAppRegistrations { <# .SYNOPSIS @@ -258,9 +309,9 @@ param # Defining Variables $graphApiVersion = "beta" $User_resource = "users/$id/managedAppRegistrations" - + try { - + if(!$id){ Write-Host "No AAD User ID was passed to the function, specify a valid AAD User ID" -ForegroundColor Red @@ -271,12 +322,12 @@ $User_resource = "users/$id/managedAppRegistrations" else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$User_resource" + $uri = "$global:GraphEndpoint/$graphApiVersion/$User_resource" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - + } catch { @@ -300,50 +351,10 @@ $User_resource = "users/$id/managedAppRegistrations" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -376,8 +387,8 @@ $ManagedAppReg = Get-AADUserManagedAppRegistrations -id $UserID if($DeviceTag.count -eq 1){ $DeviceName = $ManagedAppReg.deviceName - - $uri = "https://graph.microsoft.com/beta/users('$UserID')/wipeManagedAppRegistrationByDeviceTag" + + $uri = "beta/users('$UserID')/wipeManagedAppRegistrationByDeviceTag" $JSON = @" @@ -386,13 +397,13 @@ $JSON = @" } "@ - + write-host "Are you sure you want to wipe application data on device $DeviceName`? Y or N?" -f Yellow $Confirm = read-host if($Confirm -eq "y" -or $Confirm -eq "Y"){ - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -413,13 +424,13 @@ $JSON = @" Write-Host $MAM_Devices = $ManagedAppReg.deviceName | sort -Unique - + # Building menu from Array to show more than one device $menu = @{} - for ($i=1;$i -le $MAM_Devices.count; $i++) - { Write-Host "$i. $($MAM_Devices[$i-1])" + for ($i=1;$i -le $MAM_Devices.count; $i++) + { Write-Host "$i. $($MAM_Devices[$i-1])" $menu.Add($i,($MAM_Devices[$i-1]))} Write-Host @@ -433,7 +444,7 @@ $JSON = @" $SelectedDeviceTag = $ManagedAppReg | ? { $_.deviceName -eq "$Selection" } | sort -Unique | select -ExpandProperty deviceTag - $uri = "https://graph.microsoft.com/beta/users('$UserID')/wipeManagedAppRegistrationByDeviceTag" + $uri = "beta/users('$UserID')/wipeManagedAppRegistrationByDeviceTag" $JSON = @" @@ -448,7 +459,7 @@ $JSON = @" if($Confirm -eq "y" -or $Confirm -eq "Y"){ - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -473,3 +484,4 @@ $JSON = @" } } + diff --git a/AppleEnrollment/APNS_Get.ps1 b/AppleEnrollment/APNS_Get.ps1 index e1a8aa2..f88ddcc 100644 --- a/AppleEnrollment/APNS_Get.ps1 +++ b/AppleEnrollment/APNS_Get.ps1 @@ -1,158 +1,210 @@ - + <# -  + .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - } + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null + [Parameter(Mandatory = $false)] + [object]$Body = $null, -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) -$clientId = "" -  -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" -  -$resourceAppIdURI = "https://graph.microsoft.com" -  -$authority = "https://login.microsoftonline.com/$Tenant" -  try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } - -} #################################################### -Function Get-ApplePushNotificationCertificate(){ +function Get-ApplePushNotificationCertificate { <# .SYNOPSIS @@ -174,8 +226,8 @@ $Resource = "devicemanagement/applePushNotificationCertificate" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET) } @@ -184,9 +236,9 @@ $Resource = "devicemanagement/applePushNotificationCertificate" $ex = $_.Exception if(($ex.message).contains("404")){ - + Write-Host "Resource Not Configured" -ForegroundColor Red - + } else { @@ -210,50 +262,10 @@ $Resource = "devicemanagement/applePushNotificationCertificate" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -267,3 +279,4 @@ $APNS $DateTime = ([datetimeoffset]::Parse($APNS.expirationDateTime)).DateTime.DateTime Write-Host "Expiration Date Time:" $DateTime + diff --git a/AppleEnrollment/AppleDEPProfile_Assign.ps1 b/AppleEnrollment/AppleDEPProfile_Assign.ps1 index d409134..2c8be53 100644 --- a/AppleEnrollment/AppleDEPProfile_Assign.ps1 +++ b/AppleEnrollment/AppleDEPProfile_Assign.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -209,38 +261,38 @@ Gets all DEP Onboarding Settings for each DEP token present in the tenant .NOTES NAME: Get-DEPOnboardingSettings #> - + [cmdletbinding()] - + Param( [parameter(Mandatory=$false)] [string]$tokenid ) - + $graphApiVersion = "beta" - + try { - + if ($tokenid){ - + $Resource = "deviceManagement/depOnboardingSettings/$tokenid/" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get) - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) + } - + else { - + $Resource = "deviceManagement/depOnboardingSettings/" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + } - + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -251,14 +303,14 @@ Param( Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - - } + + } #################################################### -Function Get-DEPProfiles(){ +function Get-DEPProfiles { <# .SYNOPSIS @@ -285,7 +337,7 @@ $Resource = "deviceManagement/depOnboardingSettings/$id/enrollmentProfiles" try { - $SyncURI = "https://graph.microsoft.com/$graphApiVersion/$($resource)" + $SyncURI = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $SyncURI -Headers $authToken -Method GET } @@ -310,7 +362,7 @@ $Resource = "deviceManagement/depOnboardingSettings/$id/enrollmentProfiles" #################################################### -Function Assign-ProfileToDevice(){ +function Assign-ProfileToDevice { <# .SYNOPSIS @@ -347,8 +399,8 @@ $Resource = "deviceManagement/depOnboardingSettings/$id/enrollmentProfiles('$Pro Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON Write-Host "Success: " -f Green -NoNewline Write-Host "Device assigned!" @@ -378,50 +430,10 @@ $Resource = "deviceManagement/depOnboardingSettings/$id/enrollmentProfiles('$Pro #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token - $global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -447,8 +459,8 @@ Write-Host $menu = @{} - for ($i=1;$i -le $DEP_Tokens.count; $i++) - { Write-Host "$i. $($DEP_Tokens[$i-1])" + for ($i=1;$i -le $DEP_Tokens.count; $i++) + { Write-Host "$i. $($DEP_Tokens[$i-1])" $menu.Add($i,($DEP_Tokens[$i-1]))} Write-Host @@ -470,20 +482,20 @@ Write-Host elseif ($tokencount -eq 1) { $id = (Get-DEPOnboardingSettings).id - + } } else { - + Write-Warning "No DEP tokens found!" Write-Host break } -#endregion +#endregion #################################################### @@ -493,18 +505,18 @@ $DeviceSerialNumber = Read-Host "Please enter device serial number" $DeviceSerialNumber = $DeviceSerialNumber.replace(" ","") if(!($DeviceSerialNumber)){ - + Write-Host "Error: No serial number entered!" -ForegroundColor Red Write-Host break - + } $graphApiVersion = "beta" $Resource = "deviceManagement/depOnboardingSettings/$($id)/importedAppleDeviceIdentities?`$filter=discoverySource eq 'deviceEnrollmentProgram' and contains(serialNumber,'$DeviceSerialNumber')" -$uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" -$SearchResult = (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).value +$uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" +$SearchResult = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value if (!($SearchResult)){ @@ -519,7 +531,7 @@ if (!($SearchResult)){ $Profiles = (Get-DEPProfiles -id $id).value if($Profiles){ - + Write-Host Write-Host "Listing DEP Profiles..." -ForegroundColor Yellow Write-Host @@ -528,8 +540,8 @@ $enrollmentProfiles = $Profiles.displayname | Sort-Object -Unique $menu = @{} -for ($i=1;$i -le $enrollmentProfiles.count; $i++) -{ Write-Host "$i. $($enrollmentProfiles[$i-1])" +for ($i=1;$i -le $enrollmentProfiles.count; $i++) +{ Write-Host "$i. $($enrollmentProfiles[$i-1])" $menu.Add($i,($enrollmentProfiles[$i-1]))} Write-Host @@ -543,7 +555,7 @@ $ans = Read-Host 'Select the profile you wish to assign (numerical value)' } if ($selection){ - + $SelectedProfile = $Profiles | Where-Object { $_.DisplayName -eq "$Selection" } $SelectedProfileId = $SelectedProfile | Select-Object -ExpandProperty id $ProfileID = $SelectedProfileId @@ -562,7 +574,7 @@ $ans = Read-Host 'Select the profile you wish to assign (numerical value)' } else { - + Write-Host Write-Warning "No DEP profiles found!" break @@ -572,3 +584,4 @@ else { #################################################### Assign-ProfileToDevice -id $id -DeviceSerialNumber $DeviceSerialNumber -ProfileId $ProfileID + diff --git a/AppleEnrollment/AppleDEP_Sync.ps1 b/AppleEnrollment/AppleDEP_Sync.ps1 index af004d5..054bb59 100644 --- a/AppleEnrollment/AppleDEP_Sync.ps1 +++ b/AppleEnrollment/AppleDEP_Sync.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Sync-AppleDEP(){ +function Sync-AppleDEP { <# .SYNOPSIS @@ -177,11 +229,11 @@ $Resource = "deviceManagement/depOnboardingSettings/$id/syncWithAppleDeviceEnrol try { - $SyncURI = "https://graph.microsoft.com/$graphApiVersion/$($resource)" + $SyncURI = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $SyncURI -Headers $authToken -Method Post } - + catch { $ex = $_.Exception @@ -227,23 +279,23 @@ $graphApiVersion = "beta" try { if ($tokenid){ - + $Resource = "deviceManagement/depOnboardingSettings/$tokenid/" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get) - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) + } else { - + $Resource = "deviceManagement/depOnboardingSettings/" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + } - + } - + catch { $ex = $_.Exception @@ -259,56 +311,16 @@ $graphApiVersion = "beta" } -} +} #################################################### #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -330,8 +342,8 @@ Write-Host $menu = @{} - for ($i=1;$i -le $DEP_Tokens.count; $i++) - { Write-Host "$i. $($DEP_Tokens[$i-1])" + for ($i=1;$i -le $DEP_Tokens.count; $i++) + { Write-Host "$i. $($DEP_Tokens[$i-1])" $menu.Add($i,($DEP_Tokens[$i-1]))} Write-Host @@ -358,7 +370,7 @@ Write-Host } else { - + Write-Host Write-Warning "No DEP tokens found!" break @@ -383,10 +395,10 @@ Write-Host Write-Warning "Syncing in progress. You can retry sync in $RemainingTimeToSync minutes" Write-Host - } - + } + else { - + Write-Host "Syncing '$TokenDisplayName' DEP token with Apple DEP service..." Sync-AppleDEP $id @@ -401,3 +413,4 @@ else { break } + diff --git a/ApplicationSync/AppleVPP_Sync.ps1 b/ApplicationSync/AppleVPP_Sync.ps1 index 0009ef4..160d4ab 100644 --- a/ApplicationSync/AppleVPP_Sync.ps1 +++ b/ApplicationSync/AppleVPP_Sync.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Sync-AppleVPP(){ +function Sync-AppleVPP { <# .SYNOPSIS @@ -179,13 +231,13 @@ $Resource = "deviceAppManagement/vppTokens/$id/syncLicenses" try { - $SyncURI = "https://graph.microsoft.com/$graphApiVersion/$($resource)" + $SyncURI = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Write-Host "Syncing $TokenDisplayName with Apple VPP service..." Invoke-RestMethod -Uri $SyncURI -Headers $authToken -Method Post } - + catch { $ex = $_.Exception @@ -227,14 +279,14 @@ Param( $graphApiVersion = "beta" $Resource = "deviceAppManagement/vppTokens" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + } - + catch { $ex = $_.Exception @@ -250,56 +302,16 @@ $Resource = "deviceAppManagement/vppTokens" } -} +} #################################################### #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -323,8 +335,8 @@ Write-Host $menu = @{} - for ($i=1;$i -le $VPP_Tokens.count; $i++) - { Write-Host "$i. $($VPP_Tokens[$i-1])" + for ($i=1;$i -le $VPP_Tokens.count; $i++) + { Write-Host "$i. $($VPP_Tokens[$i-1])" $menu.Add($i,($VPP_Tokens[$i-1]))} Write-Host @@ -366,7 +378,7 @@ $SyncID = $SelectedTokenId $VPPToken = Get-VPPToken | Where-Object { $_.id -eq "$SyncID"} if ($VPPToken.lastSyncStatus -eq "Completed") { - + $VPPSync = Sync-AppleVPP -id $SyncID Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "$TokenDisplayName sync initiated." @@ -374,10 +386,11 @@ if ($VPPToken.lastSyncStatus -eq "Completed") { } else { - + $LastSyncStatus = $VPPToken.lastSyncStatus Write-Warning "'$TokenDisplayName' sync status '$LastSyncStatus'..." } Write-Host + diff --git a/ApplicationSync/ManagedGooglePlay_Sync.ps1 b/ApplicationSync/ManagedGooglePlay_Sync.ps1 index 66b3094..a6b346c 100644 --- a/ApplicationSync/ManagedGooglePlay_Sync.ps1 +++ b/ApplicationSync/ManagedGooglePlay_Sync.ps1 @@ -1,153 +1,205 @@ -<# -  +<# + .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - } + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null + [Parameter(Mandatory = $false)] + [object]$Body = $null, -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) -$clientId = "" -  -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" -  -$resourceAppIdURI = "https://graph.microsoft.com" -  -$authority = "https://login.microsoftonline.com/$Tenant" -  try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### @@ -157,23 +209,23 @@ function Get-AndroidManagedStoreAccount { .SYNOPSIS This function is used to query the Managed Google Play configuration via the Graph API REST interface .DESCRIPTION -The function connects to the Graph API Interface and returns the Managed Google Play configuration +The function connects to the Graph API Interface and returns the Managed Google Play configuration .EXAMPLE -Get-AndroidManagedStoreAccount -Returns the Managed Google Play configuration from Intune +Get-AndroidManagedStoreAccount +Returns the Managed Google Play configuration from Intune .NOTES NAME: Get-AndroidManagedStoreAccount #> - + $graphApiVersion = "beta" $Resource = "deviceManagement/androidManagedStoreAccountEnterpriseSettings" - + try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" - Invoke-RestMethod -Method Get -Uri $uri -Headers $authToken - + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" + Invoke-RestMethod -Method Get -Uri $uri -Headers $authToken + } catch { @@ -213,15 +265,15 @@ Initiates a Managed Google Play Sync in Intune NAME: Sync-AndroidManagedStoreAccount #> - + $graphApiVersion = "beta" $Resource = "deviceManagement/androidManagedStoreAccountEnterpriseSettings/syncApps" - + try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" - Invoke-RestMethod -Method Post -Uri $uri -Headers $authToken - + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" + Invoke-RestMethod -Method Post -Uri $uri -Headers $authToken + } catch { @@ -248,50 +300,10 @@ $Resource = "deviceManagement/androidManagedStoreAccountEnterpriseSettings/syncA #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -301,25 +313,25 @@ $global:authToken = Get-AuthToken -User $User if((Get-AndroidManagedStoreAccount).bindStatus -ne "notBound"){ Write-Host "Found Managed Google Play Configuration. Performing Sync..." -ForegroundColor Yellow - + $ManagedPlaySync = Sync-AndroidManagedStoreAccount - - if($ManagedPlaySync -ne $null){ + + if($null -ne $ManagedPlaySync){ Write-Host "Starting sync with managed Google Play, Sync will take some time" -ForegroundColor Green - + } - + else { - + $ManagedPlaySync Write-Host "Managed Google Play sync was not successful" -ForegroundColor Red break - + } } - + else { Write-Host "No Managed Google Play configuration found for this tenant" -ForegroundColor Cyan @@ -327,3 +339,4 @@ else { } Write-Host + diff --git a/Applications/Application_Android_Add_No_Icon.ps1 b/Applications/Application_Android_Add_No_Icon.ps1 index 547c2af..b7c18f0 100644 --- a/Applications/Application_Android_Add_No_Icon.ps1 +++ b/Applications/Application_Android_Add_No_Icon.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -197,7 +249,7 @@ $JSON #################################################### -Function Add-AndroidApplication(){ +function Add-AndroidApplication { <# .SYNOPSIS @@ -263,7 +315,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -272,7 +324,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -300,50 +352,10 @@ $App_resource = "deviceAppManagement/mobileApps" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -434,3 +446,4 @@ $Create_Excel = Add-AndroidApplication -JSON $Excel Write-Host "Application created as $($Create_Excel.displayName)/$($create_Excel.id)" Write-Host + diff --git a/Applications/Application_Android_Add_With_Icon.ps1 b/Applications/Application_Android_Add_With_Icon.ps1 index 76946bd..8344fdb 100644 --- a/Applications/Application_Android_Add_With_Icon.ps1 +++ b/Applications/Application_Android_Add_With_Icon.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -197,7 +249,7 @@ $JSON #################################################### -Function Add-AndroidApplication(){ +function Add-AndroidApplication { <# .SYNOPSIS @@ -263,7 +315,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -272,7 +324,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -300,50 +352,10 @@ $App_resource = "deviceAppManagement/mobileApps" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -481,3 +493,4 @@ $Create_Excel = Add-AndroidApplication -JSON $Excel -IconURL "$iconUrl_Excel" Write-Host "Application created as $($Create_Excel.displayName)/$($create_Excel.id)" Write-Host + diff --git a/Applications/Application_Category_Add.ps1 b/Applications/Application_Category_Add.ps1 index ce9d74a..6fa1634 100644 --- a/Applications/Application_Category_Add.ps1 +++ b/Applications/Application_Category_Add.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -197,7 +249,7 @@ $JSON #################################################### -Function Add-ApplicationCategory(){ +function Add-ApplicationCategory { <# .SYNOPSIS @@ -239,8 +291,8 @@ $JSON = @" "@ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -265,50 +317,10 @@ $JSON = @" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -319,3 +331,4 @@ $Category = Add-ApplicationCategory -AppCategoryName "LOB Apps 2" Write-Host "Application category added" $Category.displayname "with ID" $Category.id -ForegroundColor Green Write-Host + diff --git a/Applications/Application_Category_Get.ps1 b/Applications/Application_Category_Get.ps1 index becfa7c..ec0d23f 100644 --- a/Applications/Application_Category_Get.ps1 +++ b/Applications/Application_Category_Get.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-ApplicationCategory(){ +function Get-ApplicationCategory { <# .SYNOPSIS @@ -180,15 +231,15 @@ $Resource = "deviceAppManagement/mobileAppCategories" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -216,50 +267,10 @@ $Resource = "deviceAppManagement/mobileAppCategories" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -267,3 +278,4 @@ $global:authToken = Get-AuthToken -User $User #################################################### Get-ApplicationCategory + diff --git a/Applications/Application_Category_Remove.ps1 b/Applications/Application_Category_Remove.ps1 index f67b689..c8163ca 100644 --- a/Applications/Application_Category_Remove.ps1 +++ b/Applications/Application_Category_Remove.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "DeviceManagementApps.ReadWrite.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-ApplicationCategory(){ +function Get-ApplicationCategory { <# .SYNOPSIS @@ -180,15 +232,15 @@ $Resource = "deviceAppManagement/mobileAppCategories" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -214,7 +266,7 @@ $Resource = "deviceAppManagement/mobileAppCategories" #################################################### -Function Remove-ApplicationCategory(){ +function Remove-ApplicationCategory { <# .SYNOPSIS @@ -240,7 +292,7 @@ $Resource = "deviceAppManagement/mobileAppCategories" try { - if($id -eq "" -or $id -eq $null){ + if("" -eq $id -or $null -eq $id){ write-host "No id specified for application category, can't remove application category..." -f Red write-host "Please specify id for application category..." -f Red @@ -250,7 +302,7 @@ $Resource = "deviceAppManagement/mobileAppCategories" else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)/$id" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)/$id" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Delete } @@ -278,50 +330,10 @@ $Resource = "deviceAppManagement/mobileAppCategories" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -354,3 +366,4 @@ $App = Get-ApplicationCategory -Name "LOB Apps 2" | Where-Object { $_.lastModifi Write-Host } + diff --git a/Applications/Application_Get_Assign.ps1 b/Applications/Application_Get_Assign.ps1 index fd2dbf4..5d141e3 100644 --- a/Applications/Application_Get_Assign.ps1 +++ b/Applications/Application_Get_Assign.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-IntuneApplication(){ +function Get-IntuneApplication { <# .SYNOPSIS @@ -170,14 +222,14 @@ NAME: Get-IntuneApplication $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value | ? { (!($_.'@odata.type').Contains("managed")) } + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | ? { (!($_.'@odata.type').Contains("managed")) } } - + catch { $ex = $_.Exception @@ -198,7 +250,7 @@ $Resource = "deviceAppManagement/mobileApps" #################################################### -Function Get-ApplicationAssignment(){ +function Get-ApplicationAssignment { <# .SYNOPSIS @@ -221,9 +273,9 @@ param $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps/$ApplicationId/?`$expand=categories,assignments" - + try { - + if(!$ApplicationId){ write-host "No Application Id specified, specify a valid Application Id" -f Red @@ -232,14 +284,14 @@ $Resource = "deviceAppManagement/mobileApps/$ApplicationId/?`$expand=categories, } else { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get) - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) + } - + } - + catch { $ex = $_.Exception @@ -255,11 +307,11 @@ $Resource = "deviceAppManagement/mobileApps/$ApplicationId/?`$expand=categories, } -} +} #################################################### -Function Add-ApplicationAssignment(){ +function Add-ApplicationAssignment { <# .SYNOPSIS @@ -285,7 +337,7 @@ param $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assign" - + try { if(!$ApplicationId){ @@ -302,7 +354,7 @@ $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assign" } - + if(!$InstallIntent){ write-host "No Install Intent specified, specify a valid Install Intent - available, notApplicable, required, uninstall, availableWithoutEnrollment" -f Red @@ -347,7 +399,7 @@ $ExistingTargetGroupId = $Assignment.target.GroupId $ExistingInstallIntent = $Assignment.intent $JSON += @" - + { "@odata.type": "#microsoft.graph.mobileAppAssignment", "target": { @@ -389,8 +441,8 @@ $JSON += @" "@ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -415,13 +467,13 @@ $JSON = @" "@ -$uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" -Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" +$uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" +Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -441,7 +493,7 @@ Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -Conten #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -467,37 +519,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + if($Group){ $GID = $Group.id @@ -505,13 +557,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } } - + } } @@ -537,50 +589,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -593,7 +605,7 @@ $AADGroup = Read-Host -Prompt "Enter the Azure AD Group name where application w $TargetGroupId = (get-AADGroup -GroupName "$AADGroup").id - if($TargetGroupId -eq $null -or $TargetGroupId -eq ""){ + if($null -eq $TargetGroupId -or "" -eq $TargetGroupId){ Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red Write-Host @@ -624,3 +636,4 @@ $Application = Get-IntuneApplication | ? { $_.displayName -eq "$ApplicationName" Write-Host } + diff --git a/Applications/Application_InstallStats.ps1 b/Applications/Application_InstallStats.ps1 index 9f0fc1f..3f38906 100644 --- a/Applications/Application_InstallStats.ps1 +++ b/Applications/Application_InstallStats.ps1 @@ -6,152 +6,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) + + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } + + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } + + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } + } + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false + } +} -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" - -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" - -$resourceAppIdURI = "https://graph.microsoft.com" - -$authority = "https://login.microsoftonline.com/$Tenant" - - try { - - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" - - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result - - # If the accesstoken is valid then create the authentication header - - if($authResult.AccessToken){ - - # Creating header for Authorization token - - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn - } - - return $authHeader - - } - - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - - } - - } - - catch { +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, + + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', + + [Parameter(Mandatory = $false)] + [object]$Body = $null, + + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) + + try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } + + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } + + $results = @() + $nextLink = $Uri + + do { + Write-Verbose "Making request to: $nextLink" + + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } + + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } + + $response = Invoke-MgGraphRequest @requestParams + + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null + } + + } while ($nextLink) + + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" + } + else { + Write-Error "Graph API request failed: $errorMessage" + } + throw + } +} - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-IntuneApplication(){ +function Get-IntuneApplication { <# .SYNOPSIS @@ -179,15 +230,15 @@ $Resource = "deviceAppManagement/mobileApps" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") -and (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") -and (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } } @@ -210,7 +261,7 @@ $Resource = "deviceAppManagement/mobileApps" } } - + #################################################### Function Get-InstallStatusForApp { @@ -226,7 +277,7 @@ This will return the installation status of the application with the ID of a1a2a .NOTES NAME: Get-InstallStatusForApp #> - + [cmdletbinding()] param @@ -234,21 +285,21 @@ param [Parameter(Mandatory=$true)] [string]$AppId ) - + $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps/$AppId/installSummary" - + try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -259,9 +310,9 @@ param Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### @@ -279,7 +330,7 @@ This will return devices and their installation status of the application with t .NOTES NAME: Get-DeviceStatusForApp #> - + [cmdletbinding()] param @@ -287,21 +338,21 @@ param [Parameter(Mandatory=$true)] [string]$AppId ) - + $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps/$AppId/deviceStatuses" - + try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -312,59 +363,19 @@ param Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -380,3 +391,4 @@ if($Application){ } + diff --git a/Applications/Application_MAM_Get.ps1 b/Applications/Application_MAM_Get.ps1 index 21f2c07..90303de 100644 --- a/Applications/Application_MAM_Get.ps1 +++ b/Applications/Application_MAM_Get.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-IntuneMAMApplication(){ +function Get-IntuneMAMApplication { <# .SYNOPSIS @@ -173,8 +224,8 @@ $Resource = "deviceAppManagement/mobileApps" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | ? { ($_.'@odata.type').Contains("managed") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | ? { ($_.'@odata.type').Contains("managed") } } @@ -199,50 +250,10 @@ $Resource = "deviceAppManagement/mobileApps" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -250,3 +261,4 @@ $global:authToken = Get-AuthToken -User $User #################################################### Get-IntuneMAMApplication + diff --git a/Applications/Application_MDM_Export.ps1 b/Applications/Application_MDM_Export.ps1 index c3b2339..d866159 100644 --- a/Applications/Application_MDM_Export.ps1 +++ b/Applications/Application_MDM_Export.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-IntuneApplication(){ +function Get-IntuneApplication { <# .SYNOPSIS @@ -180,22 +231,22 @@ $Resource = "deviceAppManagement/mobileApps" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") -and (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") -and (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } } elseif($AppId){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)/$AppId" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)/$AppId" + (Invoke-IntuneRestMethod -Uri $uri -Method GET) } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | ? { (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) -and (!($_.'@odata.type').Contains("#microsoft.graph.windowsAppX")) -and (!($_.'@odata.type').Contains("#microsoft.graph.androidForWorkApp")) -and (!($_.'@odata.type').Contains("#microsoft.graph.windowsMobileMSI")) -and (!($_.'@odata.type').Contains("#microsoft.graph.androidLobApp")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosLobApp")) -and (!($_.'@odata.type').Contains("#microsoft.graph.microsoftStoreForBusinessApp")) } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | ? { (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) -and (!($_.'@odata.type').Contains("#microsoft.graph.windowsAppX")) -and (!($_.'@odata.type').Contains("#microsoft.graph.androidForWorkApp")) -and (!($_.'@odata.type').Contains("#microsoft.graph.windowsMobileMSI")) -and (!($_.'@odata.type').Contains("#microsoft.graph.androidLobApp")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosLobApp")) -and (!($_.'@odata.type').Contains("#microsoft.graph.microsoftStoreForBusinessApp")) } } @@ -221,7 +272,7 @@ $Resource = "deviceAppManagement/mobileApps" #################################################### -Function Export-JSONData(){ +function Export-JSONData { <# .SYNOPSIS @@ -245,7 +296,7 @@ $ExportPath try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON..." -f Red @@ -304,7 +355,7 @@ $ExportPath $JSON1 | Set-Content -LiteralPath "$ExportPath\$FileName_JSON" write-host "CSV created in $ExportPath\$FileName_CSV..." -f cyan write-host "JSON created in $ExportPath\$FileName_JSON..." -f cyan - + } } @@ -321,50 +372,10 @@ $ExportPath #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -426,3 +437,4 @@ else { Write-Host } + diff --git a/Applications/Application_MDM_Get.ps1 b/Applications/Application_MDM_Get.ps1 index 1f82e14..7831582 100644 --- a/Applications/Application_MDM_Get.ps1 +++ b/Applications/Application_MDM_Get.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-IntuneApplication(){ +function Get-IntuneApplication { <# .SYNOPSIS @@ -180,15 +231,15 @@ $Resource = "deviceAppManagement/mobileApps" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") -and (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") -and (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } } @@ -214,7 +265,7 @@ $Resource = "deviceAppManagement/mobileApps" #################################################### -Function Get-ApplicationAssignment(){ +function Get-ApplicationAssignment { <# .SYNOPSIS @@ -249,8 +300,8 @@ $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assignments" else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -275,7 +326,7 @@ $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assignments" #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -306,15 +357,15 @@ $Group_resource = "groups" if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - elseif($GroupName -eq "" -or $GroupName -eq $null){ + elseif("" -eq $GroupName -or $null -eq $GroupName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -322,15 +373,15 @@ $Group_resource = "groups" if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } elseif($Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value if($Group){ @@ -339,8 +390,8 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -371,50 +422,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -451,3 +462,4 @@ $App_Assignment = Get-ApplicationAssignment -ApplicationId $_.id Write-Host } + diff --git a/Applications/Application_MDM_Import_FromJSON.ps1 b/Applications/Application_MDM_Import_FromJSON.ps1 index cb69ac8..9345655 100644 --- a/Applications/Application_MDM_Import_FromJSON.ps1 +++ b/Applications/Application_MDM_Import_FromJSON.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-MDMApplication(){ +function Add-MDMApplication { <# .SYNOPSIS @@ -186,7 +237,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -210,7 +261,7 @@ $App_resource = "deviceAppManagement/mobileApps" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -245,7 +296,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -257,50 +308,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -331,7 +342,7 @@ $JSON_Convert = $JSON_Data | ConvertFrom-Json | Select-Object -Property * -Exclu $DisplayName = $JSON_Convert.displayName $JSON_Output = $JSON_Convert | ConvertTo-Json - + write-host write-host "MDM Application '$DisplayName' Found..." -ForegroundColor Yellow write-host @@ -339,3 +350,4 @@ $JSON_Output write-host Write-Host "MDM Application '$DisplayName'" -ForegroundColor Yellow Add-MDMApplication -JSON $JSON_Output + diff --git a/Applications/Application_MDM_Remove.ps1 b/Applications/Application_MDM_Remove.ps1 index b2672a6..480e3d2 100644 --- a/Applications/Application_MDM_Remove.ps1 +++ b/Applications/Application_MDM_Remove.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "DeviceManagementApps.ReadWrite.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-IntuneApplication(){ +function Get-IntuneApplication { <# .SYNOPSIS @@ -180,15 +232,15 @@ $Resource = "deviceAppManagement/mobileApps" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") -and (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") -and (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } } @@ -214,7 +266,7 @@ $Resource = "deviceAppManagement/mobileApps" #################################################### -Function Remove-IntuneApplication(){ +function Remove-IntuneApplication { <# .SYNOPSIS @@ -240,7 +292,7 @@ $Resource = "deviceAppManagement/mobileApps" try { - if($id -eq "" -or $id -eq $null){ + if("" -eq $id -or $null -eq $id){ write-host "No id specified for application, can't remove application..." -f Red write-host "Please specify id for application..." -f Red @@ -250,7 +302,7 @@ $Resource = "deviceAppManagement/mobileApps" else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)/$id" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)/$id" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Delete } @@ -279,50 +331,10 @@ $Resource = "deviceAppManagement/mobileApps" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -358,3 +370,4 @@ Write-Host "Application doesn't exist in Intune..." -ForegroundColor Red Write-Host } + diff --git a/Applications/Application_MacOSOffice365_Add.ps1 b/Applications/Application_MacOSOffice365_Add.ps1 index 0730ead..c7aa177 100644 --- a/Applications/Application_MacOSOffice365_Add.ps1 +++ b/Applications/Application_MacOSOffice365_Add.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-MDMApplication(){ +function Add-MDMApplication { <# .SYNOPSIS @@ -187,7 +239,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -211,7 +263,7 @@ $App_resource = "deviceAppManagement/mobileApps" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -246,7 +298,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -258,50 +310,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -340,3 +352,4 @@ $Create_Application Write-Host "Application created as $($Create_Application.displayName)/$($create_Application.id)" -ForegroundColor Cyan Write-Host + diff --git a/Applications/Application_MacOSOffice365_Add_Assign.ps1 b/Applications/Application_MacOSOffice365_Add_Assign.ps1 index 0a0c7a9..a141516 100644 --- a/Applications/Application_MacOSOffice365_Add_Assign.ps1 +++ b/Applications/Application_MacOSOffice365_Add_Assign.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-MDMApplication(){ +function Add-MDMApplication { <# .SYNOPSIS @@ -187,7 +239,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -211,7 +263,7 @@ $App_resource = "deviceAppManagement/mobileApps" #################################################### -Function Add-ApplicationAssignment(){ +function Add-ApplicationAssignment { <# .SYNOPSIS @@ -236,7 +288,7 @@ param $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assign" - + try { if(!$ApplicationId){ @@ -253,7 +305,7 @@ $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assign" } - + if(!$InstallIntent){ write-host "No Install Intent specified, specify a valid Install Intent - available, notApplicable, required, uninstall, availableWithoutEnrollment" -f Red @@ -278,11 +330,11 @@ $JSON = @" "@ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } - + catch { $ex = $_.Exception @@ -302,7 +354,7 @@ $JSON = @" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -337,7 +389,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -347,7 +399,7 @@ $JSON #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -373,37 +425,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + if($Group){ $GID = $Group.id @@ -411,13 +463,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } } - + } } @@ -443,50 +495,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -501,7 +513,7 @@ $AADGroup = Read-Host -Prompt "Enter the Azure AD Group name where applications $TargetGroupId = (get-AADGroup -GroupName "$AADGroup").id - if($TargetGroupId -eq $null -or $TargetGroupId -eq ""){ + if($null -eq $TargetGroupId -or "" -eq $TargetGroupId){ Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red Write-Host @@ -548,3 +560,4 @@ $Assign_Application = Add-ApplicationAssignment -ApplicationId $ApplicationId -T Write-Host "Assigned '$AADGroup' to $($Create_Application.displayName)/$($Create_Application.id) with" $Assign_Application.InstallIntent "install Intent" Write-Host + diff --git a/Applications/Application_Office365_Add.ps1 b/Applications/Application_Office365_Add.ps1 index a4c7fdb..a180a98 100644 --- a/Applications/Application_Office365_Add.ps1 +++ b/Applications/Application_Office365_Add.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-MDMApplication(){ +function Add-MDMApplication { <# .SYNOPSIS @@ -187,7 +239,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -211,7 +263,7 @@ $App_resource = "deviceAppManagement/mobileApps" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -246,7 +298,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -258,50 +310,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -357,3 +369,4 @@ $Create_Application Write-Host "Application created as $($Create_Application.displayName)/$($create_Application.id)" -ForegroundColor Cyan Write-Host + diff --git a/Applications/Application_Office365_Add_Assign.ps1 b/Applications/Application_Office365_Add_Assign.ps1 index 0e4a6e2..a350dbe 100644 --- a/Applications/Application_Office365_Add_Assign.ps1 +++ b/Applications/Application_Office365_Add_Assign.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-MDMApplication(){ +function Add-MDMApplication { <# .SYNOPSIS @@ -187,7 +239,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -211,7 +263,7 @@ $App_resource = "deviceAppManagement/mobileApps" #################################################### -Function Add-ApplicationAssignment(){ +function Add-ApplicationAssignment { <# .SYNOPSIS @@ -236,7 +288,7 @@ param $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assign" - + try { if(!$ApplicationId){ @@ -253,7 +305,7 @@ $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assign" } - + if(!$InstallIntent){ write-host "No Install Intent specified, specify a valid Install Intent - available, notApplicable, required, uninstall, availableWithoutEnrollment" -f Red @@ -278,11 +330,11 @@ $JSON = @" "@ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } - + catch { $ex = $_.Exception @@ -302,7 +354,7 @@ $JSON = @" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -337,7 +389,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -347,7 +399,7 @@ $JSON #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -373,37 +425,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + if($Group){ $GID = $Group.id @@ -411,13 +463,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } } - + } } @@ -443,50 +495,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -501,7 +513,7 @@ $AADGroup = Read-Host -Prompt "Enter the Azure AD Group name where applications $TargetGroupId = (get-AADGroup -GroupName "$AADGroup").id - if($TargetGroupId -eq $null -or $TargetGroupId -eq ""){ + if($null -eq $TargetGroupId -or "" -eq $TargetGroupId){ Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red Write-Host @@ -565,3 +577,4 @@ $Assign_Application = Add-ApplicationAssignment -ApplicationId $ApplicationId -T Write-Host "Assigned '$AADGroup' to $($Create_Application.displayName)/$($Create_Application.id) with" $Assign_Application.InstallIntent "install Intent" Write-Host + diff --git a/Applications/Application_Web_Add_No_Icon.ps1 b/Applications/Application_Web_Add_No_Icon.ps1 index a4a3de7..bdc20c3 100644 --- a/Applications/Application_Web_Add_No_Icon.ps1 +++ b/Applications/Application_Web_Add_No_Icon.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -197,7 +249,7 @@ $JSON #################################################### -Function Add-WebApplication(){ +function Add-WebApplication { <# .SYNOPSIS @@ -263,7 +315,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -272,7 +324,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -300,50 +352,10 @@ $App_resource = "deviceAppManagement/mobileApps" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -372,3 +384,4 @@ $Create_Bing = Add-WebApplication -JSON $Bing -IconURL "$iconUrl_Bing" Write-Host "Application created as $($Create_Bing.displayName)/$($create_Bing.id)" Write-Host + diff --git a/Applications/Application_Web_Add_With_Icon.ps1 b/Applications/Application_Web_Add_With_Icon.ps1 index 3c09db5..212f30a 100644 --- a/Applications/Application_Web_Add_With_Icon.ps1 +++ b/Applications/Application_Web_Add_With_Icon.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -197,7 +249,7 @@ $JSON #################################################### -Function Add-WebApplication(){ +function Add-WebApplication { <# .SYNOPSIS @@ -263,7 +315,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -272,7 +324,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -300,50 +352,10 @@ $App_resource = "deviceAppManagement/mobileApps" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -383,3 +395,4 @@ $Create_Bing = Add-WebApplication -JSON $Bing -IconURL "$iconUrl_Bing" Write-Host "Application created as $($Create_Bing.displayName)/$($create_Bing.id)" Write-Host + diff --git a/Applications/Application_iOS_Add.ps1 b/Applications/Application_iOS_Add.ps1 index b897829..10f518f 100644 --- a/Applications/Application_iOS_Add.ps1 +++ b/Applications/Application_iOS_Add.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-itunesApplication(){ +function Get-itunesApplication { <# .SYNOPSIS @@ -169,65 +221,65 @@ Function Get-itunesApplication(){ NAME: Get-itunesApplication https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ #> - + [cmdletbinding()] - + param ( [Parameter(Mandatory=$true)] $SearchString, [int]$Limit ) - + try{ - + Write-Verbose $SearchString - + # Testing if string contains a space and replacing it with %20 $SearchString = $SearchString.replace(" ","%20") - + Write-Verbose "SearchString variable converted if there is a space in the name $SearchString" - + if($Limit){ - + $iTunesUrl = "https://itunes.apple.com/search?country=us&media=software&entity=software,iPadSoftware&term=$SearchString&limit=$limit" - + } - + else { - + $iTunesUrl = "https://itunes.apple.com/search?country=us&entity=software&term=$SearchString&attribute=softwareDeveloper" - + } - + write-verbose $iTunesUrl - + $apps = Invoke-RestMethod -Uri $iTunesUrl -Method Get - + # Putting sleep in so that no more than 20 API calls to itunes REST API # https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ Start-Sleep 3 - + return $apps - + } - + catch { - + write-host $_.Exception.Message -f Red write-host $_.Exception.ItemName -f Red write-verbose $_.Exception write-host break - + } - + } #################################################### -Function Add-iOSApplication(){ - +function Add-iOSApplication { + <# .SYNOPSIS This function is used to add an iOS application using the Graph API REST interface @@ -239,71 +291,71 @@ Function Add-iOSApplication(){ .NOTES NAME: Add-iOSApplication #> - + [cmdletbinding()] - + param ( $itunesApp ) - + $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + $app = $itunesApp - + Write-Verbose $app - + Write-Host "Publishing $($app.trackName)" -f Yellow - + # Step 1 - Downloading the icon for the application $iconUrl = $app.artworkUrl60 - - if ($iconUrl -eq $null){ - + + if ($null -eq $iconUrl){ + Write-Host "60x60 icon not found, using 100x100 icon" $iconUrl = $app.artworkUrl100 - + } - - if ($iconUrl -eq $null){ - + + if ($null -eq $iconUrl){ + Write-Host "60x60 icon not found, using 512x512 icon" $iconUrl = $app.artworkUrl512 - + } - + $iconResponse = Invoke-WebRequest $iconUrl $base64icon = [System.Convert]::ToBase64String($iconResponse.Content) $iconType = $iconResponse.Headers["Content-Type"] - + if(($app.minimumOsVersion.Split(".")).Count -gt 2){ - + $Split = $app.minimumOsVersion.Split(".") - + $MOV = $Split[0] + "." + $Split[1] - + $osVersion = [Convert]::ToDouble($MOV) - + } - + else { - + $osVersion = [Convert]::ToDouble($app.minimumOsVersion) - + } - + # Setting support Operating System Devices if($app.supportedDevices -match "iPadMini"){ $iPad = $true } else { $iPad = $false } if($app.supportedDevices -match "iPhone6"){ $iPhone = $true } else { $iPhone = $false } - + # Step 2 - Create the Hashtable Object of the application $description = $app.description -replace "[^\x00-\x7F]+","" - + $graphApp = @{ "@odata.type"="#microsoft.graph.iosStoreApp"; displayName=$app.trackName; @@ -319,7 +371,7 @@ Function Add-iOSApplication(){ iPad=$iPad; iPhoneAndIPod=$iPhone; }; - minimumSupportedOperatingSystem=@{ + minimumSupportedOperatingSystem=@{ v8_0=$osVersion -lt 9.0; v9_0=$osVersion.ToString().StartsWith(9) v10_0=$osVersion.ToString().StartsWith(10) @@ -328,24 +380,24 @@ Function Add-iOSApplication(){ v13_0=$osVersion.ToString().StartsWith(13) }; }; - + # Step 3 - Publish the application to Graph Write-Host "Creating application via Graph" $createResult = Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body (ConvertTo-Json $graphApp) -Headers $authToken Write-Host "Application created as $uri/$($createResult.id)" write-host - + } - + catch { - + $ex = $_.Exception Write-Host "Request to $Uri failed with HTTP Status $([int]$ex.Response.StatusCode) $($ex.Response.StatusDescription)" -f Red - + $errorResponse = $ex.Response.GetResponseStream() - + $ex.Response.GetResponseStream() - + $reader = New-Object System.IO.StreamReader($errorResponse) $reader.BaseStream.Position = 0 $reader.DiscardBufferedData() @@ -354,59 +406,19 @@ Function Add-iOSApplication(){ Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -421,7 +433,7 @@ $OldCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture $OldUICulture = [System.Threading.Thread]::CurrentThread.CurrentUICulture -# Set new Culture for script execution +# Set new Culture for script execution [System.Threading.Thread]::CurrentThread.CurrentCulture = $culture [System.Threading.Thread]::CurrentThread.CurrentUICulture = $culture @@ -508,3 +520,4 @@ else { # Restore culture from backup [System.Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture [System.Threading.Thread]::CurrentThread.CurrentUICulture = $OldUICulture + diff --git a/Applications/Application_iOS_Add_Assign.ps1 b/Applications/Application_iOS_Add_Assign.ps1 index 00704da..5108bdd 100644 --- a/Applications/Application_iOS_Add_Assign.ps1 +++ b/Applications/Application_iOS_Add_Assign.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-itunesApplication(){ +function Get-itunesApplication { <# .SYNOPSIS @@ -169,65 +221,65 @@ Function Get-itunesApplication(){ NAME: Get-itunesApplication https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ #> - + [cmdletbinding()] - + param ( [Parameter(Mandatory=$true)] $SearchString, [int]$Limit ) - + try{ - + Write-Verbose $SearchString - + # Testing if string contains a space and replacing it with %20 $SearchString = $SearchString.replace(" ","%20") - + Write-Verbose "SearchString variable converted if there is a space in the name $SearchString" - + if($Limit){ - + $iTunesUrl = "https://itunes.apple.com/search?country=us&media=software&entity=software,iPadSoftware&term=$SearchString&limit=$limit" - + } - + else { - + $iTunesUrl = "https://itunes.apple.com/search?country=us&entity=software&term=$SearchString&attribute=softwareDeveloper" - + } - + write-verbose $iTunesUrl - + $apps = Invoke-RestMethod -Uri $iTunesUrl -Method Get - + # Putting sleep in so that no more than 20 API calls to itunes REST API # https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ Start-Sleep 3 - + return $apps - + } - + catch { - + write-host $_.Exception.Message -f Red write-host $_.Exception.ItemName -f Red write-verbose $_.Exception write-host break - + } - + } #################################################### -Function Add-iOSApplication(){ - +function Add-iOSApplication { + <# .SYNOPSIS This function is used to add an iOS application using the Graph API REST interface @@ -239,71 +291,71 @@ Function Add-iOSApplication(){ .NOTES NAME: Add-iOSApplication #> - + [cmdletbinding()] - + param ( $itunesApp ) - + $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + $app = $itunesApp - + Write-Verbose $app - + Write-Host "Publishing $($app.trackName)" -f Yellow - + # Step 1 - Downloading the icon for the application $iconUrl = $app.artworkUrl60 - - if ($iconUrl -eq $null){ - + + if ($null -eq $iconUrl){ + Write-Host "60x60 icon not found, using 100x100 icon" $iconUrl = $app.artworkUrl100 - + } - - if ($iconUrl -eq $null){ - + + if ($null -eq $iconUrl){ + Write-Host "60x60 icon not found, using 512x512 icon" $iconUrl = $app.artworkUrl512 - + } - + $iconResponse = Invoke-WebRequest $iconUrl $base64icon = [System.Convert]::ToBase64String($iconResponse.Content) $iconType = $iconResponse.Headers["Content-Type"] - + if(($app.minimumOsVersion.Split(".")).Count -gt 2){ - + $Split = $app.minimumOsVersion.Split(".") - + $MOV = $Split[0] + "." + $Split[1] - + $osVersion = [Convert]::ToDouble($MOV) - + } - + else { - + $osVersion = [Convert]::ToDouble($app.minimumOsVersion) - + } - + # Setting support Operating System Devices if($app.supportedDevices -match "iPadMini"){ $iPad = $true } else { $iPad = $false } if($app.supportedDevices -match "iPhone6"){ $iPhone = $true } else { $iPhone = $false } - + # Step 2 - Create the Hashtable Object of the application $description = $app.description -replace "[^\x00-\x7F]+","" - + $graphApp = @{ "@odata.type"="#microsoft.graph.iosStoreApp"; displayName=$app.trackName; @@ -319,7 +371,7 @@ Function Add-iOSApplication(){ iPad=$iPad; iPhoneAndIPod=$iPhone; }; - minimumSupportedOperatingSystem=@{ + minimumSupportedOperatingSystem=@{ v8_0=$osVersion -lt 9.0; v9_0=$osVersion.ToString().StartsWith(9) v10_0=$osVersion.ToString().StartsWith(10) @@ -328,24 +380,24 @@ Function Add-iOSApplication(){ v13_0=$osVersion.ToString().StartsWith(13) }; }; - + # Step 3 - Publish the application to Graph Write-Host "Creating application via Graph" $createResult = Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body (ConvertTo-Json $graphApp) -Headers $authToken Write-Host "Application created as $uri/$($createResult.id)" write-host - + } - + catch { - + $ex = $_.Exception Write-Host "Request to $Uri failed with HTTP Status $([int]$ex.Response.StatusCode) $($ex.Response.StatusDescription)" -f Red - + $errorResponse = $ex.Response.GetResponseStream() - + $ex.Response.GetResponseStream() - + $reader = New-Object System.IO.StreamReader($errorResponse) $reader.BaseStream.Position = 0 $reader.DiscardBufferedData() @@ -354,14 +406,14 @@ Function Add-iOSApplication(){ Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### -Function Add-ApplicationAssignment(){ +function Add-ApplicationAssignment { <# .SYNOPSIS @@ -386,7 +438,7 @@ param $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assign" - + try { if(!$ApplicationId){ @@ -403,7 +455,7 @@ $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assign" } - + if(!$InstallIntent){ write-host "No Install Intent specified, specify a valid Install Intent - available, notApplicable, required, uninstall, availableWithoutEnrollment" -f Red @@ -428,11 +480,11 @@ $JSON = @" "@ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } - + catch { $ex = $_.Exception @@ -452,7 +504,7 @@ $JSON = @" #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -478,37 +530,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ $GID = $Group.id @@ -516,13 +568,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } } @@ -548,50 +600,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -607,7 +619,7 @@ $AADGroup = Read-Host -Prompt "Enter the Azure AD Group name where applications $TargetGroupId = (get-AADGroup -GroupName "$AADGroup").id - if($TargetGroupId -eq $null -or $TargetGroupId -eq ""){ + if($null -eq $TargetGroupId -or "" -eq $TargetGroupId){ Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red Write-Host @@ -627,7 +639,7 @@ $OldCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture $OldUICulture = [System.Threading.Thread]::CurrentThread.CurrentUICulture -# Set new Culture for script execution +# Set new Culture for script execution [System.Threading.Thread]::CurrentThread.CurrentCulture = $culture [System.Threading.Thread]::CurrentThread.CurrentUICulture = $culture @@ -641,7 +653,7 @@ $Applications = 'Microsoft Outlook','Microsoft Excel','OneDrive' # If application list is specified if($Applications) { - + # Looping through applications list foreach($Application in $Applications){ @@ -667,7 +679,7 @@ if($Applications) { } } - + # Single application found, adding application elseif($itunesApp){ @@ -679,7 +691,7 @@ if($Applications) { Write-Host "Assigned '$AADGroup' to $($Create_App.displayName)/$($create_App.id) with" $Assign_App.InstallIntent "install Intent" Write-Host - + } # if application isn't found in itunes returning doesn't exist @@ -697,14 +709,14 @@ if($Applications) { # No Applications have been specified else { - + # if there are results returned from itunes query if($itunesApps.results){ write-host write-host "Number of iOS applications to add:" $itunesApps.results.count -f Yellow Write-Host - + # Looping through applications returned from itunes foreach($itunesApp in $itunesApps.results){ @@ -734,3 +746,4 @@ else { #Restore culture from backup [System.Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture [System.Threading.Thread]::CurrentThread.CurrentUICulture = $OldUICulture + diff --git a/Applications/MobileAppConfiguration_Get.ps1 b/Applications/MobileAppConfiguration_Get.ps1 index c57d10d..960cc9a 100644 --- a/Applications/MobileAppConfiguration_Get.ps1 +++ b/Applications/MobileAppConfiguration_Get.ps1 @@ -1,4 +1,4 @@ - + <# @@ -8,160 +8,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> - -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) + + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } + + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } + + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } - -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null +} -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$clientId = "" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) -$authority = "https://login.microsoftonline.com/$Tenant" - try { - - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" - - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $MethodArguments = [Type[]]@("System.String", "System.String", "System.Uri", "Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior", "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier") - $NonAsync = $AuthContext.GetType().GetMethod("AcquireToken", $MethodArguments) - - if ($NonAsync -ne $null) { - $authResult = $authContext.AcquireToken($resourceAppIdURI, $clientId, [Uri]$redirectUri, [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Auto, $userId) - } else { - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $clientId, [Uri]$redirectUri, $platformParameters, $userId).Result - } - - # If the accesstoken is valid then create the authentication header - - if($authResult.AccessToken){ - - # Creating header for Authorization token - - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn - } - - return $authHeader - + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" } - - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" } - + + $results = @() + $nextLink = $Uri + + do { + Write-Verbose "Making request to: $nextLink" + + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } + + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } + + $response = Invoke-MgGraphRequest @requestParams + + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null + } + + } while ($nextLink) + + return $results } - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break - + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" + } + else { + Write-Error "Graph API request failed: $errorMessage" + } + throw } - } #################################################### -Function Get-MobileAppConfigurations(){ - +#################################################### + + +#################################################### + +function Get-MobileAppConfigurations { + <# .SYNOPSIS This function is used to get all Mobile App Configuration Policies using the Graph API REST interface @@ -175,21 +219,21 @@ NAME: Get-MobileAppConfigurations #> [cmdletbinding()] - + $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileAppConfigurations?`$expand=assignments" - + try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Method Get -Headers $authToken).value + Invoke-IntuneRestMethod -Uri $uri -Method GET } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -200,15 +244,15 @@ $Resource = "deviceAppManagement/mobileAppConfigurations?`$expand=assignments" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### -Function Get-TargetedManagedAppConfigurations(){ - +function Get-TargetedManagedAppConfigurations { + <# .SYNOPSIS This function is used to get all Targeted Managed App Configuration Policies using the Graph API REST interface @@ -228,31 +272,31 @@ param [Parameter(Mandatory=$false)] $PolicyId ) - + $graphApiVersion = "Beta" - + try { if($PolicyId){ $Resource = "deviceAppManagement/targetedManagedAppConfigurations('$PolicyId')?`$expand=apps,assignments" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Method Get -Headers $authToken) + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET) } else { $Resource = "deviceAppManagement/targetedManagedAppConfigurations" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Method Get -Headers $authToken).value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET } } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -263,14 +307,14 @@ $graphApiVersion = "Beta" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -301,15 +345,15 @@ $Group_resource = "groups" if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - elseif($GroupName -eq "" -or $GroupName -eq $null){ + elseif("" -eq $GroupName -or $null -eq $GroupName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -317,15 +361,15 @@ $Group_resource = "groups" if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } elseif($Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value if($Group){ @@ -334,8 +378,8 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -366,50 +410,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -489,3 +493,4 @@ else { Write-Host } + diff --git a/Auditing/Auditing_Get.ps1 b/Auditing/Auditing_Get.ps1 index 9fa51f5..92fdec6 100644 --- a/Auditing/Auditing_Get.ps1 +++ b/Auditing/Auditing_Get.ps1 @@ -7,153 +7,206 @@ See LICENSE in the project root for license information. #> -#################################################### -function Get-AuthToken { - +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> - -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All", + "DeviceManagementConfiguration.Read.All", + "DeviceManagementManagedDevices.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) + + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } + + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } + + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } - -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" - -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" - -$resourceAppIdURI = "https://graph.microsoft.com" - -$authority = "https://login.microsoftonline.com/$Tenant" - +} + +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, + + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', + + [Parameter(Mandatory = $false)] + [object]$Body = $null, + + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) + try { - - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" - - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result - - # If the accesstoken is valid then create the authentication header - - if($authResult.AccessToken){ - - # Creating header for Authorization token - - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn - } - - return $authHeader - + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" } - - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" } - + + $results = @() + $nextLink = $Uri + + do { + Write-Verbose "Making request to: $nextLink" + + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } + + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } + + $response = Invoke-MgGraphRequest @requestParams + + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null + } + + } while ($nextLink) + + return $results } - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break - + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" + } + else { + Write-Error "Graph API request failed: $errorMessage" + } + throw } - } - + +#################################################### + +#################################################### + + #################################################### -Function Get-AuditCategories(){ - +function Get-AuditCategories { + <# .SYNOPSIS This function is used to get all audit categories from the Graph API REST interface @@ -165,26 +218,26 @@ Returns all audit categories configured in Intune .NOTES NAME: Get-AuditCategories #> - + [cmdletbinding()] - + param ( $Name ) - + $graphApiVersion = "Beta" $Resource = "deviceManagement/auditEvents/getAuditCategories" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -195,15 +248,15 @@ $Resource = "deviceManagement/auditEvents/getAuditCategories" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### -Function Get-AuditEvents(){ - +function Get-AuditEvents { + <# .SYNOPSIS This function is used to get all audit events from a specific category using the Graph API REST interface @@ -217,9 +270,9 @@ Returns audit events from the category "Application" in the past 7 days configur .NOTES NAME: Get-AuditEvents #> - + [cmdletbinding()] - + param ( [Parameter(Mandatory=$true)] @@ -228,7 +281,7 @@ param [ValidateRange(1,30)] [Int]$days ) - + $graphApiVersion = "Beta" $Resource = "deviceManagement/auditEvents" @@ -236,17 +289,17 @@ if($days){ $days } else { $days = 30 } $daysago = "{0:s}" -f (get-date).AddDays(-$days) + "Z" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)?`$filter=category eq '$Category' and activityDateTime gt $daysago" + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)?`$filter=category eq '$Category' and activityDateTime gt $daysago" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -257,63 +310,23 @@ $daysago = "{0:s}" -f (get-date).AddDays(-$days) + "Z" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } - + #################################################### #region Authentication - -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - + +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } - + #endregion - + #################################################### $AuditCategories = Get-AuditCategories @@ -322,8 +335,8 @@ Write-Host "Intune Audit Categories:" -ForegroundColor Yellow $menu = @{} -for ($i=1;$i -le $AuditCategories.count; $i++) -{ Write-Host "$i. $($AuditCategories[$i-1])" +for ($i=1;$i -le $AuditCategories.count; $i++) +{ Write-Host "$i. $($AuditCategories[$i-1])" $menu.Add($i,($AuditCategories[$i-1]))} Write-Host @@ -386,3 +399,4 @@ $Events = Get-AuditEvents -Category $selection } } + diff --git a/Auditing/Auditing_User_Report_Get.ps1 b/Auditing/Auditing_User_Report_Get.ps1 index 9e4c959..e9f3f24 100644 --- a/Auditing/Auditing_User_Report_Get.ps1 +++ b/Auditing/Auditing_User_Report_Get.ps1 @@ -1,158 +1,211 @@ - + <# -  + .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. #> -#################################################### -  -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.Read.All", + "DeviceManagementConfiguration.Read.All", + "DeviceManagementManagedDevices.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - } + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null + [Parameter(Mandatory = $false)] + [object]$Body = $null, -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) -$clientId = "" -  -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" -  -$resourceAppIdURI = "https://graph.microsoft.com" -  -$authority = "https://login.microsoftonline.com/$Tenant" -  try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } -} -  #################################################### -Function Get-AADUser(){ +function Get-AADUser { <# .SYNOPSIS @@ -180,36 +233,36 @@ param # Defining Variables $graphApiVersion = "v1.0" $User_resource = "users" - + try { - - if($userPrincipalName -eq "" -or $userPrincipalName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - + + if("" -eq $userPrincipalName -or $null -eq $userPrincipalName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + } else { - - if($Property -eq "" -or $Property -eq $null){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName" + if("" -eq $Property -or $null -eq $Property){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName" Write-Verbose $uri - Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get + Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } } - + } catch { @@ -231,8 +284,8 @@ $User_resource = "users" #################################################### -Function Get-AuditCategories(){ - +function Get-AuditCategories { + <# .SYNOPSIS This function is used to get all audit categories from the Graph API REST interface @@ -244,26 +297,26 @@ Returns all audit categories configured in Intune .NOTES NAME: Get-AuditCategories #> - + [cmdletbinding()] - + param ( $Name ) - + $graphApiVersion = "Beta" $Resource = "deviceManagement/auditEvents/getAuditCategories" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -274,15 +327,15 @@ $Resource = "deviceManagement/auditEvents/getAuditCategories" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } - + #################################################### -Function Get-AuditEvents(){ - +function Get-AuditEvents { + <# .SYNOPSIS This function is used to get all audit events from a specific category using the Graph API REST interface @@ -296,9 +349,9 @@ Returns audit events from the category "Application" in the past 7 days configur .NOTES NAME: Get-AuditEvents #> - + [cmdletbinding()] - + param ( [Parameter(Mandatory=$true)] @@ -307,7 +360,7 @@ param [ValidateRange(1,30)] [Int]$days ) - + $graphApiVersion = "Beta" $Resource = "deviceManagement/auditEvents" @@ -315,17 +368,17 @@ if($days){ $days } else { $days = 30 } $daysago = "{0:s}" -f (get-date).AddDays(-$days) + "Z" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)?`$filter=category eq '$Category' and activityDateTime gt $daysago" + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)?`$filter=category eq '$Category' and activityDateTime gt $daysago" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -336,59 +389,19 @@ $daysago = "{0:s}" -f (get-date).AddDays(-$days) + "Z" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -470,3 +483,4 @@ $Events += $AuditEvents Write-Host } + diff --git a/Authentication/Auth_AppOnly.ps1 b/Authentication/Auth_AppOnly.ps1 index 8cc165c..b1eccd9 100644 --- a/Authentication/Auth_AppOnly.ps1 +++ b/Authentication/Auth_AppOnly.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,133 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string]$Tenant, - - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string]$ClientId, - - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string]$ClientSecret -) - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - if ($AadModule -eq $null) { + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} + +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', - if($AadModule.count -gt 1){ + [Parameter(Mandatory = $false)] + [object]$Body = $null, - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } + try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - # Checking if there are multiple versions of the same module found + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - if($AadModule.count -gt 1){ + $results = @() + $nextLink = $Uri - $aadModule = $AadModule | select -Unique + do { + Write-Verbose "Making request to: $nextLink" + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - } + $response = Invoke-MgGraphRequest @requestParams - else { + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + } while ($nextLink) + return $results } - -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" - -$resourceAppIdURI = "https://graph.microsoft.com" - -$authority = "https://login.microsoftonline.com/$Tenant" - - try { - - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority, $false - - # turn this on for app only auth - $ClientCred = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential" -ArgumentList $clientId, $ClientSecret - - # turn this on for app only auth - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $ClientCred) - - - # If the accesstoken is valid then create the authentication header - $accesstoken = $authResult.Result.CreateAuthorizationHeader() - - $authHeader = @{ - - 'Authorization'=$accesstoken - + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - - return $authHeader - + else { + Write-Error "Graph API request failed: $errorMessage" + } + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -$global:authToken = Get-AuthToken -Tenant "tenantname.onmicrosoft.com" -ClientId "ClientId/ApplicationId" -ClientSecret "ClientSecret" +$global:authToken = Connect-GraphAPInant "tenantname.onmicrosoft.com" -ClientId "ClientId/ApplicationId" -ClientSecret "ClientSecret" -#################################################### \ No newline at end of file +#################################################### diff --git a/Authentication/Auth_From_File.ps1 b/Authentication/Auth_From_File.ps1 index 07fefb9..97346bb 100644 --- a/Authentication/Auth_From_File.ps1 +++ b/Authentication/Auth_From_File.ps1 @@ -7,226 +7,209 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> - -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User, - $Password -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" - -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" - -$resourceAppIdURI = "https://graph.microsoft.com" - -$authority = "https://login.microsoftonline.com/$Tenant" + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) try { - - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" - - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - if($Password -eq $null){ - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result - + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } } - else { - - if(test-path "$Password"){ - - $UserPassword = get-Content "$Password" | ConvertTo-SecureString - - $userCredentials = new-object Microsoft.IdentityModel.Clients.ActiveDirectory.UserPasswordCredential -ArgumentList $userUPN,$UserPassword - - $authResult = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContextIntegratedAuthExtensions]::AcquireTokenAsync($authContext, $resourceAppIdURI, $clientid, $userCredentials).Result; - - } - - else { - - Write-Host "Path to Password file" $Password "doesn't exist, please specify a valid path..." -ForegroundColor Red - Write-Host "Script can't continue..." -ForegroundColor Red - Write-Host - break - - } - + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false } - if($authResult.AccessToken){ - - # Creating header for Authorization token - - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn - } - - return $authHeader - + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Failed to establish connection to Microsoft Graph" + return $false } - } - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break - + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } - } -#################################################### - -#region Authentication +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$User = "serviceaccount@tenant.onmicrosoft.com" -$Password = "c:\credentials\credentials.txt" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -write-host + [Parameter(Mandatory = $false)] + [object]$Body = $null, -# Checking if authToken exists before running authentication -if($global:authToken){ + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() + try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - if($TokenExpires -le 0){ + $results = @() + $nextLink = $Uri - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host + do { + Write-Verbose "Making request to: $nextLink" - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($User -eq $null -or $User -eq ""){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host + $response = Invoke-MgGraphRequest @requestParams + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - $global:authToken = Get-AuthToken -User $User -Password "$Password" + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" + } + else { + Write-Error "Graph API request failed: $errorMessage" } + throw + } } -# Authentication doesn't exist, calling Get-AuthToken function - -else { +#################################################### - if($User -eq $null -or $User -eq ""){ +#################################################### - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - } +#################################################### -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User -Password "$Password" +#region Authentication +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -234,3 +217,4 @@ $global:authToken = Get-AuthToken -User $User -Password "$Password" #################################################### Write-Host + diff --git a/Batch/Batch_Post.ps1 b/Batch/Batch_Post.ps1 index 268dc31..140d7e9 100644 --- a/Batch/Batch_Post.ps1 +++ b/Batch/Batch_Post.ps1 @@ -1,4 +1,4 @@ - + <#   .COPYRIGHT @@ -7,197 +7,209 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" -  -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" -  -$resourceAppIdURI = "https://graph.microsoft.com" -  -$authority = "https://login.microsoftonline.com/$Tenant" -  try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" - - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result - - # If the accesstoken is valid then create the authentication header - - if($authResult.AccessToken){ - - # Creating header for Authorization token - - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn - } - - return $authHeader + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Failed to establish connection to Microsoft Graph" + return $false } - } - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break - + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } - } -#################################################### +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -#region Authentication + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -write-host + [Parameter(Mandatory = $false)] + [object]$Body = $null, -# Checking if authToken exists before running authentication -if($global:authToken){ + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() + try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - if($TokenExpires -le 0){ + $results = @() + $nextLink = $Uri - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host + do { + Write-Verbose "Making request to: $nextLink" - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($User -eq $null -or $User -eq ""){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host + $response = Invoke-MgGraphRequest @requestParams + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - $global:authToken = Get-AuthToken -User $User + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } + else { + Write-Error "Graph API request failed: $errorMessage" + } + throw + } } -# Authentication doesn't exist, calling Get-AuthToken function - -else { +#################################################### - if($User -eq $null -or $User -eq ""){ +#################################################### - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - } +#################################################### -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User +#region Authentication +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -230,7 +242,7 @@ $batch = @" #################################################### -$uri = "https://graph.microsoft.com/beta/`$batch" +$uri = "beta/`$batch" $Post = Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $batch -ContentType "application/json" @@ -242,3 +254,4 @@ foreach($Element in $Post.responses.body){ Write-Host } + diff --git a/CertificationAuthority/CertificateConnector_Get.ps1 b/CertificationAuthority/CertificateConnector_Get.ps1 index 50f67c1..de9756e 100644 --- a/CertificationAuthority/CertificateConnector_Get.ps1 +++ b/CertificationAuthority/CertificateConnector_Get.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-CertificateConnector(){ +function Get-CertificateConnector { <# .SYNOPSIS @@ -182,15 +233,15 @@ $Resource = "deviceManagement/ndesconnectors" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -217,50 +268,10 @@ $Resource = "deviceManagement/ndesconnectors" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -290,3 +301,4 @@ else { } + diff --git a/CertificationAuthority/Validate-NDESConfiguration.ps1 b/CertificationAuthority/Validate-NDESConfiguration.ps1 index 73ac914..a90dfc2 100644 --- a/CertificationAuthority/Validate-NDESConfiguration.ps1 +++ b/CertificationAuthority/Validate-NDESConfiguration.ps1 @@ -1,12 +1,12 @@ - + <# .SYNOPSIS Highlights configuration problems on an NDES server, as configured for use with Intune Standalone SCEP certificates. .DESCRIPTION -Validate-NDESConfig looks at the configuration of your NDES server and ensures it aligns to the "Configure and manage SCEP -certificates with Intune" article. +Validate-NDESConfig looks at the configuration of your NDES server and ensures it aligns to the "Configure and manage SCEP +certificates with Intune" article. .NOTE This script is used purely to validate the configuration. All remedial tasks will need to be carried out manually. Where possible, a link and section description will be provided. @@ -31,7 +31,7 @@ Param( if ($_ -match ".\\."){ $True - + } else { @@ -43,7 +43,7 @@ Param( $EnteredDomain = $_.split("\") $ads = New-Object -ComObject ADSystemInfo $Domain = $ads.GetType().InvokeMember('DomainShortName','GetProperty', $Null, $ads, $Null) - + if ($EnteredDomain -like "$Domain") { $True @@ -51,7 +51,7 @@ Param( } else { - + Throw "Incorrect Domain. Ensure domain is '$($Domain)\'" } @@ -71,7 +71,7 @@ Param( } else { - + Throw "The Network Device Enrollment Server and the Certificate Authority are not members of the same Active Directory domain. This is an unsupported configuration." } @@ -90,13 +90,205 @@ Param( [parameter(ParameterSetName="Help")] [alias("u")] -[switch]$usage +[switch]$usage + - ) ####################################################################### + +function Connect-GraphAPI { +<# +.SYNOPSIS +Connects to Microsoft Graph API with appropriate scopes for Intune operations +.DESCRIPTION +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment +.NOTES +Requires Microsoft.Graph.Authentication module +#> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) + + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } + + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } + + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } + } + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false + } +} + +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, + + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', + + [Parameter(Mandatory = $false)] + [object]$Body = $null, + + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) + + try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } + + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } + + $results = @() + $nextLink = $Uri + + do { + Write-Verbose "Making request to: $nextLink" + + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } + + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } + + $response = Invoke-MgGraphRequest @requestParams + + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null + } + + } while ($nextLink) + + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" + } + else { + Write-Error "Graph API request failed: $errorMessage" + } + throw + } +} + +#################################################### + Function Log-ScriptEvent { [CmdletBinding()] @@ -116,7 +308,7 @@ Param( [Single]$Severity ) -$DateTime = New-Object -ComObject WbemScripting.SWbemDateTime +$DateTime = New-Object -ComObject WbemScripting.SWbemDateTime $DateTime.SetVarDate($(Get-Date)) $UtcValue = $DateTime.Value $UtcOffset = $UtcValue.Substring(21, $UtcValue.Length - 21) @@ -141,11 +333,11 @@ function Show-Usage { Write-Host Write-Host "-help -h Displays the help." Write-Host "-usage -u Displays this usage information." - Write-Host "-NDESExternalHostname -ed External DNS name for the NDES server (SSL certificate subject will be checked for this. It should be in the SAN of the certificate if" + Write-Host "-NDESExternalHostname -ed External DNS name for the NDES server (SSL certificate subject will be checked for this. It should be in the SAN of the certificate if" write-host " clients communicate directly with the NDES server)" Write-Host "-NDESServiceAccount -sa Username of the NDES service account. Format is Domain\sAMAccountName, such as Contoso\NDES_SVC." Write-Host "-IssuingCAServerFQDN -ca Name of the issuing CA to which you'll be connecting the NDES server. Format is FQDN, such as 'MyIssuingCAServer.contoso.com'." - Write-Host "-SCEPUserCertTemplate -t Name of the SCEP Certificate template. Please note this is _not_ the display name of the template. Value should not contain spaces." + Write-Host "-SCEPUserCertTemplate -t Name of the SCEP Certificate template. Please note this is _not_ the display name of the template. Value should not contain spaces." Write-Host } @@ -198,7 +390,7 @@ $LogFilePath = "$($TempDirPath)\Validate-NDESConfig.log" Write-Host Write-host "......................................................." Write-Host - Write-Host "NDES Service Account = "-NoNewline + Write-Host "NDES Service Account = "-NoNewline Write-Host "$($NDESServiceAccount)" -ForegroundColor Cyan Write-host Write-Host "Issuing CA Server = " -NoNewline @@ -210,7 +402,7 @@ $LogFilePath = "$($TempDirPath)\Validate-NDESConfig.log" Write-host "......................................................." Write-Host Write-Host "Proceed with variables? [Y]es, [N]o" - + $confirmation = Read-Host #endregion @@ -231,7 +423,7 @@ $LogFilePath = "$($TempDirPath)\Validate-NDESConfig.log" #region Install RSAT tools, Check if NDES and IIS installed if (-not (Get-WindowsFeature ADCS-Device-Enrollment).Installed){ - + Write-Host "Error: NDES Not installed" -BackgroundColor Red write-host "Exiting....................." Log-ScriptEvent $LogFilePath "NDES Not installed" NDES_Validation 3 @@ -249,7 +441,7 @@ Import-Module ActiveDirectory | Out-Null Write-Warning "IIS is not installed. Some tests will not run as we're unable to import the WebAdministration module" Write-Host Log-ScriptEvent $LogFilePath "IIS is not installed. Some tests will not run as we're unable to import the WebAdministration module" NDES_Validation 2 - + } else { @@ -263,7 +455,7 @@ Import-Module ActiveDirectory | Out-Null ####################################################################### #region checking OS version - + Write-Host Write-host "Checking Windows OS version..." -ForegroundColor Yellow Write-host @@ -273,26 +465,26 @@ $OSVersion = (Get-CimInstance -class Win32_OperatingSystem).Version $MinOSVersion = "6.3" if ([version]$OSVersion -lt [version]$MinOSVersion){ - + Write-host "Error: Unsupported OS Version. NDES Requires 2012 R2 and above." -BackgroundColor Red Log-ScriptEvent $LogFilePath "Unsupported OS Version. NDES Requires 2012 R2 and above." NDES_Validation 3 - - } - + + } + else { - + Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "OS Version " -NoNewline write-host "$($OSVersion)" -NoNewline -ForegroundColor Cyan write-host " supported." Log-ScriptEvent $LogFilePath "Server is version $($OSVersion)" NDES_Validation 1 - + } #endregion ####################################################################### - + #region Checking NDES Service Account properties in Active Directory Write-host @@ -307,23 +499,23 @@ $ADUser = $NDESServiceAccount.split("\")[1] $ADUserProps = (Get-ADUser $ADUser -Properties SamAccountName,enabled,AccountExpirationDate,accountExpires,accountlockouttime,PasswordExpired,PasswordLastSet,PasswordNeverExpires,LockedOut) if ($ADUserProps.enabled -ne $TRUE -OR $ADUserProps.PasswordExpired -ne $false -OR $ADUserProps.LockedOut -eq $TRUE){ - + Write-Host "Error: Problem with the AD account. Please see output below to determine the issue" -BackgroundColor Red Write-Host Log-ScriptEvent $LogFilePath "Problem with the AD account. Please see output below to determine the issue" NDES_Validation 3 - + } - + else { Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "NDES Service Account seems to be in working order:" Log-ScriptEvent $LogFilePath "NDES Service Account seems to be in working order" NDES_Validation 1 - + } - + Get-ADUser $ADUser -Properties SamAccountName,enabled,AccountExpirationDate,accountExpires,accountlockouttime,PasswordExpired,PasswordLastSet,PasswordNeverExpires,LockedOut | fl SamAccountName,enabled,AccountExpirationDate,accountExpires,accountlockouttime,PasswordExpired,PasswordLastSet,PasswordNeverExpires,LockedOut #endregion @@ -334,16 +526,16 @@ Get-ADUser $ADUser -Properties SamAccountName,enabled,AccountExpirationDate,acco Write-host "`n.......................................................`n" Write-host "Checking if NDES server is the CA...`n" -ForegroundColor Yellow -Log-ScriptEvent $LogFilePath "Checking if NDES server is the CA" NDES_Validation 1 +Log-ScriptEvent $LogFilePath "Checking if NDES server is the CA" NDES_Validation 1 $hostname = ([System.Net.Dns]::GetHostByName(($env:computerName))).hostname $CARoleInstalled = (Get-WindowsFeature ADCS-Cert-Authority).InstallState -eq "Installed" if ($hostname -match $IssuingCAServerFQDN){ - + Write-host "Error: NDES is running on the CA. This is an unsupported configuration!" -BackgroundColor Red Log-ScriptEvent $LogFilePath "NDES is running on the CA" NDES_Validation 3 - + } elseif($CARoleInstalled) { @@ -354,8 +546,8 @@ $CARoleInstalled = (Get-WindowsFeature ADCS-Cert-Authority).InstallState -eq "In Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "NDES server is not running on the CA" - Log-ScriptEvent $LogFilePath "NDES server is not running on the CA" NDES_Validation 1 - + Log-ScriptEvent $LogFilePath "NDES server is not running on the CA" NDES_Validation 1 + } #endregion @@ -369,14 +561,14 @@ Write-host "......................................................." Write-host Write-host "Checking NDES Service Account local permissions..." -ForegroundColor Yellow Write-host -Log-ScriptEvent $LogFilePath "Checking NDES Service Account local permissions" NDES_Validation 1 +Log-ScriptEvent $LogFilePath "Checking NDES Service Account local permissions" NDES_Validation 1 if ((net localgroup) -match "Administrators"){ $LocalAdminsMember = ((net localgroup Administrators)) if ($LocalAdminsMember -like "*$NDESServiceAccount*"){ - + Write-Warning "NDES Service Account is a member of the local Administrators group. This will provide the requisite rights but is _not_ a secure configuration. Use IIS_IUSERS instead." Log-ScriptEvent $LogFilePath "NDES Service Account is a member of the local Administrators group. This will provide the requisite rights but is _not_ a secure configuration. Use IIS_IUSERS instead." NDES_Validation 2 @@ -387,7 +579,7 @@ Log-ScriptEvent $LogFilePath "Checking NDES Service Account local permissions" N Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "NDES Service account is not a member of the Local Administrators group" Log-ScriptEvent $LogFilePath "NDES Service account is not a member of the Local Administrators group" NDES_Validation 1 - + } Write-host @@ -403,13 +595,13 @@ Log-ScriptEvent $LogFilePath "Checking NDES Service Account local permissions" N Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "NDES Service Account is a member of the local IIS_IUSR group" -NoNewline Log-ScriptEvent $LogFilePath "NDES Service Account is a member of the local IIS_IUSR group" NDES_Validation 1 - + } - + else { Write-Host "Error: NDES Service Account is not a member of the local IIS_IUSR group" -BackgroundColor red - Log-ScriptEvent $LogFilePath "NDES Service Account is not a member of the local IIS_IUSR group" NDES_Validation 3 + Log-ScriptEvent $LogFilePath "NDES Service Account is not a member of the local IIS_IUSR group" NDES_Validation 3 Write-host Write-host "Checking Local Security Policy for explicit rights via gpedit..." -ForegroundColor Yellow @@ -418,11 +610,11 @@ Log-ScriptEvent $LogFilePath "Checking NDES Service Account local permissions" N & "secedit" "/export" "/cfg" "$TempFile" | Out-Null $LocalSecPol = Get-Content $TempFile $ADUserProps = Get-ADUser $ADUser - $NDESSVCAccountSID = $ADUserProps.SID.Value + $NDESSVCAccountSID = $ADUserProps.SID.Value $LocalSecPolResults = $LocalSecPol | Select-String $NDESSVCAccountSID if ($LocalSecPolResults -match "SeInteractiveLogonRight" -AND $LocalSecPolResults -match "SeBatchLogonRight" -AND $LocalSecPolResults -match "SeServiceLogonRight"){ - + Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "NDES Service Account has been assigned the Logon Locally, Logon as a Service and Logon as a batch job rights explicitly." Log-ScriptEvent $LogFilePath "NDES Service Account has been assigned the Logon Locally, Logon as a Service and Logon as a batch job rights explicitly." NDES_Validation 1 @@ -433,29 +625,29 @@ Log-ScriptEvent $LogFilePath "Checking NDES Service Account local permissions" N Write-Host "Note:" -BackgroundColor Red -NoNewline Write-Host 'Consider using the IIS_IUSERS group instead of explicit rights as documented under "Step 1 - Create an NDES service account".' write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" - + } - + else { - Write-Host "Error: NDES Service Account has _NOT_ been assigned the Logon Locally, Logon as a Service or Logon as a batch job rights _explicitly_." -BackgroundColor red - Write-Host 'Please review "Step 1 - Create an NDES service account".' + Write-Host "Error: NDES Service Account has _NOT_ been assigned the Logon Locally, Logon as a Service or Logon as a batch job rights _explicitly_." -BackgroundColor red + Write-Host 'Please review "Step 1 - Create an NDES service account".' write-host "https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" Log-ScriptEvent $LogFilePath "NDES Service Account has _NOT_ been assigned the Logon Locally, Logon as a Service or Logon as a batch job rights _explicitly_." NDES_Validation 3 - + } - + } } else { - Write-Host "Error: No IIS_IUSRS group exists. Ensure IIS is installed." -BackgroundColor red - write-host 'Please review "Step 3.1 - Configure prerequisites on the NDES server".' + Write-Host "Error: No IIS_IUSRS group exists. Ensure IIS is installed." -BackgroundColor red + write-host 'Please review "Step 3.1 - Configure prerequisites on the NDES server".' write-host "https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" Log-ScriptEvent $LogFilePath "No IIS_IUSRS group exists. Ensure IIS is installed." NDES_Validation 3 - + } } @@ -464,7 +656,7 @@ Log-ScriptEvent $LogFilePath "Checking NDES Service Account local permissions" N Write-Warning "No local Administrators group exists, likely due to this being a Domain Controller. It is not recommended to run NDES on a Domain Controller." Log-ScriptEvent $LogFilePath "No local Administrators group exists, likely due to this being a Domain Controller. It is not recommended to run NDES on a Domain Controller." NDES_Validation 2 - + } #endregion @@ -489,20 +681,20 @@ $Feature = Get-WindowsFeature $WindowsFeature $FeatureDisplayName = $Feature.displayName if($Feature.installed){ - + Write-host "Success:" -ForegroundColor Green -NoNewline write-host "$FeatureDisplayName Feature Installed" Log-ScriptEvent $LogFilePath "$($FeatureDisplayName) Feature Installed" NDES_Validation 1 - + } else { - Write-Host "Error: $FeatureDisplayName Feature not installed!" -BackgroundColor red - Write-Host 'Please review "Step 3.1b - Configure prerequisites on the NDES server".' + Write-Host "Error: $FeatureDisplayName Feature not installed!" -BackgroundColor red + Write-Host 'Please review "Step 3.1b - Configure prerequisites on the NDES server".' write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" Log-ScriptEvent $LogFilePath "$($FeatureDisplayName) Feature not installed" NDES_Validation 3 - + } } @@ -541,7 +733,7 @@ Where-Object {$_.message -match "Install-AdcsNetworkDeviceEnrollmentService"}| S Write-Host "Error: Incorrect CSP selected during install. NDES only supports the CryptoAPI CSP." -BackgroundColor red Write-Host Write-Host $InstallParams.Message - Log-ScriptEvent $LogFilePath "Error: Incorrect CSP selected during install. NDES only supports the CryptoAPI CSP" NDES_Validation 3 + Log-ScriptEvent $LogFilePath "Error: Incorrect CSP selected during install. NDES only supports the CryptoAPI CSP" NDES_Validation 3 Log-ScriptEvent $LogFilePath "$($InstallParams.Message)" NDES_Eventvwr 3 } @@ -562,60 +754,60 @@ Log-ScriptEvent $LogFilePath "Checking IIS Application Pool health" NDES_Validat if (-not ($IISNotInstalled -eq $TRUE)){ - # If SCEP AppPool Exists + # If SCEP AppPool Exists if (Test-Path 'IIS:\AppPools\SCEP'){ $IISSCEPAppPoolAccount = Get-Item 'IIS:\AppPools\SCEP' | select -expandproperty processmodel | select -Expand username - + if ((Get-WebAppPoolState "SCEP").value -match "Started"){ - + $SCEPAppPoolRunning = $TRUE - + } } else { - Write-Host "Error: SCEP Application Pool missing!" -BackgroundColor red - Write-Host 'Please review "Step 3.1 - Configure prerequisites on the NDES server"'. - write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" + Write-Host "Error: SCEP Application Pool missing!" -BackgroundColor red + Write-Host 'Please review "Step 3.1 - Configure prerequisites on the NDES server"'. + write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" Log-ScriptEvent $LogFilePath "SCEP Application Pool missing" NDES_Validation 3 - + } - + if ($IISSCEPAppPoolAccount -contains "$NDESServiceAccount"){ - + Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "Application Pool is configured to use " -NoNewline Write-Host "$($IISSCEPAppPoolAccount)" Log-ScriptEvent $LogFilePath "Application Pool is configured to use $($IISSCEPAppPoolAccount)" NDES_Validation 1 - + } - + else { - Write-Host "Error: Application Pool is not configured to use the NDES Service Account" -BackgroundColor red - Write-Host 'Please review "Step 4.1 - Configure NDES for use with Intune".' - write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" + Write-Host "Error: Application Pool is not configured to use the NDES Service Account" -BackgroundColor red + Write-Host 'Please review "Step 4.1 - Configure NDES for use with Intune".' + write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" Log-ScriptEvent $LogFilePath "Application Pool is not configured to use the NDES Service Account" NDES_Validation 3 - + } - + if ($SCEPAppPoolRunning){ - + Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "SCEP Application Pool is Started " -NoNewline Log-ScriptEvent $LogFilePath "SCEP Application Pool is Started" NDES_Validation 1 - + } - + else { - Write-Host "Error: SCEP Application Pool is stopped!" -BackgroundColor red + Write-Host "Error: SCEP Application Pool is stopped!" -BackgroundColor red Write-Host "Please start the SCEP Application Pool via IIS Management Console. You should also review the Application Event log output for Errors" Log-ScriptEvent $LogFilePath "SCEP Application Pool is stopped" NDES_Validation 3 - + } } @@ -623,7 +815,7 @@ Log-ScriptEvent $LogFilePath "Checking IIS Application Pool health" NDES_Validat else { Write-Host "IIS is not installed." -BackgroundColor red - Log-ScriptEvent $LogFilePath "SCEP Application Pool is stopped" NDES_Validation 3 + Log-ScriptEvent $LogFilePath "SCEP Application Pool is stopped" NDES_Validation 3 } @@ -646,36 +838,36 @@ Log-ScriptEvent $LogFilePath "Checking Request Filtering" NDES_Validation 1 [xml]$RequestFiltering = (c:\windows\system32\inetsrv\appcmd.exe list config "default web site" /section:requestfiltering) if ($RequestFiltering.'system.webserver'.security.requestFiltering.requestLimits.maxQueryString -eq "65534"){ - + Write-Host "Success: " -ForegroundColor Green -NoNewline write-host "MaxQueryString Set Correctly" - Log-ScriptEvent $LogFilePath "MaxQueryString Set Correctly" NDES_Validation 1 - + Log-ScriptEvent $LogFilePath "MaxQueryString Set Correctly" NDES_Validation 1 + } - + else { - Write-Host "MaxQueryString not set correctly!" -BackgroundColor red + Write-Host "MaxQueryString not set correctly!" -BackgroundColor red Write-Host 'Please review "Step 4.4 - Configure NDES for use with Intune".' write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" Log-ScriptEvent $LogFilePath "MaxQueryString not set correctly" NDES_Validation 3 - + } if ($RequestFiltering.'system.webserver'.security.requestFiltering.requestLimits.maxUrl -eq "65534"){ - + Write-Host "Success: " -ForegroundColor Green -NoNewline write-host "MaxUrl Set Correctly" Log-ScriptEvent $LogFilePath "MaxUrl Set Correctly" NDES_Validation 1 - + } else { - - Write-Host "maxUrl not set correctly!" -BackgroundColor red + + Write-Host "maxUrl not set correctly!" -BackgroundColor red Write-Host 'Please review "Step 4.4 - Configure NDES for use with Intune".' write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure'" - Log-ScriptEvent $LogFilePath "maxUrl not set correctly" NDES_Validation 3 + Log-ScriptEvent $LogFilePath "maxUrl not set correctly" NDES_Validation 3 } @@ -684,7 +876,7 @@ Log-ScriptEvent $LogFilePath "Checking Request Filtering" NDES_Validation 1 else { Write-Host "IIS is not installed." -BackgroundColor red - Log-ScriptEvent $LogFilePath "IIS is not installed" NDES_Validation 3 + Log-ScriptEvent $LogFilePath "IIS is not installed" NDES_Validation 3 } @@ -706,36 +898,36 @@ Log-ScriptEvent $LogFilePath "Checking registry (HKLM:SYSTEM\CurrentControlSet\S If ((Get-ItemProperty -Path HKLM:SYSTEM\CurrentControlSet\Services\HTTP\Parameters -Name MaxFieldLength).MaxfieldLength -notmatch "65534"){ Write-Host "Error: MaxFieldLength not set to 65534 in the registry!" -BackgroundColor red - Write-Host + Write-Host Write-Host 'Please review "Step 4.3 - Configure NDES for use with Intune".' write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" Log-ScriptEvent $LogFilePath "MaxFieldLength not set to 65534 in the registry" NDES_Validation 3 - } + } else { Write-Host "Success: " -ForegroundColor Green -NoNewline write-host "MaxFieldLength set correctly" Log-ScriptEvent $LogFilePath "MaxFieldLength set correctly" NDES_Validation 1 - + } - + if ((Get-ItemProperty -Path HKLM:SYSTEM\CurrentControlSet\Services\HTTP\Parameters -Name MaxRequestBytes).MaxRequestBytes -notmatch "65534"){ Write-Host "MaxRequestBytes not set to 65534 in the registry!" -BackgroundColor red - Write-Host + Write-Host Write-Host 'Please review "Step 4.3 - Configure NDES for use with Intune".' write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure'" - Log-ScriptEvent $LogFilePath "MaxRequestBytes not set to 65534 in the registry" NDES_Validation 3 + Log-ScriptEvent $LogFilePath "MaxRequestBytes not set to 65534 in the registry" NDES_Validation 3 } - + else { Write-Host "Success: " -ForegroundColor Green -NoNewline write-host "MaxRequestBytes set correctly" Log-ScriptEvent $LogFilePath "MaxRequestBytes set correctly" NDES_Validation 1 - + } } @@ -765,22 +957,22 @@ $hostname = ([System.Net.Dns]::GetHostByName(($env:computerName))).hostname $spn = setspn.exe -L $ADUser if ($spn -match $hostname){ - + Write-Host "Success: " -ForegroundColor Green -NoNewline write-host "Correct SPN set for the NDES service account:" Write-host Write-Host $spn -ForegroundColor Cyan Log-ScriptEvent $LogFilePath "Correct SPN set for the NDES service account: $($spn)" NDES_Validation 1 - + } - + else { - Write-Host "Error: Missing or Incorrect SPN set for the NDES Service Account!" -BackgroundColor red + Write-Host "Error: Missing or Incorrect SPN set for the NDES Service Account!" -BackgroundColor red Write-Host 'Please review "Step 3.1c - Configure prerequisites on the NDES server".' write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" - Log-ScriptEvent $LogFilePath "Missing or Incorrect SPN set for the NDES Service Account" NDES_Validation 3 - + Log-ScriptEvent $LogFilePath "Missing or Incorrect SPN set for the NDES Service Account" NDES_Validation 3 + } #endregion @@ -788,7 +980,7 @@ $spn = setspn.exe -L $ADUser ################################################################# #region Checking there are no intermediate certs are in the Trusted Root store - + Write-host Write-host "......................................................." Write-host @@ -799,21 +991,21 @@ Log-ScriptEvent $LogFilePath "Checking there are no intermediate certs are in th $IntermediateCertCheck = Get-Childitem cert:\LocalMachine\root -Recurse | Where-Object {$_.Issuer -ne $_.Subject} if ($IntermediateCertCheck){ - - Write-Host "Error: Intermediate certificate found in the Trusted Root store. This can cause undesired effects and should be removed." -BackgroundColor red + + Write-Host "Error: Intermediate certificate found in the Trusted Root store. This can cause undesired effects and should be removed." -BackgroundColor red Write-Host "Certificates:" - Write-Host + Write-Host Write-Host $IntermediateCertCheck Log-ScriptEvent $LogFilePath "Intermediate certificate found in the Trusted Root store: $($IntermediateCertCheck)" NDES_Validation 3 - + } - + else { Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "Trusted Root store does not contain any Intermediate certificates." Log-ScriptEvent $LogFilePath "Trusted Root store does not contain any Intermediate certificates." NDES_Validation 1 - + } #endregion @@ -835,66 +1027,66 @@ $certs = Get-ChildItem cert:\LocalMachine\My\ # Looping through all certificates in LocalMachine Store Foreach ($item in $certs){ - + $Output = ($item.Extensions| where-object {$_.oid.FriendlyName -like "**"}).format(0).split(",") if ($Output -match "EnrollmentAgentOffline"){ - + $EnrollmentAgentOffline = $TRUE - + } - + if ($Output -match "CEPEncryption"){ - + $CEPEncryption = $TRUE - + } - } - + } + # Checking if EnrollmentAgentOffline certificate is present if ($EnrollmentAgentOffline){ - + Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "EnrollmentAgentOffline certificate is present" Log-ScriptEvent $LogFilePath "EnrollmentAgentOffline certificate is present" NDES_Validation 1 - + } - + else { - Write-Host "Error: EnrollmentAgentOffline certificate is not present!" -BackgroundColor red - Write-Host "This can take place when an account without Enterprise Admin permissions installs NDES. You may need to remove the NDES role and reinstall with the correct permissions." - write-host 'Please review "Step 3.1 - Configure prerequisites on the NDES server".' + Write-Host "Error: EnrollmentAgentOffline certificate is not present!" -BackgroundColor red + Write-Host "This can take place when an account without Enterprise Admin permissions installs NDES. You may need to remove the NDES role and reinstall with the correct permissions." + write-host 'Please review "Step 3.1 - Configure prerequisites on the NDES server".' write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" - Log-ScriptEvent $LogFilePath "EnrollmentAgentOffline certificate is not present" NDES_Validation 3 - + Log-ScriptEvent $LogFilePath "EnrollmentAgentOffline certificate is not present" NDES_Validation 3 + } - + # Checking if CEPEncryption is present if ($CEPEncryption){ - + Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "CEPEncryption certificate is present" Log-ScriptEvent $LogFilePath "CEPEncryption certificate is present" NDES_Validation 1 - + } - + else { - Write-Host "Error: CEPEncryption certificate is not present!" -BackgroundColor red - Write-Host "This can take place when an account without Enterprise Admin permissions installs NDES. You may need to remove the NDES role and reinstall with the correct permissions." - write-host 'Please review "Step 3.1 - Configure prerequisites on the NDES server".' + Write-Host "Error: CEPEncryption certificate is not present!" -BackgroundColor red + Write-Host "This can take place when an account without Enterprise Admin permissions installs NDES. You may need to remove the NDES role and reinstall with the correct permissions." + write-host 'Please review "Step 3.1 - Configure prerequisites on the NDES server".' write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" Log-ScriptEvent $LogFilePath "CEPEncryption certificate is not present" NDES_Validation 3 - + } $ErrorActionPreference = "Continue" #endregion -################################################################# +################################################################# #region Checking registry has been set with the SCEP certificate template name @@ -910,7 +1102,7 @@ Log-ScriptEvent $LogFilePath "Checking registry (HKLM:SOFTWARE\Microsoft\Cryptog Write-host "Error: Registry key does not exist. This can occur if the NDES role has been installed but not configured." -BackgroundColor Red Write-host 'Please review "Step 3 - Configure prerequisites on the NDES server".' write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" - Log-ScriptEvent $LogFilePath "MSCEP Registry key does not exist." NDES_Validation 3 + Log-ScriptEvent $LogFilePath "MSCEP Registry key does not exist." NDES_Validation 3 } @@ -918,27 +1110,27 @@ Log-ScriptEvent $LogFilePath "Checking registry (HKLM:SOFTWARE\Microsoft\Cryptog $SignatureTemplate = (Get-ItemProperty -Path HKLM:SOFTWARE\Microsoft\Cryptography\MSCEP\ -Name SignatureTemplate).SignatureTemplate $EncryptionTemplate = (Get-ItemProperty -Path HKLM:SOFTWARE\Microsoft\Cryptography\MSCEP\ -Name EncryptionTemplate).EncryptionTemplate - $GeneralPurposeTemplate = (Get-ItemProperty -Path HKLM:SOFTWARE\Microsoft\Cryptography\MSCEP\ -Name GeneralPurposeTemplate).GeneralPurposeTemplate + $GeneralPurposeTemplate = (Get-ItemProperty -Path HKLM:SOFTWARE\Microsoft\Cryptography\MSCEP\ -Name GeneralPurposeTemplate).GeneralPurposeTemplate $DefaultUsageTemplate = "IPSECIntermediateOffline" if ($SignatureTemplate -match $DefaultUsageTemplate -AND $EncryptionTemplate -match $DefaultUsageTemplate -AND $GeneralPurposeTemplate -match $DefaultUsageTemplate){ - + Write-Host "Error: Registry has not been configured with the SCEP Certificate template name. Default values have _not_ been changed." -BackgroundColor red - write-host 'Please review "Step 3.1 - Configure prerequisites on the NDES server".' + write-host 'Please review "Step 3.1 - Configure prerequisites on the NDES server".' write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" Write-Host Log-ScriptEvent $LogFilePath "Registry has not been configured with the SCEP Certificate template name. Default values have _not_ been changed." NDES_Validation 3 $FurtherReading = $FALSE - + } else { Write-Host "One or more default values have been changed." - Write-Host + Write-Host write-host "Checking SignatureTemplate key..." Write-host - + if ($SignatureTemplate -match $SCEPUserCertTemplate){ Write-Host "Success: " -ForegroundColor Green -NoNewline @@ -949,7 +1141,7 @@ Log-ScriptEvent $LogFilePath "Checking registry (HKLM:SOFTWARE\Microsoft\Cryptog } else { - + Write-Warning '"SignatureTemplate key does not match the SCEP certificate template name. Unless your template is explicitly set for the "Signature" purpose, this can safely be ignored."' Write-Host write-host "Registry value: " -NoNewline @@ -959,24 +1151,24 @@ Log-ScriptEvent $LogFilePath "Checking registry (HKLM:SOFTWARE\Microsoft\Cryptog Write-host "$($SCEPUserCertTemplate)" -ForegroundColor Cyan Write-Host Log-ScriptEvent $LogFilePath "SignatureTemplate key does not match the SCEP certificate template name.Registry value=$($SignatureTemplate)|SCEP certificate template value=$($SCEPUserCertTemplate)" NDES_Validation 2 - + } - + Write-host "......................." Write-Host Write-Host "Checking EncryptionTemplate key..." Write-host if ($EncryptionTemplate -match $SCEPUserCertTemplate){ - + Write-Host "Success: " -ForegroundColor Green -NoNewline write-host "SCEP certificate template '$($SCEPUserCertTemplate)' has been written to the registry under the _EncryptionTemplate_ key. Ensure this aligns with the usage specificed on the SCEP template." Write-host Log-ScriptEvent $LogFilePath "SCEP certificate template $($SCEPUserCertTemplate) has been written to the registry under the _EncryptionTemplate_ key" NDES_Validation 1 - + } - + else { Write-Warning '"EncryptionTemplate key does not match the SCEP certificate template name. Unless your template is explicitly set for the "Encryption" purpose, this can safely be ignored."' @@ -989,22 +1181,22 @@ Log-ScriptEvent $LogFilePath "Checking registry (HKLM:SOFTWARE\Microsoft\Cryptog Write-Host Log-ScriptEvent $LogFilePath "EncryptionTemplate key does not match the SCEP certificate template name.Registry value=$($EncryptionTemplate)|SCEP certificate template value=$($SCEPUserCertTemplate)" NDES_Validation 2 - + } - + Write-host "......................." Write-Host Write-Host "Checking GeneralPurposeTemplate key..." Write-host if ($GeneralPurposeTemplate -match $SCEPUserCertTemplate){ - + Write-Host "Success: " -ForegroundColor Green -NoNewline write-host "SCEP certificate template '$($SCEPUserCertTemplate)' has been written to the registry under the _GeneralPurposeTemplate_ key. Ensure this aligns with the usage specificed on the SCEP template" Log-ScriptEvent $LogFilePath "SCEP certificate template $($SCEPUserCertTemplate) has been written to the registry under the _GeneralPurposeTemplate_ key" NDES_Validation 1 } - + else { Write-Warning '"GeneralPurposeTemplate key does not match the SCEP certificate template name. Unless your template is set for the "Signature and Encryption" (General) purpose, this can safely be ignored."' @@ -1017,13 +1209,13 @@ Log-ScriptEvent $LogFilePath "Checking registry (HKLM:SOFTWARE\Microsoft\Cryptog Write-Host Log-ScriptEvent $LogFilePath "GeneralPurposeTemplate key does not match the SCEP certificate template name.Registry value=$($GeneralPurposeTemplate)|SCEP certificate template value=$($SCEPUserCertTemplate)" NDES_Validation 2 - + } } if ($furtherreading-EQ $true){ - + Write-host "......................." Write-Host Write-host 'For further reading, please review "Step 4.2 - Configure NDES for use with Intune".' @@ -1032,7 +1224,7 @@ Log-ScriptEvent $LogFilePath "Checking registry (HKLM:SOFTWARE\Microsoft\Cryptog } } - + $ErrorActionPreference = "Continue" #endregion @@ -1052,8 +1244,8 @@ $hostname = ([System.Net.Dns]::GetHostByName(($env:computerName))).hostname $serverAuthEKU = "1.3.6.1.5.5.7.3.1" # Server Authentication $allSSLCerts = Get-ChildItem Cert:\LocalMachine\My $BoundServerCert = netsh http show sslcert - - foreach ($Cert in $allSSLCerts) { + + foreach ($Cert in $allSSLCerts) { $ServerCertThumb = $cert.Thumbprint @@ -1074,9 +1266,9 @@ $ServerCertObject = Get-ChildItem Cert:\LocalMachine\My\$BoundServerCertThumb } else { - + $SelfSigned = $false - + } if ($ServerCertObject.EnhancedKeyUsageList -match $serverAuthEKU -AND (($ServerCertObject.Subject -match $hostname) -or ($ServerCertObject.DnsNameList -match $hostname)) -AND $ServerCertObject.Issuer -notmatch $ServerCertObject.Subject){ @@ -1101,57 +1293,57 @@ $ServerCertObject = Get-ChildItem Cert:\LocalMachine\My\$BoundServerCertThumb Log-ScriptEvent $LogFilePath "Certificate bound in IIS is valid. Subject:$($ServerCertObject.Subject)|Thumbprint:$($ServerCertObject.Thumbprint)|ValidUntil:$($ServerCertObject.NotAfter)|Internal&ExternalHostnames:$($DNSNameList)" NDES_Validation 1 } - + else { - Write-Host "Error: The certificate bound in IIS is not valid for use. Reason:" -BackgroundColor red + Write-Host "Error: The certificate bound in IIS is not valid for use. Reason:" -BackgroundColor red write-host - + if ($ServerCertObject.EnhancedKeyUsageList -match $serverAuthEKU) { - + $EKUValid = $true } else { - + $EKUValid = $false write-host "Correct EKU: " -NoNewline Write-Host "$($EKUValid)" -ForegroundColor Cyan Write-Host - + } if ($ServerCertObject.Subject -match $hostname) { - + $SubjectValid = $true } else { - + $SubjectValid = $false write-host "Correct Subject: " -NoNewline write-host "$($SubjectValid)" -ForegroundColor Cyan Write-Host - + } if ($SelfSigned -eq $false){ - + Out-Null - + } else { - + write-host "Is Self-Signed: " -NoNewline write-host "$($SelfSigned)" -ForegroundColor Cyan Write-Host - + } Write-Host 'Please review "Step 4 - Configure NDES for use with Intune>To Install and bind certificates on the NDES Server".' @@ -1159,7 +1351,7 @@ $ServerCertObject = Get-ChildItem Cert:\LocalMachine\My\$BoundServerCertThumb Log-ScriptEvent $LogFilePath "The certificate bound in IIS is not valid for use. CorrectEKU=$($EKUValid)|CorrectSubject=$($SubjectValid)|IsSelfSigned=$($SelfSigned)" NDES_Validation 3 } - + #endregion ################################################################# @@ -1185,9 +1377,9 @@ $ClientCertObject = Get-ChildItem Cert:\LocalMachine\My\$NDESCertThumbprint } else { - + $ClientCertSelfSigned = $false - + } if ($ClientCertObject.EnhancedKeyUsageList -match $clientAuthEku -AND $ClientCertObject.Subject -match $hostname -AND $ClientCertObject.Issuer -notmatch $ClientCertObject.Subject){ @@ -1206,56 +1398,56 @@ $ClientCertObject = Get-ChildItem Cert:\LocalMachine\My\$NDESCertThumbprint Log-ScriptEvent $LogFilePath "Client certificate bound to NDES Connector is valid. Subject:$($ClientCertObject.Subject)|Thumbprint:$($ClientCertObject.Thumbprint)|ValidUntil:$($ClientCertObject.NotAfter)" NDES_Validation 1 } - + else { - Write-Host "Error: The certificate bound to the NDES Connector is not valid for use. Reason:" -BackgroundColor red - write-host + Write-Host "Error: The certificate bound to the NDES Connector is not valid for use. Reason:" -BackgroundColor red + write-host if ($ClientCertObject.EnhancedKeyUsageList -match $clientAuthEku) { - + $ClientCertEKUValid = $true } else { - + $ClientCertEKUValid = $false write-host "Correct EKU: " -NoNewline Write-Host "$($ClientCertEKUValid)" -ForegroundColor Cyan Write-Host - + } if ($ClientCertObject.Subject -match $hostname) { - + $ClientCertSubjectValid = $true } else { - + $ClientCertSubjectValid = $false write-host "Correct Subject: " -NoNewline write-host "$($ClientCertSubjectValid)" -ForegroundColor Cyan Write-Host - + } if ($ClientCertSelfSigned -eq $false){ - + Out-Null - + } else { - + write-host "Is Self-Signed: " -NoNewline write-host "$($ClientCertSelfSigned)" -ForegroundColor Cyan Write-Host - + } Write-Host 'Please review "Step 4 - Configure NDES for use with Intune>To Install and bind certificates on the NDES Server".' @@ -1264,7 +1456,7 @@ $ClientCertObject = Get-ChildItem Cert:\LocalMachine\My\$NDESCertThumbprint } - + #endregion ################################################################# @@ -1287,7 +1479,7 @@ $Statuscode = try {(Invoke-WebRequest -Uri https://$hostname/certsrv/mscep/mscep Write-host "Error: https://$hostname/certsrv/mscep/mscep.dll returns 200 OK. This usually signifies an error with the Intune Connector registering itself or not being installed." -BackgroundColor Red Log-ScriptEvent $LogFilePath "https://$hostname/certsrv/mscep/mscep.dll returns 200 OK. This usually signifies an error with the Intune Connector registering itself or not being installed" NDES_Validation 3 - } + } elseif ($statuscode -eq "403"){ @@ -1308,19 +1500,19 @@ $Statuscode = try {(Invoke-WebRequest -Uri https://$hostname/certsrv/mscep/mscep Write-Host write-host $CACaps Log-ScriptEvent $LogFilePath "CA Capabilities retrieved:$CACaps" NDES_Validation 1 - + } } - + else { - + Write-host "Error: Unexpected Error code! This usually signifies an error with the Intune Connector registering itself or not being installed" -BackgroundColor Red Write-host "Expected value is a 403. We received a $($Statuscode). This could be down to a missing reboot post policy module install. Verify last boot time and module install time further down the validation." Log-ScriptEvent $LogFilePath "Unexpected Error code. Expected:403|Received:$Statuscode" NDES_Validation 3 - + } - + #endregion ################################################################# @@ -1353,13 +1545,13 @@ Write-host "......................................................." Write-host Write-Host "Checking Intune Connector is installed..." -ForegroundColor Yellow Write-host -Log-ScriptEvent $LogFilePath "Checking Intune Connector is installed" NDES_Validation 1 +Log-ScriptEvent $LogFilePath "Checking Intune Connector is installed" NDES_Validation 1 if ($IntuneConnector = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | ? {$_.DisplayName -eq "Microsoft Intune Connector"}){ Write-Host "Success: " -ForegroundColor Green -NoNewline - Write-Host "$($IntuneConnector.DisplayName) was installed on " -NoNewline - Write-Host "$($IntuneConnector.InstallDate) " -ForegroundColor Cyan -NoNewline + Write-Host "$($IntuneConnector.DisplayName) was installed on " -NoNewline + Write-Host "$($IntuneConnector.InstallDate) " -ForegroundColor Cyan -NoNewline write-host "and is version " -NoNewline Write-Host "$($IntuneConnector.DisplayVersion)" -ForegroundColor Cyan -NoNewline Write-host @@ -1369,12 +1561,12 @@ Log-ScriptEvent $LogFilePath "Checking Intune Connector is installed" NDES_Valid else { - Write-Host "Error: Intune Connector not installed" -BackgroundColor red + Write-Host "Error: Intune Connector not installed" -BackgroundColor red Write-Host 'Please review "Step 5 - Enable, install, and configure the Intune certificate connector".' write-host "URL: https://docs.microsoft.com/en-us/intune/certificates-scep-configure#configure-your-infrastructure" Write-Host - Log-ScriptEvent $LogFilePath "ConnectorNotInstalled" NDES_Validation 3 - + Log-ScriptEvent $LogFilePath "ConnectorNotInstalled" NDES_Validation 3 + } @@ -1400,7 +1592,7 @@ $SigningCertificate = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MicrosoftIntune\NDE Write-host "Error: KeyRecoveryAgentCertificate Registry key does not exist." -BackgroundColor Red Write-Host - Log-ScriptEvent $LogFilePath "KeyRecoveryAgentCertificate Registry key does not exist." NDES_Validation 3 + Log-ScriptEvent $LogFilePath "KeyRecoveryAgentCertificate Registry key does not exist." NDES_Validation 3 } @@ -1409,14 +1601,14 @@ $SigningCertificate = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MicrosoftIntune\NDE $KeyRecoveryAgentCertificatePresent = (Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\MicrosoftIntune\NDESConnector\ -Name KeyRecoveryAgentCertificate).KeyRecoveryAgentCertificate if (-not ($KeyRecoveryAgentCertificatePresent)) { - + Write-Warning "KeyRecoveryAgentCertificate registry key exists but has no value" Log-ScriptEvent $LogFilePath "KeyRecoveryAgentCertificate missing Value" NDES_Validation 2 } else { - + Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "KeyRecoveryAgentCertificate registry key exists" Log-ScriptEvent $LogFilePath "KeyRecoveryAgentCertificate registry key exists" NDES_Validation 1 @@ -1431,7 +1623,7 @@ $SigningCertificate = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MicrosoftIntune\NDE Write-host "Error: PfxSigningCertificate Registry key does not exist." -BackgroundColor Red Write-Host - Log-ScriptEvent $LogFilePath "PfxSigningCertificate Registry key does not exist." NDES_Validation 3 + Log-ScriptEvent $LogFilePath "PfxSigningCertificate Registry key does not exist." NDES_Validation 3 } @@ -1441,14 +1633,14 @@ $SigningCertificate = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MicrosoftIntune\NDE $PfxSigningCertificatePresent = (Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\MicrosoftIntune\NDESConnector\ -Name PfxSigningCertificate).PfxSigningCertificate if (-not ($PfxSigningCertificatePresent)) { - + Write-Warning "PfxSigningCertificate registry key exists but has no value" Log-ScriptEvent $LogFilePath "PfxSigningCertificate missing Value" NDES_Validation 2 } else { - + Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "PfxSigningCertificate registry keys exists" Log-ScriptEvent $LogFilePath "PfxSigningCertificate registry key exists" NDES_Validation 1 @@ -1463,7 +1655,7 @@ $SigningCertificate = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MicrosoftIntune\NDE Write-host "Error: SigningCertificate Registry key does not exist." -BackgroundColor Red Write-Host - Log-ScriptEvent $LogFilePath "SigningCertificate Registry key does not exist" NDES_Validation 3 + Log-ScriptEvent $LogFilePath "SigningCertificate Registry key does not exist" NDES_Validation 3 } @@ -1472,7 +1664,7 @@ $SigningCertificate = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MicrosoftIntune\NDE $SigningCertificatePresent = (Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\MicrosoftIntune\NDESConnector\ -Name SigningCertificate).SigningCertificate if (-not ($SigningCertificatePresent)) { - + Write-Warning "SigningCertificate registry key exists but has no value" Log-ScriptEvent $LogFilePath "SigningCertificate registry key exists but has no value" NDES_Validation 2 @@ -1480,7 +1672,7 @@ $SigningCertificate = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MicrosoftIntune\NDE } else { - + Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host "SigningCertificate registry key exists" Log-ScriptEvent $LogFilePath "SigningCertificate registry key exists" NDES_Validation 1 @@ -1535,7 +1727,7 @@ Log-ScriptEvent $LogFilePath "Checking Event logs for pertinent errors" NDES_Val $i++ } - + } if (-not (Get-EventLog -LogName "Application" -EntryType Error -Source NDESConnector,Microsoft-Windows-NetworkDeviceEnrollmentService -After $EventLogCollDays -ErrorAction silentlycontinue)) { @@ -1577,7 +1769,7 @@ Write-host Write-host "......................................................." Write-host Write-host "Log Files..." -ForegroundColor Yellow -Write-host +Write-host write-host "Do you want to gather troubleshooting files? This includes IIS, NDES Connector, NDES Plugin, CRP, and MSCEP log files, in addition to the SCEP template configuration. [Y]es, [N]o:" $LogFileCollectionConfirmation = Read-Host @@ -1664,7 +1856,7 @@ Write-Host } write-host "Ending script..." -ForegroundColor Yellow -Write-host +Write-host #endregion @@ -1684,3 +1876,4 @@ Write-Host exit } + diff --git a/CertificationAuthority/Validate-NDESUrl.ps1 b/CertificationAuthority/Validate-NDESUrl.ps1 index f562f48..298d407 100644 --- a/CertificationAuthority/Validate-NDESUrl.ps1 +++ b/CertificationAuthority/Validate-NDESUrl.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -21,14 +21,14 @@ Validate-NDESUrl [CmdletBinding(DefaultParameterSetName="NormalRun")] Param( - + [parameter(Mandatory=$true,ParameterSetName="NormalRun")] [alias("s")] [ValidateScript({ if (!($_.contains("/"))){ $True - + } else { @@ -41,16 +41,16 @@ Param( )] [string]$server, - + [parameter(Mandatory=$true,ParameterSetName="NormalRun")] [alias("q")] - [ValidateRange(1,31)] + [ValidateRange(1,31)] [INT]$querysize, [parameter(ParameterSetName="Help")] [alias("h","?","/?")] [switch]$help, - + [parameter(ParameterSetName="Help")] [alias("u")] [switch]$usage @@ -58,6 +58,198 @@ Param( ################################################################# + +function Connect-GraphAPI { +<# +.SYNOPSIS +Connects to Microsoft Graph API with appropriate scopes for Intune operations +.DESCRIPTION +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment +.NOTES +Requires Microsoft.Graph.Authentication module +#> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) + + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } + + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } + + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } + } + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false + } +} + +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, + + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', + + [Parameter(Mandatory = $false)] + [object]$Body = $null, + + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) + + try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } + + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } + + $results = @() + $nextLink = $Uri + + do { + Write-Verbose "Making request to: $nextLink" + + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } + + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } + + $response = Invoke-MgGraphRequest @requestParams + + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null + } + + } while ($nextLink) + + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" + } + else { + Write-Error "Graph API request failed: $errorMessage" + } + throw + } +} + +#################################################### + function Show-Usage{ Write-Host @@ -93,7 +285,7 @@ function Get-NDESURLHelp{ if($usage){ - Show-Usage + Show-Usage break } @@ -108,7 +300,7 @@ function Get-NDESURLHelp{ if ((Get-WmiObject -class Win32_OperatingSystem).ProductType -notlike "1"){ if (Test-Path HKLM:SOFTWARE\Microsoft\Cryptography\MSCEP) { - + Write-Host Write-Host "Error: This appears to be the NDES server. Please run this script from a different machine. An external (guest) connection is best." -BackgroundColor Red write-host "Exiting......................" @@ -141,7 +333,7 @@ Write-host Write-Host Write-Host "This state will _not_ provide a working NDES infrastructure, although validation of long URI support can continue." Write-Host - + } @@ -153,17 +345,17 @@ Write-host } else { - + Write-Warning "Unexpected Error code! This usually signifies an error with the Intune Connector registering itself or not being installed." Write-Host Write-host "Expected value is a 403. We received a $($BaseURLstatuscode). This state will _not_ provide a working NDES infrastructure, although we can proceed with the validation included in this test" - + } } else { - + write-host "Error: Cannot resolve $($server)" -BackgroundColor Red Write-Host Write-Host "Please ensure a DNS record is in place and name resolution is successful" @@ -171,7 +363,7 @@ Write-host Write-Host "Exiting................................................" Write-Host exit - + } @@ -209,7 +401,7 @@ $CACapsStatuscode = try {(Invoke-WebRequest -Uri $GetCACaps).statuscode} catch { Write-Host "$GetCACaps" -ForegroundColor Cyan Write-host - $CACaps = (Invoke-WebRequest -Uri $GetCACaps).content + $CACaps = (Invoke-WebRequest -Uri $GetCACaps).content if ($CACaps) { @@ -305,14 +497,14 @@ Write-Host $challengeBase = "NDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallengeNDESLongUrlValidatorFakeChallenge"; $testChallenge = $null - for ($i=1; $i -le $querySize; $i++){ + for ($i=1; $i -le $querySize; $i++){ $testChallenge += $challengeBase + ($i + 1) } $LongUrl = "$($NDESUrl)?operation=PKIOperation&message=$($testChallenge)" -$LongUrlStatusCode = try {(Invoke-WebRequest -Uri $LongUrl).statuscode} catch {$_.Exception.Response.StatusCode.Value__} +$LongUrlStatusCode = try {(Invoke-WebRequest -Uri $LongUrl).statuscode} catch {$_.Exception.Response.StatusCode.Value__} if ($LongUrlStatusCode -eq "414"){ @@ -353,6 +545,7 @@ Write-host Write-host "End of NDES URI validation" -ForegroundColor Yellow Write-Host write-host "Ending script..." -ForegroundColor Yellow -Write-host +Write-host #endregion + diff --git a/CheckStatus/Check_enrolledDateTime.ps1 b/CheckStatus/Check_enrolledDateTime.ps1 index d96f47b..54c8128 100644 --- a/CheckStatus/Check_enrolledDateTime.ps1 +++ b/CheckStatus/Check_enrolledDateTime.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-AADUser(){ +function Get-AADUser { <# .SYNOPSIS @@ -180,36 +232,36 @@ param # Defining Variables $graphApiVersion = "v1.0" $User_resource = "users" - + try { - - if($userPrincipalName -eq "" -or $userPrincipalName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + if("" -eq $userPrincipalName -or $null -eq $userPrincipalName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - - if($Property -eq "" -or $Property -eq $null){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName" + if("" -eq $Property -or $null -eq $Property){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName" Write-Verbose $uri - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } catch { @@ -233,50 +285,10 @@ $User_resource = "users" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -299,14 +311,14 @@ Write-Host try { - $uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$filter=enrolledDateTime ge $minutesago" + $uri = "beta/deviceManagement/managedDevices?`$filter=enrolledDateTime ge $minutesago" - $Devices = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | sort deviceName + $Devices = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | sort deviceName $Devices = $Devices | ? { $_.managementAgent -ne "eas" } # If there are devices not synced in the past 30 days script continues - + if($Devices){ $DeviceCount = @($Devices).count @@ -318,7 +330,7 @@ Write-Host Write-Host # Looping through all the devices returned - + foreach($Device in $Devices){ write-host "------------------------------------------------------------------" @@ -379,3 +391,4 @@ Write-Host break } + diff --git a/CheckStatus/Check_lastSyncDateTime.ps1 b/CheckStatus/Check_lastSyncDateTime.ps1 index ac94e1d..3aaaec5 100644 --- a/CheckStatus/Check_lastSyncDateTime.ps1 +++ b/CheckStatus/Check_lastSyncDateTime.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-AADUser(){ +function Get-AADUser { <# .SYNOPSIS @@ -180,36 +232,36 @@ param # Defining Variables $graphApiVersion = "v1.0" $User_resource = "users" - + try { - - if($userPrincipalName -eq "" -or $userPrincipalName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + if("" -eq $userPrincipalName -or $null -eq $userPrincipalName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - - if($Property -eq "" -or $Property -eq $null){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName" + if("" -eq $Property -or $null -eq $Property){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName" Write-Verbose $uri - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } catch { @@ -233,50 +285,10 @@ $User_resource = "users" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -295,12 +307,12 @@ Write-Host try { - $uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$filter=lastSyncDateTime le $daysago" + $uri = "beta/deviceManagement/managedDevices?`$filter=lastSyncDateTime le $daysago" - $Devices = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | sort deviceName + $Devices = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | sort deviceName # If there are devices not synced in the past 30 days script continues - + if($Devices){ Write-Host "There are" $Devices.count "devices that have not synced in the last $days days..." -ForegroundColor Red @@ -310,7 +322,7 @@ Write-Host Write-Host # Looping through all the devices returned - + foreach($Device in $Devices){ write-host "------------------------------------------------------------------" @@ -368,3 +380,4 @@ Write-Host break } + diff --git a/CheckStatus/DirectoryRoles_Get.ps1 b/CheckStatus/DirectoryRoles_Get.ps1 index 31d7508..c992447 100644 --- a/CheckStatus/DirectoryRoles_Get.ps1 +++ b/CheckStatus/DirectoryRoles_Get.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } - -} #################################################### -Function Get-DirectoryRoles(){ +function Get-DirectoryRoles { <# .SYNOPSIS @@ -178,34 +230,34 @@ param # Defining Variables $graphApiVersion = "v1.0" $Resource = "directoryRoles" - + try { - - if($RoleId -eq "" -or $RoleId -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + if("" -eq $RoleId -or $null -eq $RoleId){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - - if($Property -eq "" -or $Property -eq $null){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)/$RoleId" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + if("" -eq $Property -or $null -eq $Property){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)/$RoleId" + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)/$RoleId/$Property" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)/$RoleId/$Property" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } catch { @@ -229,50 +281,10 @@ $Resource = "directoryRoles" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -287,8 +299,8 @@ $Roles = (Get-DirectoryRoles | Select-Object displayName).displayName | Sort-Obj $menu = @{} -for ($i=1;$i -le $Roles.count; $i++) -{ Write-Host "$i. $($Roles[$i-1])" +for ($i=1;$i -le $Roles.count; $i++) +{ Write-Host "$i. $($Roles[$i-1])" $menu.Add($i,($Roles[$i-1]))} Write-Host @@ -319,7 +331,7 @@ $selection = $menu.Item($ans) } } - + else { Write-Host @@ -331,3 +343,4 @@ $selection = $menu.Item($ans) } Write-Host + diff --git a/CompanyPortalBranding/CompanyPortal_Export.ps1 b/CompanyPortalBranding/CompanyPortal_Export.ps1 index 1b24adb..a829678 100644 --- a/CompanyPortalBranding/CompanyPortal_Export.ps1 +++ b/CompanyPortalBranding/CompanyPortal_Export.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-IntuneBrand(){ +function Get-IntuneBrand { <# .SYNOPSIS @@ -173,8 +225,8 @@ $Resource = "deviceManagement/intuneBrand" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET } @@ -197,7 +249,7 @@ $Resource = "deviceManagement/intuneBrand" #################################################### -Function Export-JSONData(){ +function Export-JSONData { <# .SYNOPSIS @@ -220,7 +272,7 @@ $ExportPath try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON..." -f Red @@ -265,7 +317,7 @@ $ExportPath $JSON1 | Set-Content -LiteralPath "$ExportPath\$FileName_JSON" write-host "JSON created in $ExportPath\$FileName_JSON..." -f cyan - + } } @@ -282,50 +334,10 @@ $ExportPath #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -384,3 +396,4 @@ else { } + diff --git a/CompanyPortalBranding/CompanyPortal_Get.ps1 b/CompanyPortalBranding/CompanyPortal_Get.ps1 index 1f4f2c1..8f5b009 100644 --- a/CompanyPortalBranding/CompanyPortal_Get.ps1 +++ b/CompanyPortalBranding/CompanyPortal_Get.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-IntuneBrand(){ +function Get-IntuneBrand { <# .SYNOPSIS @@ -173,8 +225,8 @@ $Resource = "deviceManagement/intuneBrandingProfiles" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } @@ -199,50 +251,10 @@ $Resource = "deviceManagement/intuneBrandingProfiles" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -250,3 +262,4 @@ $global:authToken = Get-AuthToken -User $User #################################################### Get-IntuneBrand + diff --git a/CompanyPortalBranding/CompanyPortal_Import_FromJSON.ps1 b/CompanyPortalBranding/CompanyPortal_Import_FromJSON.ps1 index 59c1613..96d69e0 100644 --- a/CompanyPortalBranding/CompanyPortal_Import_FromJSON.ps1 +++ b/CompanyPortalBranding/CompanyPortal_Import_FromJSON.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -186,7 +238,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -196,7 +248,7 @@ $JSON #################################################### -Function Set-IntuneBrand(){ +function Set-IntuneBrand { <# .SYNOPSIS @@ -233,7 +285,7 @@ $App_resource = "deviceManagement" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Patch -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -261,50 +313,10 @@ $App_resource = "deviceManagement" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -345,3 +357,4 @@ $JSON_Output write-host Write-Host "Setting Company Portal Branding '$DisplayName'" -ForegroundColor Yellow Set-IntuneBrand -JSON $JSON_Output + diff --git a/CompanyPortalBranding/CompanyPortal_Set_Brand.ps1 b/CompanyPortalBranding/CompanyPortal_Set_Brand.ps1 index 275fd9f..18228d3 100644 --- a/CompanyPortalBranding/CompanyPortal_Set_Brand.ps1 +++ b/CompanyPortalBranding/CompanyPortal_Set_Brand.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Set-IntuneBrand(){ +function Set-IntuneBrand { <# .SYNOPSIS @@ -189,7 +241,7 @@ $App_resource = "deviceManagement" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Patch -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -215,7 +267,7 @@ $App_resource = "deviceManagement" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -262,50 +314,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -361,3 +373,4 @@ $JSON_Logo = @" #################################################### Set-IntuneBrand -JSON $JSON_Logo + diff --git a/CompanyPortalBranding/CompanyPortal_Set_Default.ps1 b/CompanyPortalBranding/CompanyPortal_Set_Default.ps1 index b46fc6d..d2caa1e 100644 --- a/CompanyPortalBranding/CompanyPortal_Set_Default.ps1 +++ b/CompanyPortalBranding/CompanyPortal_Set_Default.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-IntuneBrand(){ +function Get-IntuneBrand { <# .SYNOPSIS @@ -173,8 +225,8 @@ $Resource = "deviceManagement/intuneBrandingProfiles" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } @@ -197,7 +249,7 @@ $Resource = "deviceManagement/intuneBrandingProfiles" #################################################### -Function Set-IntuneBrand(){ +function Set-IntuneBrand { <# .SYNOPSIS @@ -235,7 +287,7 @@ $Resource = "deviceManagement/intuneBrandingProfiles('$id')" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $uri -Method Patch -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -261,7 +313,7 @@ $Resource = "deviceManagement/intuneBrandingProfiles('$id')" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -308,50 +360,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -385,3 +397,4 @@ $IntuneBrand = Get-IntuneBrand $id = $IntuneBrand.id Set-IntuneBrand -id $id -JSON $JSON_Default + diff --git a/CompliancePolicy/CompliancePolicy_Add.ps1 b/CompliancePolicy/CompliancePolicy_Add.ps1 index 593c890..0a62261 100644 --- a/CompliancePolicy/CompliancePolicy_Add.ps1 +++ b/CompliancePolicy/CompliancePolicy_Add.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -197,7 +249,7 @@ $JSON #################################################### -Function Add-DeviceCompliancePolicy(){ +function Add-DeviceCompliancePolicy { <# .SYNOPSIS @@ -223,7 +275,7 @@ $Resource = "deviceManagement/deviceCompliancePolicies" try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red @@ -233,8 +285,8 @@ $Resource = "deviceManagement/deviceCompliancePolicies" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -262,50 +314,10 @@ $Resource = "deviceManagement/deviceCompliancePolicies" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -371,3 +383,4 @@ Write-Host "Adding iOS Compliance Policy from JSON..." -ForegroundColor Yellow Write-Host Add-DeviceCompliancePolicy -JSON $JSON_iOS + diff --git a/CompliancePolicy/CompliancePolicy_Add_Assign.ps1 b/CompliancePolicy/CompliancePolicy_Add_Assign.ps1 index 22e42e8..5b28c5e 100644 --- a/CompliancePolicy/CompliancePolicy_Add_Assign.ps1 +++ b/CompliancePolicy/CompliancePolicy_Add_Assign.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -187,7 +239,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -197,7 +249,7 @@ $JSON #################################################### -Function Add-DeviceCompliancePolicy(){ +function Add-DeviceCompliancePolicy { <# .SYNOPSIS @@ -220,10 +272,10 @@ param $graphApiVersion = "v1.0" $Resource = "deviceManagement/deviceCompliancePolicies" - + try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red @@ -233,13 +285,13 @@ $Resource = "deviceManagement/deviceCompliancePolicies" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { Write-Host @@ -260,7 +312,7 @@ $Resource = "deviceManagement/deviceCompliancePolicies" #################################################### -Function Add-DeviceCompliancePolicyAssignment(){ +function Add-DeviceCompliancePolicyAssignment { <# .SYNOPSIS @@ -284,7 +336,7 @@ param $graphApiVersion = "v1.0" $Resource = "deviceManagement/deviceCompliancePolicies/$CompliancePolicyId/assign" - + try { if(!$CompliancePolicyId){ @@ -313,14 +365,14 @@ $JSON = @" } ] } - + "@ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } - + catch { $ex = $_.Exception @@ -340,7 +392,7 @@ $JSON = @" #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -366,37 +418,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ $GID = $Group.id @@ -404,13 +456,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } } @@ -436,50 +488,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -543,7 +555,7 @@ $AADGroup = Read-Host -Prompt "Enter the Azure AD Group name where policies will $TargetGroupId = (get-AADGroup -GroupName "$AADGroup").id - if($TargetGroupId -eq $null -or $TargetGroupId -eq ""){ + if($null -eq $TargetGroupId -or "" -eq $TargetGroupId){ Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red Write-Host @@ -583,3 +595,4 @@ $Assign_iOS = Add-DeviceCompliancePolicyAssignment -CompliancePolicyId $CreateRe Write-Host "Assigned '$AADGroup' to $($CreateResult_iOS.displayName)/$($CreateResult_iOS.id)" Write-Host + diff --git a/CompliancePolicy/CompliancePolicy_Export.ps1 b/CompliancePolicy/CompliancePolicy_Export.ps1 index b9892df..fe336d6 100644 --- a/CompliancePolicy/CompliancePolicy_Export.ps1 +++ b/CompliancePolicy/CompliancePolicy_Export.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-DeviceCompliancePolicy(){ +function Get-DeviceCompliancePolicy { <# .SYNOPSIS @@ -183,7 +234,7 @@ param $graphApiVersion = "Beta" $Resource = "deviceManagement/deviceCompliancePolicies" - + try { $Count_Params = 0 @@ -193,41 +244,41 @@ $Resource = "deviceManagement/deviceCompliancePolicies" if($Win10.IsPresent){ $Count_Params++ } if($Count_Params -gt 1){ - + write-host "Multiple parameters set, specify a single parameter -Android -iOS or -Win10 against the function" -f Red - + } - + elseif($Android){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("android") } - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("android") } + } - + elseif($iOS){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("ios") } - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("ios") } + } elseif($Win10){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") } - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") } + } - + else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + catch { $ex = $_.Exception @@ -247,7 +298,7 @@ $Resource = "deviceManagement/deviceCompliancePolicies" #################################################### -Function Export-JSONData(){ +function Export-JSONData { <# .SYNOPSIS @@ -270,7 +321,7 @@ $ExportPath try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON..." -f Red @@ -318,7 +369,7 @@ $ExportPath $JSON1 | Set-Content -LiteralPath "$ExportPath\$FileName_JSON" write-host "CSV created in $ExportPath\$FileName_CSV..." -f cyan write-host "JSON created in $ExportPath\$FileName_JSON..." -f cyan - + } } @@ -335,50 +386,10 @@ $ExportPath #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -427,3 +438,4 @@ $CPs = Get-DeviceCompliancePolicy Write-Host } + diff --git a/CompliancePolicy/CompliancePolicy_Get.ps1 b/CompliancePolicy/CompliancePolicy_Get.ps1 index 53fc338..c76ca9b 100644 --- a/CompliancePolicy/CompliancePolicy_Get.ps1 +++ b/CompliancePolicy/CompliancePolicy_Get.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-DeviceCompliancePolicy(){ +function Get-DeviceCompliancePolicy { <# .SYNOPSIS @@ -202,36 +253,36 @@ $Resource = "deviceManagement/deviceCompliancePolicies" elseif($Android){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("android") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("android") } } elseif($iOS){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("ios") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("ios") } } elseif($Win10){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") } } elseif($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -256,7 +307,7 @@ $Resource = "deviceManagement/deviceCompliancePolicies" #################################################### -Function Get-DeviceCompliancePolicyAssignment(){ +function Get-DeviceCompliancePolicyAssignment { <# .SYNOPSIS @@ -283,8 +334,8 @@ $DCP_resource = "deviceManagement/deviceCompliancePolicies" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)/$id/assignments" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)/$id/assignments" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -307,7 +358,7 @@ $DCP_resource = "deviceManagement/deviceCompliancePolicies" #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -338,15 +389,15 @@ $Group_resource = "groups" if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - elseif($GroupName -eq "" -or $GroupName -eq $null){ + elseif("" -eq $GroupName -or $null -eq $GroupName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -354,15 +405,15 @@ $Group_resource = "groups" if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } elseif($Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value if($Group){ @@ -371,8 +422,8 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -403,50 +454,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -491,3 +502,4 @@ $DCPA = Get-DeviceCompliancePolicyAssignment -id $id Write-Host } + diff --git a/CompliancePolicy/CompliancePolicy_Import_FromJSON.ps1 b/CompliancePolicy/CompliancePolicy_Import_FromJSON.ps1 index f552177..bb45a3f 100644 --- a/CompliancePolicy/CompliancePolicy_Import_FromJSON.ps1 +++ b/CompliancePolicy/CompliancePolicy_Import_FromJSON.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -187,7 +238,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -197,7 +248,7 @@ $JSON #################################################### -Function Add-DeviceCompliancePolicy(){ +function Add-DeviceCompliancePolicy { <# .SYNOPSIS @@ -220,10 +271,10 @@ param $graphApiVersion = "Beta" $Resource = "deviceManagement/deviceCompliancePolicies" - + try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the iOS Policy..." -f Red @@ -233,13 +284,13 @@ $Resource = "deviceManagement/deviceCompliancePolicies" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -261,50 +312,10 @@ $Resource = "deviceManagement/deviceCompliancePolicies" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -335,7 +346,7 @@ $DisplayName = $JSON_Convert.displayName $JSON_Output = $JSON_Convert | ConvertTo-Json -Depth 5 # Adding Scheduled Actions Rule to JSON -$scheduledActionsForRule = '"scheduledActionsForRule":[{"ruleName":"PasswordRequired","scheduledActionConfigurations":[{"actionType":"block","gracePeriodHours":0,"notificationTemplateId":"","notificationMessageCCList":[]}]}]' +$scheduledActionsForRule = '"scheduledActionsForRule":[{"ruleName":"PasswordRequired","scheduledActionConfigurations":[{"actionType":"block","gracePeriodHours":0,"notificationTemplateId":"","notificationMessageCCList":[]}]}]' $JSON_Output = $JSON_Output.trimend("}") @@ -343,7 +354,7 @@ $JSON_Output = $JSON_Output.TrimEnd() + "," + "`r`n" # Joining the JSON together $JSON_Output = $JSON_Output + $scheduledActionsForRule + "`r`n" + "}" - + write-host write-host "Compliance Policy '$DisplayName' Found..." -ForegroundColor Yellow write-host @@ -351,4 +362,5 @@ $JSON_Output write-host Write-Host "Adding Compliance Policy '$DisplayName'" -ForegroundColor Yellow Add-DeviceCompliancePolicy -JSON $JSON_Output - + + diff --git a/CompliancePolicy/CompliancePolicy_Remove.ps1 b/CompliancePolicy/CompliancePolicy_Remove.ps1 index 618be65..6dd0da5 100644 --- a/CompliancePolicy/CompliancePolicy_Remove.ps1 +++ b/CompliancePolicy/CompliancePolicy_Remove.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "DeviceManagementApps.ReadWrite.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-DeviceCompliancePolicy(){ +function Get-DeviceCompliancePolicy { <# .SYNOPSIS @@ -202,36 +254,36 @@ $Resource = "deviceManagement/deviceCompliancePolicies" elseif($Android){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("android") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("android") } } elseif($iOS){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("ios") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("ios") } } elseif($Win10){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") } } elseif($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -256,7 +308,7 @@ $Resource = "deviceManagement/deviceCompliancePolicies" #################################################### -Function Remove-DeviceCompliancePolicy(){ +function Remove-DeviceCompliancePolicy { <# .SYNOPSIS @@ -282,7 +334,7 @@ $Resource = "deviceManagement/deviceCompliancePolicies" try { - if($id -eq "" -or $id -eq $null){ + if("" -eq $id -or $null -eq $id){ write-host "No id specified for device compliance, can't remove compliance policy..." -f Red write-host "Please specify id for device compliance policy..." -f Red @@ -292,7 +344,7 @@ $Resource = "deviceManagement/deviceCompliancePolicies" else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)/$id" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)/$id" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Delete } @@ -320,50 +372,10 @@ $Resource = "deviceManagement/deviceCompliancePolicies" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -389,3 +401,4 @@ $CP = Get-DeviceCompliancePolicy -Name "Test Policy" } } + diff --git a/CorporateDeviceEnrollment/CorpDeviceEnrollment_Add.ps1 b/CorporateDeviceEnrollment/CorpDeviceEnrollment_Add.ps1 index ae3217c..1c615b2 100644 --- a/CorporateDeviceEnrollment/CorpDeviceEnrollment_Add.ps1 +++ b/CorporateDeviceEnrollment/CorpDeviceEnrollment_Add.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-CorporateDeviceIdentifiers(){ +function Add-CorporateDeviceIdentifiers { <# .SYNOPSIS @@ -194,7 +246,7 @@ $JSON = @" { "overwriteImportedDeviceIdentities": $OverwriteImportedDeviceIdentities, -"importedDeviceIdentities": [ { +"importedDeviceIdentities": [ { "importedDeviceIdentifier": "$Identifier", "importedDeviceIdentityType": "$IdentifierType", "description": "$Description"} @@ -207,7 +259,7 @@ $JSON = @" if(($Identifier -match "^[0-9]+$") -and ($Identifier.length -ge 15)){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" (Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken).value } @@ -224,7 +276,7 @@ $JSON = @" if(($Identifier -match "^[a-zA-Z0-9]+$") -and (@($Description).length -le 128)){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" (Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken).value } @@ -262,50 +314,10 @@ $JSON = @" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -328,3 +340,4 @@ elseif($Status.status -eq $false) { } Write-Host + diff --git a/CorporateDeviceEnrollment/CorpDeviceEnrollment_Export.ps1 b/CorporateDeviceEnrollment/CorpDeviceEnrollment_Export.ps1 index b16fcf7..9076d4e 100644 --- a/CorporateDeviceEnrollment/CorpDeviceEnrollment_Export.ps1 +++ b/CorporateDeviceEnrollment/CorpDeviceEnrollment_Export.ps1 @@ -1,158 +1,210 @@ - + <# -  + .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - } + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null + [Parameter(Mandatory = $false)] + [object]$Body = $null, -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) -$clientId = "" -  -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" -  -$resourceAppIdURI = "https://graph.microsoft.com" -  -$authority = "https://login.microsoftonline.com/$Tenant" -  try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-CorporateDeviceIdentifiers(){ +function Get-CorporateDeviceIdentifiers { <# .SYNOPSIS @@ -174,8 +226,8 @@ $Resource = "deviceManagement/importedDeviceIdentities" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } @@ -200,50 +252,10 @@ $Resource = "deviceManagement/importedDeviceIdentities" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -308,3 +320,4 @@ $CDI = Get-CorporateDeviceIdentifiers } Write-Host + diff --git a/CorporateDeviceEnrollment/CorpDeviceEnrollment_Get.ps1 b/CorporateDeviceEnrollment/CorpDeviceEnrollment_Get.ps1 index 2255f14..5b11896 100644 --- a/CorporateDeviceEnrollment/CorpDeviceEnrollment_Get.ps1 +++ b/CorporateDeviceEnrollment/CorpDeviceEnrollment_Get.ps1 @@ -1,158 +1,210 @@ - + <# -  + .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - } + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null + [Parameter(Mandatory = $false)] + [object]$Body = $null, -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) -$clientId = "" -  -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" -  -$resourceAppIdURI = "https://graph.microsoft.com" -  -$authority = "https://login.microsoftonline.com/$Tenant" -  try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-CorporateDeviceIdentifiers(){ +function Get-CorporateDeviceIdentifiers { <# .SYNOPSIS @@ -182,18 +234,18 @@ $graphApiVersion = "beta" if($DeviceIdentifier){ $Resource = "deviceManagement/importedDeviceIdentities?`$filter=contains(importedDeviceIdentifier,'$DeviceIdentifier')" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" } else { $Resource = "deviceManagement/importedDeviceIdentities" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" } - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } @@ -218,50 +270,10 @@ $graphApiVersion = "beta" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -283,3 +295,4 @@ $CDI = Get-CorporateDeviceIdentifiers } Write-Host + diff --git a/CorporateDeviceEnrollment/CorpDeviceEnrollment_Remove.ps1 b/CorporateDeviceEnrollment/CorpDeviceEnrollment_Remove.ps1 index 432af2f..aad338e 100644 --- a/CorporateDeviceEnrollment/CorpDeviceEnrollment_Remove.ps1 +++ b/CorporateDeviceEnrollment/CorpDeviceEnrollment_Remove.ps1 @@ -1,157 +1,209 @@ -<# - +<# + .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "DeviceManagementApps.ReadWrite.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - } + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null + [Parameter(Mandatory = $false)] + [object]$Body = $null, -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) -$clientId = "" - -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" - -$resourceAppIdURI = "https://graph.microsoft.com" - -$authority = "https://login.microsoftonline.com/$Tenant" - try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-CorporateDeviceIdentifiers(){ +function Get-CorporateDeviceIdentifiers { <# .SYNOPSIS @@ -181,18 +233,18 @@ $graphApiVersion = "beta" if($DeviceIdentifier){ $Resource = "deviceManagement/importedDeviceIdentities?`$filter=contains(importedDeviceIdentifier,'$DeviceIdentifier')" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" } else { $Resource = "deviceManagement/importedDeviceIdentities" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" } - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } @@ -215,7 +267,7 @@ $graphApiVersion = "beta" #################################################### -Function Remove-CorporateDeviceIdentifier(){ +function Remove-CorporateDeviceIdentifier { <# .SYNOPSIS @@ -249,7 +301,7 @@ $Resource = "deviceManagement/importedDeviceIdentities/$ImportedDeviceId" if($Confirm -eq "y" -or $Confirm -eq "Y"){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Delete } @@ -284,50 +336,10 @@ $Resource = "deviceManagement/importedDeviceIdentities/$ImportedDeviceId" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -344,7 +356,7 @@ if(@($CDI).count -eq 1){ Write-Host "DeviceId:" $CDI_Id Write-Host "Imported Device Identifier:" $CDI_Identifier Write-Host - + Remove-CorporateDeviceIdentifier -ImportedDeviceId $CDI_Id } @@ -357,3 +369,4 @@ else { } + diff --git a/DeviceConfiguration/DeviceConfiguration_Add.ps1 b/DeviceConfiguration/DeviceConfiguration_Add.ps1 index 0d9c7da..c380157 100644 --- a/DeviceConfiguration/DeviceConfiguration_Add.ps1 +++ b/DeviceConfiguration/DeviceConfiguration_Add.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-DeviceConfigurationPolicy(){ +function Add-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -179,7 +231,7 @@ Write-Verbose "Resource: $DCP_resource" try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red @@ -189,8 +241,8 @@ Write-Verbose "Resource: $DCP_resource" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -215,7 +267,7 @@ Write-Verbose "Resource: $DCP_resource" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -262,50 +314,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -506,3 +518,4 @@ $Android = @" Add-DeviceConfigurationPolicy -Json $Android Add-DeviceConfigurationPolicy -Json $iOS + diff --git a/DeviceConfiguration/DeviceConfiguration_Add_Assign.ps1 b/DeviceConfiguration/DeviceConfiguration_Add_Assign.ps1 index 63cddfd..6807d8d 100644 --- a/DeviceConfiguration/DeviceConfiguration_Add_Assign.ps1 +++ b/DeviceConfiguration/DeviceConfiguration_Add_Assign.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-DeviceConfigurationPolicy(){ +function Add-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -179,7 +231,7 @@ Write-Verbose "Resource: $DCP_resource" try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red @@ -189,13 +241,13 @@ Write-Verbose "Resource: $DCP_resource" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -215,7 +267,7 @@ Write-Verbose "Resource: $DCP_resource" #################################################### -Function Add-DeviceConfigurationPolicyAssignment(){ +function Add-DeviceConfigurationPolicyAssignment { <# .SYNOPSIS @@ -228,152 +280,152 @@ Function Add-DeviceConfigurationPolicyAssignment(){ .NOTES NAME: Add-DeviceConfigurationPolicyAssignment #> - + [cmdletbinding()] - + param ( [parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] $ConfigurationPolicyId, - + [parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] $TargetGroupId, - + [parameter(Mandatory=$true)] [ValidateSet("Included","Excluded")] [ValidateNotNullOrEmpty()] [string]$AssignmentType ) - + $graphApiVersion = "Beta" $Resource = "deviceManagement/deviceConfigurations/$ConfigurationPolicyId/assign" - + try { - + if(!$ConfigurationPolicyId){ - + write-host "No Configuration Policy Id specified, specify a valid Configuration Policy Id" -f Red break - + } - + if(!$TargetGroupId){ - + write-host "No Target Group Id specified, specify a valid Target Group Id" -f Red break - + } - + # Checking if there are Assignments already configured in the Policy $DCPA = Get-DeviceConfigurationPolicyAssignment -id $ConfigurationPolicyId - + $TargetGroups = @() - + if(@($DCPA).count -ge 1){ - + if($DCPA.targetGroupId -contains $TargetGroupId){ - + Write-Host "Group with Id '$TargetGroupId' already assigned to Policy..." -ForegroundColor Red Write-Host break - + } - + # Looping through previously configured assignements - + $DCPA | foreach { - + $TargetGroup = New-Object -TypeName psobject - + if($_.excludeGroup -eq $true){ - + $TargetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.exclusionGroupAssignmentTarget' - + } - + else { - + $TargetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.groupAssignmentTarget' - + } - + $TargetGroup | Add-Member -MemberType NoteProperty -Name 'groupId' -Value $_.targetGroupId - + $Target = New-Object -TypeName psobject $Target | Add-Member -MemberType NoteProperty -Name 'target' -Value $TargetGroup - + $TargetGroups += $Target - + } - + # Adding new group to psobject $TargetGroup = New-Object -TypeName psobject - + if($AssignmentType -eq "Excluded"){ - + $TargetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.exclusionGroupAssignmentTarget' - + } - + elseif($AssignmentType -eq "Included") { - + $TargetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.groupAssignmentTarget' - + } - + $TargetGroup | Add-Member -MemberType NoteProperty -Name 'groupId' -Value "$TargetGroupId" - + $Target = New-Object -TypeName psobject $Target | Add-Member -MemberType NoteProperty -Name 'target' -Value $TargetGroup - + $TargetGroups += $Target - + } - + else { - + # No assignments configured creating new JSON object of group assigned - + $TargetGroup = New-Object -TypeName psobject - + if($AssignmentType -eq "Excluded"){ - + $TargetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.exclusionGroupAssignmentTarget' - + } - + elseif($AssignmentType -eq "Included") { - + $TargetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.groupAssignmentTarget' - + } - + $TargetGroup | Add-Member -MemberType NoteProperty -Name 'groupId' -Value "$TargetGroupId" - + $Target = New-Object -TypeName psobject $Target | Add-Member -MemberType NoteProperty -Name 'target' -Value $TargetGroup - + $TargetGroups = $Target - + } - + # Creating JSON object to pass to Graph $Output = New-Object -TypeName psobject - + $Output | Add-Member -MemberType NoteProperty -Name 'assignments' -Value @($TargetGroups) - + $JSON = $Output | ConvertTo-Json -Depth 3 - + # POST to Graph Service - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -384,14 +436,14 @@ Function Add-DeviceConfigurationPolicyAssignment(){ Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### -Function Get-DeviceConfigurationPolicyAssignment(){ +function Get-DeviceConfigurationPolicyAssignment { <# .SYNOPSIS @@ -404,27 +456,27 @@ Function Get-DeviceConfigurationPolicyAssignment(){ .NOTES NAME: Get-DeviceConfigurationPolicyAssignment #> - + [cmdletbinding()] - + param ( [Parameter(Mandatory=$true,HelpMessage="Enter id (guid) for the Device Configuration Policy you want to check assignment")] $id ) - + $graphApiVersion = "Beta" $DCP_resource = "deviceManagement/deviceConfigurations" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)/$id/groupAssignments" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)/$id/groupAssignments" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -435,14 +487,14 @@ Function Get-DeviceConfigurationPolicyAssignment(){ Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } - + #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -477,7 +529,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -487,7 +539,7 @@ $JSON #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -513,37 +565,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ $GID = $Group.id @@ -551,13 +603,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } } @@ -583,50 +635,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -831,7 +843,7 @@ $AADGroup = Read-Host -Prompt "Enter the Azure AD Group name where policies will $TargetGroupId = (get-AADGroup -GroupName "$AADGroup").id - if($TargetGroupId -eq $null -or $TargetGroupId -eq ""){ + if($null -eq $TargetGroupId -or "" -eq $TargetGroupId){ Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red Write-Host @@ -869,3 +881,4 @@ $Assign_iOS = Add-DeviceConfigurationPolicyAssignment -ConfigurationPolicyId $Cr Write-Host "Assigned '$AADGroup' to $($CreateResult_iOS.displayName)/$($CreateResult_iOS.id)" Write-Host + diff --git a/DeviceConfiguration/DeviceConfiguration_Export.ps1 b/DeviceConfiguration/DeviceConfiguration_Export.ps1 index c7dcbd5..f16abfd 100644 --- a/DeviceConfiguration/DeviceConfiguration_Export.ps1 +++ b/DeviceConfiguration/DeviceConfiguration_Export.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-DeviceConfigurationPolicy(){ +function Get-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -170,14 +221,14 @@ NAME: Get-DeviceConfigurationPolicy $graphApiVersion = "Beta" $DCP_resource = "deviceManagement/deviceConfigurations" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { $ex = $_.Exception @@ -197,7 +248,7 @@ $DCP_resource = "deviceManagement/deviceConfigurations" #################################################### -Function Export-JSONData(){ +function Export-JSONData { <# .SYNOPSIS @@ -220,7 +271,7 @@ $ExportPath try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON..." -f Red @@ -255,7 +306,7 @@ $ExportPath $JSON1 | Set-Content -LiteralPath "$ExportPath\$FileName_JSON" write-host "JSON created in $ExportPath\$FileName_JSON..." -f cyan - + } } @@ -272,50 +323,10 @@ $ExportPath #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -366,3 +377,4 @@ Write-Host } Write-Host + diff --git a/DeviceConfiguration/DeviceConfiguration_Get.ps1 b/DeviceConfiguration/DeviceConfiguration_Get.ps1 index b17fb13..5ae4f91 100644 --- a/DeviceConfiguration/DeviceConfiguration_Get.ps1 +++ b/DeviceConfiguration/DeviceConfiguration_Get.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-DeviceConfigurationPolicy(){ +function Get-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -180,15 +231,15 @@ $DCP_resource = "deviceManagement/deviceConfigurations" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -213,7 +264,7 @@ $DCP_resource = "deviceManagement/deviceConfigurations" #################################################### -Function Get-DeviceConfigurationPolicyAssignment(){ +function Get-DeviceConfigurationPolicyAssignment { <# .SYNOPSIS @@ -240,8 +291,8 @@ $DCP_resource = "deviceManagement/deviceConfigurations" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)/$id/groupAssignments" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)/$id/groupAssignments" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -264,7 +315,7 @@ $DCP_resource = "deviceManagement/deviceConfigurations" #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -298,19 +349,19 @@ $Group_resource = "groups" if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" switch ( $id ) { $AllUsers { $grp = [PSCustomObject]@{ displayName = "All users"}; $grp } $AllDevices { $grp = [PSCustomObject]@{ displayName = "All devices"}; $grp } - default { (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } + default { (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } - elseif($GroupName -eq "" -or $GroupName -eq $null){ + elseif("" -eq $GroupName -or $null -eq $GroupName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -318,15 +369,15 @@ $Group_resource = "groups" if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } elseif($Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value if($Group){ @@ -335,8 +386,8 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -367,50 +418,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -455,3 +466,4 @@ write-host "Getting Configuration Policy assignment..." -f Cyan Write-Host } + diff --git a/DeviceConfiguration/DeviceConfiguration_Get_Assign.ps1 b/DeviceConfiguration/DeviceConfiguration_Get_Assign.ps1 index dedcc7a..82cf8e7 100644 --- a/DeviceConfiguration/DeviceConfiguration_Get_Assign.ps1 +++ b/DeviceConfiguration/DeviceConfiguration_Get_Assign.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-DeviceConfigurationPolicy(){ +function Get-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -180,15 +232,15 @@ $DCP_resource = "deviceManagement/deviceConfigurations" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)?`$filter=displayName eq '$name'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)?`$filter=displayName eq '$name'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -213,7 +265,7 @@ $DCP_resource = "deviceManagement/deviceConfigurations" #################################################### -Function Get-DeviceConfigurationPolicyAssignment(){ +function Get-DeviceConfigurationPolicyAssignment { <# .SYNOPSIS @@ -240,8 +292,8 @@ $DCP_resource = "deviceManagement/deviceConfigurations" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)/$id/groupAssignments" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)/$id/groupAssignments" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -264,7 +316,7 @@ $DCP_resource = "deviceManagement/deviceConfigurations" #################################################### -Function Add-DeviceConfigurationPolicyAssignment(){ +function Add-DeviceConfigurationPolicyAssignment { <# .SYNOPSIS @@ -298,7 +350,7 @@ param $graphApiVersion = "Beta" $Resource = "deviceManagement/deviceConfigurations/$ConfigurationPolicyId/assign" - + try { if(!$ConfigurationPolicyId){ @@ -321,7 +373,7 @@ $Resource = "deviceManagement/deviceConfigurations/$ConfigurationPolicyId/assign $TargetGroups = @() if(@($DCPA).count -ge 1){ - + if($DCPA.targetGroupId -contains $TargetGroupId){ Write-Host "Group with Id '$TargetGroupId' already assigned to Policy..." -ForegroundColor Red @@ -335,17 +387,17 @@ $Resource = "deviceManagement/deviceConfigurations/$ConfigurationPolicyId/assign $DCPA | foreach { $TargetGroup = New-Object -TypeName psobject - + if($_.excludeGroup -eq $true){ $TargetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.exclusionGroupAssignmentTarget' - + } - + else { - + $TargetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.groupAssignmentTarget' - + } $TargetGroup | Add-Member -MemberType NoteProperty -Name 'groupId' -Value $_.targetGroupId @@ -363,15 +415,15 @@ $Resource = "deviceManagement/deviceConfigurations/$ConfigurationPolicyId/assign if($AssignmentType -eq "Excluded"){ $TargetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.exclusionGroupAssignmentTarget' - + } - + elseif($AssignmentType -eq "Included") { - + $TargetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.groupAssignmentTarget' - + } - + $TargetGroup | Add-Member -MemberType NoteProperty -Name 'groupId' -Value "$TargetGroupId" $Target = New-Object -TypeName psobject @@ -384,21 +436,21 @@ $Resource = "deviceManagement/deviceConfigurations/$ConfigurationPolicyId/assign else { # No assignments configured creating new JSON object of group assigned - + $TargetGroup = New-Object -TypeName psobject if($AssignmentType -eq "Excluded"){ $TargetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.exclusionGroupAssignmentTarget' - + } - + elseif($AssignmentType -eq "Included") { - + $TargetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.groupAssignmentTarget' - + } - + $TargetGroup | Add-Member -MemberType NoteProperty -Name 'groupId' -Value "$TargetGroupId" $Target = New-Object -TypeName psobject @@ -416,11 +468,11 @@ $Resource = "deviceManagement/deviceConfigurations/$ConfigurationPolicyId/assign $JSON = $Output | ConvertTo-Json -Depth 3 # POST to Graph Service - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } - + catch { $ex = $_.Exception @@ -440,7 +492,7 @@ $Resource = "deviceManagement/deviceConfigurations/$ConfigurationPolicyId/assign #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -466,37 +518,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ $GID = $Group.id @@ -504,13 +556,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } } @@ -536,50 +588,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -593,7 +605,7 @@ $AADGroup = Read-Host -Prompt "Enter the Azure AD Group name where policies will $TargetGroupId = (get-AADGroup -GroupName "$AADGroup").id - if($TargetGroupId -eq $null -or $TargetGroupId -eq ""){ + if($null -eq $TargetGroupId -or "" -eq $TargetGroupId){ Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red Write-Host @@ -618,6 +630,7 @@ if($DCP){ else { Write-Host "Can't find Device Configuration Policy with name '$PolicyName'..." -ForegroundColor Red - Write-Host + Write-Host } + diff --git a/DeviceConfiguration/DeviceConfiguration_Import_FromJSON.ps1 b/DeviceConfiguration/DeviceConfiguration_Import_FromJSON.ps1 index 5468479..187e0c1 100644 --- a/DeviceConfiguration/DeviceConfiguration_Import_FromJSON.ps1 +++ b/DeviceConfiguration/DeviceConfiguration_Import_FromJSON.ps1 @@ -9,152 +9,203 @@ See LICENSE in the project root for license information. [cmdletbinding()] param ( [Parameter(Mandatory=$false)][String]$FileName ) -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-DeviceConfigurationPolicy(){ +function Add-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -181,7 +232,7 @@ Write-Verbose "Resource: $DCP_resource" try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the Device Configuration Policy..." -f Red @@ -191,13 +242,13 @@ Write-Verbose "Resource: $DCP_resource" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -217,7 +268,7 @@ Write-Verbose "Resource: $DCP_resource" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -252,7 +303,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -264,50 +315,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -342,7 +353,7 @@ $JSON_Convert = $JSON_Data | ConvertFrom-Json | Select-Object -Property * -Exclu $DisplayName = $JSON_Convert.displayName $JSON_Output = $JSON_Convert | ConvertTo-Json -Depth 5 - + write-host write-host "Device Configuration Policy '$DisplayName' Found..." -ForegroundColor Yellow write-host @@ -350,3 +361,4 @@ $JSON_Output write-host Write-Host "Adding Device Configuration Policy '$DisplayName'" -ForegroundColor Yellow Add-DeviceConfigurationPolicy -JSON $JSON_Output + diff --git a/DeviceConfiguration/DeviceConfiguration_Remove.ps1 b/DeviceConfiguration/DeviceConfiguration_Remove.ps1 index 9823db7..e5208c6 100644 --- a/DeviceConfiguration/DeviceConfiguration_Remove.ps1 +++ b/DeviceConfiguration/DeviceConfiguration_Remove.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "DeviceManagementApps.ReadWrite.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-DeviceConfigurationPolicy(){ +function Get-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -180,15 +232,15 @@ $DCP_resource = "deviceManagement/deviceConfigurations" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -213,7 +265,7 @@ $DCP_resource = "deviceManagement/deviceConfigurations" #################################################### -Function Remove-DeviceConfigurationPolicy(){ +function Remove-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -239,7 +291,7 @@ $DCP_resource = "deviceManagement/deviceConfigurations" try { - if($id -eq "" -or $id -eq $null){ + if("" -eq $id -or $null -eq $id){ write-host "No id specified for device configuration, can't remove configuration..." -f Red write-host "Please specify id for device configuration..." -f Red @@ -249,7 +301,7 @@ $DCP_resource = "deviceManagement/deviceConfigurations" else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)/$id" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)/$id" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Delete } @@ -277,50 +329,10 @@ $DCP_resource = "deviceManagement/deviceConfigurations" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -353,3 +365,4 @@ $CP = Get-DeviceConfigurationPolicy -name "Test Graph Policy" Write-Host } + diff --git a/DeviceConfiguration/DeviceManagementScript_Add.ps1 b/DeviceConfiguration/DeviceManagementScript_Add.ps1 index b00cae9..db16f51 100644 --- a/DeviceConfiguration/DeviceManagementScript_Add.ps1 +++ b/DeviceConfiguration/DeviceManagementScript_Add.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { - <# +function Connect-GraphAPI { +<# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> - - [cmdletbinding()] - - param - ( - [Parameter(Mandatory = $true)] - $User + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" ) - $userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - - $tenant = $userUpn.Host - - Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - - # Getting path to ActiveDirectory Assemblies - # If the module count is greater than 1 find the latest version - - if ($AadModule.count -gt 1) { - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if ($AadModule.count -gt 1) { - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} - [System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - - [System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - - # Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information - # on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - - $clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, - $redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', - $resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, - $authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $clientId, $redirectUri, $platformParameters, $userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if ($authResult.AccessToken) { + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type' = 'application/json' - 'Authorization' = "Bearer " + $authResult.AccessToken - 'ExpiresOn' = $authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-DeviceManagementScript() { +function Add-DeviceManagementScript { <# .SYNOPSIS This function is used to add a device management script using the Graph API REST interface @@ -227,8 +279,8 @@ NAME: Add-DeviceManagementScript Write-Verbose "Resource: $DMS_resource" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$DMS_resource" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$DMS_resource" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } catch { @@ -252,50 +304,10 @@ NAME: Add-DeviceManagementScript #region Authentication -write-host - -# Checking if authToken exists before running authentication -if ($global:authToken) { - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if ($TokenExpires -le 0) { - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if ($User -eq $null -or $User -eq "") { - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if ($User -eq $null -or $User -eq "") { - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - # Getting the authorization token - $global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -306,3 +318,4 @@ else { Add-DeviceManagementScript -File "C:\Scripts\test-script.ps1" -Description "Test script" Add-DeviceManagementScript -File "https://pathtourl/test-script.ps1" -URL -Description "Test script" + diff --git a/DeviceConfiguration/DeviceManagementScript_Add_Assign.ps1 b/DeviceConfiguration/DeviceManagementScript_Add_Assign.ps1 index f0f62ad..8b697f4 100644 --- a/DeviceConfiguration/DeviceManagementScript_Add_Assign.ps1 +++ b/DeviceConfiguration/DeviceManagementScript_Add_Assign.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { - <# +function Connect-GraphAPI { +<# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> - - [cmdletbinding()] - - param - ( - [Parameter(Mandatory = $true)] - $User + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" ) - $userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - - $tenant = $userUpn.Host - - Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - - # Getting path to ActiveDirectory Assemblies - # If the module count is greater than 1 find the latest version - - if ($AadModule.count -gt 1) { - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if ($AadModule.count -gt 1) { - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} - [System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - - [System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - - # Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information - # on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - - $clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, - $redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', - $resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, - $authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $clientId, $redirectUri, $platformParameters, $userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if ($authResult.AccessToken) { + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type' = 'application/json' - 'Authorization' = "Bearer " + $authResult.AccessToken - 'ExpiresOn' = $authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-DeviceManagementScript() { +function Add-DeviceManagementScript { <# .SYNOPSIS This function is used to add a device management script using the Graph API REST interface @@ -227,8 +279,8 @@ NAME: Add-DeviceManagementScript Write-Verbose "Resource: $DMS_resource" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$DMS_resource" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$DMS_resource" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } catch { @@ -250,7 +302,7 @@ NAME: Add-DeviceManagementScript #################################################### -Function Add-DeviceManagementScriptAssignment() { +function Add-DeviceManagementScriptAssignment { <# .SYNOPSIS This function is used to add a device configuration policy assignment using the Graph API REST interface @@ -302,8 +354,8 @@ NAME: Add-DeviceConfigurationPolicyAssignment } "@ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -325,7 +377,7 @@ NAME: Add-DeviceConfigurationPolicyAssignment #################################################### -Function Get-AADGroup() { +function Get-AADGroup { <# .SYNOPSIS @@ -356,15 +408,15 @@ NAME: Get-AADGroup if ($id) { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - elseif ($GroupName -eq "" -or $GroupName -eq $null) { + elseif ("" -eq $GroupName -or $null -eq $GroupName) { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -372,15 +424,15 @@ NAME: Get-AADGroup if (!$Members) { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } elseif ($Members) { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value if ($Group) { @@ -389,8 +441,8 @@ NAME: Get-AADGroup $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } @@ -420,50 +472,10 @@ NAME: Get-AADGroup #region Authentication -write-host - -# Checking if authToken exists before running authentication -if ($global:authToken) { - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if ($TokenExpires -le 0) { - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if ($User -eq $null -or $User -eq "") { - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if ($User -eq $null -or $User -eq "") { - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - # Getting the authorization token - $global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -476,7 +488,7 @@ $AADGroup = Read-Host -Prompt "Enter the Azure AD Group name where PowerShell sc $TargetGroupId = (Get-AADGroup -GroupName "$AADGroup").id -if ($TargetGroupId -eq $null -or $TargetGroupId -eq "") { +if ($null -eq $TargetGroupId -or "" -eq $TargetGroupId) { Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red Write-Host @@ -514,3 +526,4 @@ $Assign_Web_Script = Add-DeviceManagementScriptAssignment -ScriptId $Create_Web_ Write-Host "Assigned '$AADGroup' to $($Create_Web_Script.displayName)/$($Create_Web_Script.id)" Write-Host + diff --git a/DeviceConfiguration/DeviceManagementScripts_Get.ps1 b/DeviceConfiguration/DeviceManagementScripts_Get.ps1 index cadc62e..0afe6b7 100644 --- a/DeviceConfiguration/DeviceManagementScripts_Get.ps1 +++ b/DeviceConfiguration/DeviceManagementScripts_Get.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,148 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User - -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null -  -# Client ID used for Intune scopes - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + $results = @() + $nextLink = $Uri - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } -} -  #################################################### -Function Get-DeviceManagementScripts(){ +function Get-DeviceManagementScripts { <# .SYNOPSIS @@ -175,26 +231,26 @@ param ( $graphApiVersion = "Beta" $Resource = "deviceManagement/deviceManagementScripts" - + try { if($ScriptId){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource/$ScriptId" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource/$ScriptId" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)?`$expand=groupAssignments" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)?`$expand=groupAssignments" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - + } - + catch { $ex = $_.Exception @@ -214,8 +270,8 @@ $Resource = "deviceManagement/deviceManagementScripts" #################################################### -Function Get-AADGroup(){ - +function Get-AADGroup { + <# .SYNOPSIS This function is used to get AAD Groups from the Graph API REST interface @@ -227,70 +283,70 @@ Returns all users registered with Azure AD .NOTES NAME: Get-AADGroup #> - + [cmdletbinding()] - + param ( $GroupName, $id, [switch]$Members ) - + # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { - + if($id){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + else { - + if(!$Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ - + $GID = $Group.id - + $Group.displayName write-host - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + } - + } - + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -301,59 +357,19 @@ $Group_resource = "groups" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - - } - -} - -#################################################### - -#region Authentication - -Write-Host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - Write-Host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - Write-Host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User + } - } } -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } +#################################################### -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User +#region Authentication +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -379,24 +395,24 @@ if($PSScripts){ write-host "Device Management Scripts - Assignments" -f Cyan $Assignments = $_.groupAssignments.targetGroupId - + if($Assignments){ - + foreach($Group in $Assignments){ - + (Get-AADGroup -id $Group).displayName - + } - + Write-Host - + } - + else { - + Write-Host "No assignments set for this policy..." -ForegroundColor Red Write-Host - + } $Script = Get-DeviceManagementScripts -ScriptId $ScriptId @@ -422,3 +438,4 @@ Write-Host "No PowerShell scripts have been added to the service..." -Foreground Write-Host } + diff --git a/EndpointSecurity/EndpointSecurityPolicy_Export.ps1 b/EndpointSecurity/EndpointSecurityPolicy_Export.ps1 index c636248..183df2c 100644 --- a/EndpointSecurity/EndpointSecurityPolicy_Export.ps1 +++ b/EndpointSecurity/EndpointSecurityPolicy_Export.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-EndpointSecurityTemplate(){ +function Get-EndpointSecurityTemplate { <# .SYNOPSIS @@ -160,7 +212,7 @@ This function is used to get all Endpoint Security templates using the Graph API .DESCRIPTION The function connects to the Graph API Interface and gets all Endpoint Security templates .EXAMPLE -Get-EndpointSecurityTemplate +Get-EndpointSecurityTemplate Gets all Endpoint Security Templates in Endpoint Manager .NOTES NAME: Get-EndpointSecurityTemplate @@ -172,11 +224,11 @@ $ESP_resource = "deviceManagement/templates?`$filter=(isof(%27microsoft.graph.se try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($ESP_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($ESP_resource)" (Invoke-RestMethod -Method Get -Uri $uri -Headers $authToken).value } - + catch { $ex = $_.Exception @@ -196,7 +248,7 @@ $ESP_resource = "deviceManagement/templates?`$filter=(isof(%27microsoft.graph.se #################################################### -Function Get-EndpointSecurityPolicy(){ +function Get-EndpointSecurityPolicy { <# .SYNOPSIS @@ -216,11 +268,11 @@ $ESP_resource = "deviceManagement/intents" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($ESP_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($ESP_resource)" (Invoke-RestMethod -Method Get -Uri $uri -Headers $authToken).value } - + catch { $ex = $_.Exception @@ -240,7 +292,7 @@ $ESP_resource = "deviceManagement/intents" #################################################### -Function Get-EndpointSecurityTemplateCategory(){ +function Get-EndpointSecurityTemplateCategory { <# .SYNOPSIS @@ -268,11 +320,11 @@ $ESP_resource = "deviceManagement/templates/$TemplateId/categories" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($ESP_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($ESP_resource)" (Invoke-RestMethod -Method Get -Uri $uri -Headers $authToken).value } - + catch { $ex = $_.Exception @@ -292,7 +344,7 @@ $ESP_resource = "deviceManagement/templates/$TemplateId/categories" #################################################### -Function Get-EndpointSecurityCategorySetting(){ +function Get-EndpointSecurityCategorySetting { <# .SYNOPSIS @@ -323,11 +375,11 @@ $ESP_resource = "deviceManagement/intents/$policyId/categories/$categoryId/setti try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($ESP_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($ESP_resource)" (Invoke-RestMethod -Method Get -Uri $uri -Headers $authToken).value } - + catch { $ex = $_.Exception @@ -347,7 +399,7 @@ $ESP_resource = "deviceManagement/intents/$policyId/categories/$categoryId/setti #################################################### -Function Export-JSONData(){ +function Export-JSONData { <# .SYNOPSIS @@ -370,7 +422,7 @@ $ExportPath try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON..." -f Red @@ -406,7 +458,7 @@ $ExportPath $JSON1 | Set-Content -LiteralPath "$ExportPath\$FileName_JSON" write-host "JSON created in $ExportPath\$FileName_JSON..." -f cyan - + } } @@ -423,50 +475,10 @@ $ExportPath #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -569,7 +581,7 @@ foreach($policy in $ESPolicies){ $categoryId = $category.id $Settings += Get-EndpointSecurityCategorySetting -PolicyId $policyId -categoryId $categoryId - + } # Adding All settings to settingsDelta ready for JSON export @@ -588,3 +600,4 @@ foreach($policy in $ESPolicies){ } } + diff --git a/EndpointSecurity/EndpointSecurityPolicy_Get.ps1 b/EndpointSecurity/EndpointSecurityPolicy_Get.ps1 index 142e7af..37b5b38 100644 --- a/EndpointSecurity/EndpointSecurityPolicy_Get.ps1 +++ b/EndpointSecurity/EndpointSecurityPolicy_Get.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-EndpointSecurityTemplate(){ +function Get-EndpointSecurityTemplate { <# .SYNOPSIS @@ -160,7 +212,7 @@ This function is used to get all Endpoint Security templates using the Graph API .DESCRIPTION The function connects to the Graph API Interface and gets all Endpoint Security templates .EXAMPLE -Get-EndpointSecurityTemplate +Get-EndpointSecurityTemplate Gets all Endpoint Security Templates in Endpoint Manager .NOTES NAME: Get-EndpointSecurityTemplate @@ -172,11 +224,11 @@ $ESP_resource = "deviceManagement/templates?`$filter=(isof(%27microsoft.graph.se try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($ESP_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($ESP_resource)" (Invoke-RestMethod -Method Get -Uri $uri -Headers $authToken).value } - + catch { $ex = $_.Exception @@ -196,7 +248,7 @@ $ESP_resource = "deviceManagement/templates?`$filter=(isof(%27microsoft.graph.se #################################################### -Function Get-EndpointSecurityPolicy(){ +function Get-EndpointSecurityPolicy { <# .SYNOPSIS @@ -216,11 +268,11 @@ $ESP_resource = "deviceManagement/intents" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($ESP_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($ESP_resource)" (Invoke-RestMethod -Method Get -Uri $uri -Headers $authToken).value } - + catch { $ex = $_.Exception @@ -240,7 +292,7 @@ $ESP_resource = "deviceManagement/intents" #################################################### -Function Get-EndpointSecurityTemplateCategory(){ +function Get-EndpointSecurityTemplateCategory { <# .SYNOPSIS @@ -268,11 +320,11 @@ $ESP_resource = "deviceManagement/templates/$TemplateId/categories" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($ESP_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($ESP_resource)" (Invoke-RestMethod -Method Get -Uri $uri -Headers $authToken).value } - + catch { $ex = $_.Exception @@ -292,7 +344,7 @@ $ESP_resource = "deviceManagement/templates/$TemplateId/categories" #################################################### -Function Get-EndpointSecurityCategorySetting(){ +function Get-EndpointSecurityCategorySetting { <# .SYNOPSIS @@ -323,11 +375,11 @@ $ESP_resource = "deviceManagement/intents/$policyId/categories/$categoryId/setti try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($ESP_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($ESP_resource)" (Invoke-RestMethod -Method Get -Uri $uri -Headers $authToken).value } - + catch { $ex = $_.Exception @@ -349,50 +401,10 @@ $ESP_resource = "deviceManagement/intents/$policyId/categories/$categoryId/setti #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -450,7 +462,7 @@ if($ESPolicies){ $categoryId = $category.id $Settings += Get-EndpointSecurityCategorySetting -PolicyId $policyId -categoryId $categoryId - + } # Adding All settings to settingsDelta ready for JSON export @@ -471,3 +483,4 @@ else { Write-Host } + diff --git a/EndpointSecurity/EndpointSecurityPolicy_Import_FromJSON.ps1 b/EndpointSecurity/EndpointSecurityPolicy_Import_FromJSON.ps1 index 30682fa..37f8340 100644 --- a/EndpointSecurity/EndpointSecurityPolicy_Import_FromJSON.ps1 +++ b/EndpointSecurity/EndpointSecurityPolicy_Import_FromJSON.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-EndpointSecurityTemplate(){ +function Get-EndpointSecurityTemplate { <# .SYNOPSIS @@ -160,7 +212,7 @@ This function is used to get all Endpoint Security templates using the Graph API .DESCRIPTION The function connects to the Graph API Interface and gets all Endpoint Security templates .EXAMPLE -Get-EndpointSecurityTemplate +Get-EndpointSecurityTemplate Gets all Endpoint Security Templates in Endpoint Manager .NOTES NAME: Get-EndpointSecurityTemplate @@ -172,11 +224,11 @@ $ESP_resource = "deviceManagement/templates?`$filter=(isof(%27microsoft.graph.se try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($ESP_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($ESP_resource)" (Invoke-RestMethod -Method Get -Uri $uri -Headers $authToken).value } - + catch { $ex = $_.Exception @@ -196,7 +248,7 @@ $ESP_resource = "deviceManagement/templates?`$filter=(isof(%27microsoft.graph.se #################################################### -Function Add-EndpointSecurityPolicy(){ +function Add-EndpointSecurityPolicy { <# .SYNOPSIS @@ -224,7 +276,7 @@ Write-Verbose "Resource: $ESP_resource" try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the Endpoint Security Policy..." -f Red @@ -234,13 +286,13 @@ Write-Verbose "Resource: $ESP_resource" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($ESP_resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($ESP_resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -260,7 +312,7 @@ Write-Verbose "Resource: $ESP_resource" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -295,7 +347,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -307,50 +359,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -436,7 +448,7 @@ elseif($ES_Template){ #################################################### # Else If Imported JSON template ID can't be found check if Template Display Name can be used -elseif($ES_Template -eq $null){ +elseif($null -eq $ES_Template){ Write-Host "Didn't find Template with ID $JSON_TemplateId, checking if Template DisplayName '$JSON_TemplateDisplayName' can be used..." -ForegroundColor Red $ES_Template = $Templates | ? { $_.displayName -eq "$JSON_TemplateDisplayName" } @@ -491,3 +503,4 @@ $JSON_Output write-host Write-Host "Adding Endpoint Security Policy '$DisplayName'" -ForegroundColor Yellow Add-EndpointSecurityPolicy -TemplateId $TemplateId -JSON $JSON_Output + diff --git a/EndpointSecurity/EndpointSecurityTemplates_Get.ps1 b/EndpointSecurity/EndpointSecurityTemplates_Get.ps1 index 587a12a..ed6f794 100644 --- a/EndpointSecurity/EndpointSecurityTemplates_Get.ps1 +++ b/EndpointSecurity/EndpointSecurityTemplates_Get.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-EndpointSecurityTemplate(){ +function Get-EndpointSecurityTemplate { <# .SYNOPSIS @@ -198,50 +250,10 @@ $ESP_resource = "deviceManagement/templates?`$filter=(isof(%27microsoft.graph.se #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -257,3 +269,4 @@ foreach($Template in ($Templates | sort displayName)){ $Template } + diff --git a/EnrollmentRestrictions/DeviceEnrollmentRestrictions_Get.ps1 b/EnrollmentRestrictions/DeviceEnrollmentRestrictions_Get.ps1 index 5b3c4a3..187ed71 100644 --- a/EnrollmentRestrictions/DeviceEnrollmentRestrictions_Get.ps1 +++ b/EnrollmentRestrictions/DeviceEnrollmentRestrictions_Get.ps1 @@ -7,153 +7,205 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-DeviceEnrollmentConfigurations(){ - +function Get-DeviceEnrollmentConfigurations { + <# .SYNOPSIS This function is used to get Deivce Enrollment Configurations from the Graph API REST interface @@ -165,21 +217,21 @@ Returns Device Enrollment Configurations configured in Intune .NOTES NAME: Get-DeviceEnrollmentConfigurations #> - + [cmdletbinding()] - + $graphApiVersion = "Beta" $Resource = "deviceManagement/deviceEnrollmentConfigurations" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -190,59 +242,19 @@ NAME: Get-DeviceEnrollmentConfigurations Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -252,3 +264,4 @@ $global:authToken = Get-AuthToken -User $User $DeviceEnrollmentConfigurations = Get-DeviceEnrollmentConfigurations $DeviceEnrollmentConfigurations | Where-Object { ($_.id).contains("DefaultPlatformRestrictions") } + diff --git a/EnrollmentRestrictions/DeviceEnrollmentRestrictions_Set.ps1 b/EnrollmentRestrictions/DeviceEnrollmentRestrictions_Set.ps1 index 80d80fe..8a39fa6 100644 --- a/EnrollmentRestrictions/DeviceEnrollmentRestrictions_Set.ps1 +++ b/EnrollmentRestrictions/DeviceEnrollmentRestrictions_Set.ps1 @@ -7,153 +7,205 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-DeviceEnrollmentConfigurations(){ - +function Get-DeviceEnrollmentConfigurations { + <# .SYNOPSIS This function is used to get Deivce Enrollment Configurations from the Graph API REST interface @@ -165,21 +217,21 @@ Returns Device Enrollment Configurations configured in Intune .NOTES NAME: Get-DeviceEnrollmentConfigurations #> - + [cmdletbinding()] - + $graphApiVersion = "Beta" $Resource = "deviceManagement/deviceEnrollmentConfigurations" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -190,15 +242,15 @@ $Resource = "deviceManagement/deviceEnrollmentConfigurations" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### - -Function Set-DeviceEnrollmentConfiguration(){ - + +function Set-DeviceEnrollmentConfiguration { + <# .SYNOPSIS This function is used to set the Device Enrollment Configuration resource using the Graph API REST interface @@ -210,47 +262,47 @@ Sets the Device Enrollment Configuration using Graph API .NOTES NAME: Set-DeviceEnrollmentConfiguration #> - + [cmdletbinding()] - + param ( $JSON, $DEC_Id ) - + $graphApiVersion = "Beta" $App_resource = "deviceManagement/deviceEnrollmentConfigurations" - + try { - + if(!$JSON){ - + write-host "No JSON was passed to the function, provide a JSON variable" -f Red break - + } - + elseif(!$DEC_Id){ - + write-host "No Device Enrollment Configuration ID was passed to the function, provide a Device Enrollment Configuration ID" -f Red break - + } - + else { - + Test-JSON -JSON $JSON - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)/$DEC_Id" + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)/$DEC_Id" Invoke-RestMethod -Uri $uri -Method Patch -ContentType "application/json" -Body $JSON -Headers $authToken - + } - + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -261,14 +313,14 @@ $App_resource = "deviceManagement/deviceEnrollmentConfigurations" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -315,50 +367,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -420,3 +432,4 @@ $DeviceEnrollmentConfigurations = Get-DeviceEnrollmentConfigurations $PlatformRestrictions = ($DeviceEnrollmentConfigurations | Where-Object { ($_.id).contains("DefaultPlatformRestrictions") }).id Set-DeviceEnrollmentConfiguration -DEC_Id $PlatformRestrictions -JSON $JSON + diff --git a/Filters/AssociatedFilter_Get.ps1 b/Filters/AssociatedFilter_Get.ps1 index c250bc0..8e12ec3 100644 --- a/Filters/AssociatedFilter_Get.ps1 +++ b/Filters/AssociatedFilter_Get.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-AADGroups(){ +function Get-AADGroups { <# .SYNOPSIS @@ -177,37 +229,37 @@ param # Defining Variables $graphApiVersion = "beta" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ $GID = $Group.id @@ -215,13 +267,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } } @@ -245,7 +297,7 @@ $Group_resource = "groups" #################################################### -Function Get-DeviceCompliancePolicy(){ +function Get-DeviceCompliancePolicy { <# .SYNOPSIS @@ -284,36 +336,36 @@ $Resource = "deviceManagement/deviceCompliancePolicies?`$expand=assignments" if($Platform -eq "Android"){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("android") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("android") } } elseif($Platform -eq "iOS"){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("ios") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("ios") } } elseif($Platform -eq "Windows10"){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") } } elseif($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -338,7 +390,7 @@ $Resource = "deviceManagement/deviceCompliancePolicies?`$expand=assignments" #################################################### -Function Get-DeviceConfigurationPolicy(){ +function Get-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -366,15 +418,15 @@ $DCP_resource = "deviceManagement/deviceConfigurations?`$expand=assignments" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -399,7 +451,7 @@ $DCP_resource = "deviceManagement/deviceConfigurations?`$expand=assignments" #################################################### -Function Get-AdministrativeTemplates(){ +function Get-AdministrativeTemplates { <# .SYNOPSIS @@ -425,8 +477,8 @@ $Resource = "deviceManagement/groupPolicyConfigurations?`$expand=assignments" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -449,7 +501,7 @@ $Resource = "deviceManagement/groupPolicyConfigurations?`$expand=assignments" #################################################### -Function Get-AssignmentFilters(){ +function Get-AssignmentFilters { <# .SYNOPSIS @@ -475,8 +527,8 @@ $Resource = "deviceManagement/assignmentFilters" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -499,7 +551,7 @@ $Resource = "deviceManagement/assignmentFilters" #################################################### -Function Get-SettingsCatalogPolicy(){ +function Get-SettingsCatalogPolicy { <# .SYNOPSIS @@ -533,7 +585,7 @@ param $graphApiVersion = "beta" if($Platform){ - + $Resource = "deviceManagement/configurationPolicies?`$filter=platforms has '$Platform' and technologies has 'mdm'" } @@ -552,8 +604,8 @@ $graphApiVersion = "beta" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -576,7 +628,7 @@ $graphApiVersion = "beta" #################################################### -Function Get-IntuneApplication(){ +function Get-IntuneApplication { <# .SYNOPSIS @@ -604,15 +656,15 @@ $Resource = "deviceAppManagement/mobileApps?`$expand=assignments" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") -and (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") -and (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { (!($_.'@odata.type').Contains("managed")) } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { (!($_.'@odata.type').Contains("managed")) } } @@ -640,50 +692,10 @@ $Resource = "deviceAppManagement/mobileApps?`$expand=assignments" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -693,7 +705,7 @@ $global:authToken = Get-AuthToken -User $User write-host "Filters Name:" -f Yellow $FilterName = Read-Host -if($FilterName -eq $null -or $FilterName -eq ""){ +if($null -eq $FilterName -or "" -eq $FilterName){ write-host "Filter Name is Null..." -ForegroundColor Red Write-Host "Script can't continue..." -ForegroundColor Red @@ -776,7 +788,7 @@ if(@($CPs).count -ge 1){ Write-Host "Policy Name: " -NoNewline Write-Host $CP.displayName -f green Write-Host "Filter Type:" $Com_Group.target.deviceAndAppManagementAssignmentFilterType - + if($Com_Group.target.'@odata.type' -eq "#microsoft.graph.allDevicesAssignmentTarget"){ Write-Host "AAD Group Name: All Devices" @@ -846,7 +858,7 @@ if($DCPs){ $DCPsCount = @($DCPs).count $i = 1 - + $DCP_Count = 0 foreach($DCP in $DCPs){ @@ -865,7 +877,7 @@ if($DCPs){ Write-Host "Policy Name: " -NoNewline Write-Host $DCP.displayName -f green Write-Host "Filter Type:" $Com_Group.target.deviceAndAppManagementAssignmentFilterType - + if($Com_Group.target.'@odata.type' -eq "#microsoft.graph.allDevicesAssignmentTarget"){ Write-Host "AAD Group Name: All Devices" @@ -888,7 +900,7 @@ if($DCPs){ $DCP_Count++ } - + } @@ -948,14 +960,14 @@ if($SCPolicies){ if($SCPolicyAssignment){ foreach($Com_Group in $SCPolicyAssignment){ - + if($Com_Group.target.deviceAndAppManagementAssignmentFilterId -eq $Filter.id){ Write-Host Write-Host "Policy Name: " -NoNewline Write-Host $SCPolicy.name -f green Write-Host "Filter Type:" $Com_Group.target.deviceAndAppManagementAssignmentFilterType - + if($Com_Group.target.'@odata.type' -eq "#microsoft.graph.allDevicesAssignmentTarget"){ Write-Host "AAD Group Name: All Devices" @@ -1044,7 +1056,7 @@ if($ADMXPolicies){ Write-Host "Policy Name: " -NoNewline Write-Host $ADMXPolicy.displayName -f green Write-Host "Filter Type:" $Com_Group.target.deviceAndAppManagementAssignmentFilterType - + if($Com_Group.target.'@odata.type' -eq "#microsoft.graph.allDevicesAssignmentTarget"){ Write-Host "AAD Group Name: All Devices" @@ -1126,7 +1138,7 @@ if($Apps){ if($AppAssignment){ foreach($Com_Group in $AppAssignment){ - + if($Com_Group.target.deviceAndAppManagementAssignmentFilterId -eq $Filter.id){ Write-Host @@ -1198,7 +1210,7 @@ Write-Host "Status of each area of MEM that support Filters assignment status" Write-Host Write-Host "Applicable OS Type: " -NoNewline Write-Host $Filter.Platform -ForegroundColor Yellow -Write-Host +Write-Host Write-Host "Compliance Policies: " $CP_Count write-host "Device Configuration Policies: " $DCP_Count Write-Host "Settings Catalog Policies: " $SC_Count @@ -1217,3 +1229,4 @@ write-host "-------------------------------------------------------------------" Write-Host "Evaluation complete..." -ForegroundColor Green write-host "-------------------------------------------------------------------" Write-Host + diff --git a/Filters/IntuneFilter_Export.ps1 b/Filters/IntuneFilter_Export.ps1 index a1bdc5c..710dd0f 100644 --- a/Filters/IntuneFilter_Export.ps1 +++ b/Filters/IntuneFilter_Export.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-IntuneFilter(){ +function Get-IntuneFilter { <# .SYNOPSIS @@ -177,8 +229,8 @@ $Resource = "deviceManagement/assignmentFilters" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -201,7 +253,7 @@ $Resource = "deviceManagement/assignmentFilters" #################################################### -Function Export-JSONData(){ +function Export-JSONData { <# .SYNOPSIS @@ -224,7 +276,7 @@ $ExportPath try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON..." -f Red @@ -259,7 +311,7 @@ $ExportPath $JSON1 | Set-Content -LiteralPath "$ExportPath\$FileName_JSON" write-host "JSON created in $ExportPath\$FileName_JSON..." -f cyan - + } } @@ -276,50 +328,10 @@ $ExportPath #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -370,3 +382,4 @@ foreach($Filter in $Filters){ } Write-Host + diff --git a/Filters/IntuneFilter_Get.ps1 b/Filters/IntuneFilter_Get.ps1 index 08dde01..15aab3d 100644 --- a/Filters/IntuneFilter_Get.ps1 +++ b/Filters/IntuneFilter_Get.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-IntuneFilter(){ +function Get-IntuneFilter { <# .SYNOPSIS @@ -177,8 +229,8 @@ $Resource = "deviceManagement/assignmentFilters" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -203,50 +255,10 @@ $Resource = "deviceManagement/assignmentFilters" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -272,3 +284,4 @@ else { Write-Host } + diff --git a/Filters/IntuneFilter_Import_FromJSON.ps1 b/Filters/IntuneFilter_Import_FromJSON.ps1 index 17b91d5..9eac823 100644 --- a/Filters/IntuneFilter_Import_FromJSON.ps1 +++ b/Filters/IntuneFilter_Import_FromJSON.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-IntuneFilter(){ +function Add-IntuneFilter { <# .SYNOPSIS @@ -178,7 +230,7 @@ $Resource = "deviceManagement/assignmentFilters" try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the Device Configuration Policy..." -f Red @@ -188,13 +240,13 @@ $Resource = "deviceManagement/assignmentFilters" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -214,7 +266,7 @@ $Resource = "deviceManagement/assignmentFilters" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -249,7 +301,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -261,50 +313,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -335,7 +347,7 @@ $JSON_Convert = $JSON_Data | ConvertFrom-Json | Select-Object -Property * -Exclu $DisplayName = $JSON_Convert.displayName $JSON_Output = $JSON_Convert | ConvertTo-Json -Depth 5 - + write-host write-host "Intune Filter '$DisplayName' Found..." -ForegroundColor Yellow write-host @@ -343,3 +355,4 @@ $JSON_Output write-host Write-Host "Adding Intune Filter '$DisplayName'" -ForegroundColor Yellow Add-IntuneFilter -JSON $JSON_Output + diff --git a/IntuneDataExport/Export-IntuneData.ps1 b/IntuneDataExport/Export-IntuneData.ps1 index 5f5adfe..ad79e35 100644 --- a/IntuneDataExport/Export-IntuneData.ps1 +++ b/IntuneDataExport/Export-IntuneData.ps1 @@ -4,6 +4,198 @@ Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT See LICENSE in the project root for license information. #> + +function Connect-GraphAPI { +<# +.SYNOPSIS +Connects to Microsoft Graph API with appropriate scopes for Intune operations +.DESCRIPTION +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment +.NOTES +Requires Microsoft.Graph.Authentication module +#> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) + + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } + + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } + + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } + } + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false + } +} + +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, + + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', + + [Parameter(Mandatory = $false)] + [object]$Body = $null, + + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) + + try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } + + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } + + $results = @() + $nextLink = $Uri + + do { + Write-Verbose "Making request to: $nextLink" + + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } + + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } + + $response = Invoke-MgGraphRequest @requestParams + + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null + } + + } while ($nextLink) + + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" + } + else { + Write-Error "Graph API request failed: $errorMessage" + } + throw + } +} + +#################################################### + #################################################### param( @@ -54,7 +246,7 @@ function Log-Info ($message) { #################################################### function Log-Warning ($message) { - Write-Warning "[$([System.DateTime]::Now)] - $message" -WarningAction Continue + Write-Warning "[$([System.DateTime]::Now)] - $message" -WarningAction Continue } #################################################### @@ -73,154 +265,7 @@ function Log-FatalError($message) { #################################################### -function Get-AuthToken { - - <# - .SYNOPSIS - This function is used to authenticate with the Graph API REST interface - .DESCRIPTION - The function authenticate with the Graph API Interface with the tenant name - .EXAMPLE - Get-AuthToken - Authenticates you with the Graph API interface - .NOTES - NAME: Get-AuthToken - #> - - [cmdletbinding()] - - param - ( - [Parameter(Mandatory=$true)] - $User - ) - - $userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - - $tenant = $userUpn.Host - - Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - - # Getting path to ActiveDirectory Assemblies - # If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - - [System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - - [System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - - # Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information - # on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - - $clientId = "" - - $redirectUri = "urn:ietf:wg:oauth:2.0:oob" - - $resourceAppIdURI = "https://graph.microsoft.com" - - $authority = "https://login.microsoftonline.com/$Tenant" - - try { - - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" - - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $MethodArguments = [Type[]]@("System.String", "System.String", "System.Uri", "Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior", "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier") - $NonAsync = $AuthContext.GetType().GetMethod("AcquireToken", $MethodArguments) - - if ($NonAsync -ne $null) { - $authResult = $authContext.AcquireToken($resourceAppIdURI, $clientId, [Uri]$redirectUri, [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Auto, $userId) - } else { - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $clientId, [Uri]$redirectUri, $platformParameters, $userId).Result - } - - # If the accesstoken is valid then create the authentication header - - if($authResult.AccessToken){ - - # Creating header for Authorization token - - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn - } - - return $authHeader - - } - - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - - } - - } - - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break - - } - - } - + #################################################### function Get-MsGraphObject($Path, [switch]$IgnoreNotFound) { @@ -229,7 +274,7 @@ function Get-MsGraphObject($Path, [switch]$IgnoreNotFound) { try { return Invoke-RestMethod -Method Get -Uri $FullUri -Headers $AuthHeader - } + } catch { $Response = $_.Exception.Response if ($IgnoreNotFound -and $Response.StatusCode -eq "NotFound") { @@ -258,7 +303,7 @@ function Get-MsGraphCollection($Path) { $Result = Invoke-RestMethod -Method Get -Uri $NextLink -Headers $AuthHeader $Collection += $Result.value $NextLink = $Result.'@odata.nextLink' - } + } catch { $ResponseStream = $_.Exception.Response.GetResponseStream() $ResponseReader = New-Object System.IO.StreamReader $ResponseStream @@ -268,7 +313,7 @@ function Get-MsGraphCollection($Path) { Log-Error "Response Content:`n$ResponseContent" break } - } while ($NextLink -ne $null) + } while ($null -ne $NextLink) Log-Verbose "Got $($Collection.Count) object(s)" return $Collection @@ -289,7 +334,7 @@ function Post-MsGraphObject($Path, $RequestBody) { $Result = Invoke-RestMethod -Method Post -Uri $FullUri -Headers $AuthHeader -Body $RequestBodyJson return $Result - } + } catch { $ResponseStream = $_.Exception.Response.GetResponseStream() $ResponseReader = New-Object System.IO.StreamReader $ResponseStream @@ -317,7 +362,7 @@ function Test-IntuneUser { try { Invoke-RestMethod -Method Get -Uri "https://$MsGraphHost/$MsGraphVersion/users/$($UserId)/managedDevices" -Headers $AuthHeader - } + } catch { $Response = $_.Exception.Response if ($Response.StatusCode -eq "NotFound") { @@ -346,7 +391,7 @@ function Get-RegisteredDevices { function Get-ManagedDevices { Log-Info "Getting managed devices for User $UPN" - + $DeviceIds = @(Get-MsGraphCollection "users/$UserId/managedDevices?`$select=id" | Select-Object -ExpandProperty id) $Devices = @() @@ -390,7 +435,7 @@ function Get-ManagedDevices { function Get-AuditEvents { Log-Info "Getting audit events for User $UPN" - + return Get-MsGraphCollection "`deviceManagement/auditEvents?`$filter=actor/userPrincipalName eq '$UPN'" } @@ -398,7 +443,7 @@ function Get-AuditEvents { function Get-ManagedAppRegistrations { Log-Info "Getting managed app registrations for User $UPN" - + return Get-MsGraphCollection "`users/$UserId/managedAppRegistrations?`$expand=appliedPolicies,intendedPolicies,operations" } @@ -421,10 +466,10 @@ function Get-AppleDepSettings { #################################################### function Has-UserStatus($InstallSummary) { - return ($InstallSummary.installedUserCount -gt 0) -or - ($InstallSummary.failedUserCount -gt 0) -or - ($InstallSummary.notApplicableUserCount -gt 0) -or - ($InstallSummary.notInstalledUserCount -gt 0) -or + return ($InstallSummary.installedUserCount -gt 0) -or + ($InstallSummary.failedUserCount -gt 0) -or + ($InstallSummary.notApplicableUserCount -gt 0) -or + ($InstallSummary.notInstalledUserCount -gt 0) -or ($InstallSummary.pendingInstallUserCount -gt 0) } @@ -450,17 +495,17 @@ function Get-AppInstallStatuses { Log-Verbose "Getting App Install Status for App '$($App.displayName) $($App.Id)" $UserStatusesForApp = Get-MsGraphCollection "deviceAppManagement/mobileApps/$($App.id)/userStatuses" - $DeviceStatusesForApp = Get-MsGraphCollection "deviceAppManagement/mobileApps/$($App.id)/deviceStatuses" + $DeviceStatusesForApp = Get-MsGraphCollection "deviceAppManagement/mobileApps/$($App.id)/deviceStatuses" $DeviceStatusesForUser = @() - $DeviceStatusesForUser += $DeviceStatusesForApp | Where-Object { + $DeviceStatusesForUser += $DeviceStatusesForApp | Where-Object { $_.userPrincipalName -ieq $UPN } $UserStatusesForUser = @() - $UserStatusesForUser += $UserStatusesForApp | Where-Object { + $UserStatusesForUser += $UserStatusesForApp | Where-Object { $_.userPrincipalName -ieq $UPN } - + if ($UserStatusesForUser.Count -gt 0 -or $DeviceStatusesForUser.Count -gt 0) { Add-Member NoteProperty -InputObject $App -Name "deviceStatuses" -Value @() foreach ($UserStatus in $DeviceStatusesForUser) { @@ -490,17 +535,17 @@ function Get-EbookInstallStatuses { Log-Verbose "Getting Ebook Install Status for Ebook '$($Ebook.displayName) $($Ebook.Id)" $UserStatusesForEbook = Get-MsGraphCollection "deviceAppManagement/managedEBooks/$($Ebook.id)/userStateSummary" - $DeviceStatusesForEbook = Get-MsGraphCollection "deviceAppManagement/managedEBooks/$($Ebook.id)/deviceStates" + $DeviceStatusesForEbook = Get-MsGraphCollection "deviceAppManagement/managedEBooks/$($Ebook.id)/deviceStates" $DeviceStatusesForUser = @() - $DeviceStatusesForUser += $DeviceStatusesForEbook | Where-Object { + $DeviceStatusesForUser += $DeviceStatusesForEbook | Where-Object { $_.userName -ieq $UserDisplayName } $UserStatusesForUser = @() - $UserStatusesForUser += $UserStatusesForEbook | Where-Object { + $UserStatusesForUser += $UserStatusesForEbook | Where-Object { $_.userName -ieq $UserDisplayName } - + if ($UserStatusesForUser.Count -gt 0 -or $DeviceStatusesForUser.Count -gt 0) { Add-Member NoteProperty -InputObject $Ebook -Name "deviceStates" -Value @() foreach ($UserStatus in $DeviceStatusesForUser) { @@ -571,7 +616,7 @@ function Get-ManagedDeviceMobileAppConfigurationStatuses ($Devices) { Log-Info "Getting Mobile App Configurations Statuses for user $UPN" $MobileAppConfigurationsStatuses = @() $MobileAppConfigurations = Get-MsGraphCollection "deviceAppManagement/mobileAppConfigurations" - + $DeviceIds = $Devices | Select-Object -ExpandProperty id foreach ($MobileAppConfiguration in $MobileAppConfigurations) { @@ -580,15 +625,15 @@ function Get-ManagedDeviceMobileAppConfigurationStatuses ($Devices) { $DeviceStatusesForUser = @() - + foreach ($DeviceId in $DeviceIds) { - $DeviceStatusesForUser += $DeviceStatuses | Where-Object { + $DeviceStatusesForUser += $DeviceStatuses | Where-Object { $_.id.Contains($DeviceId) } } $UserStatusesForUser = @() - $UserStatusesForUser += $UserStatuses | Where-Object { + $UserStatusesForUser += $UserStatuses | Where-Object { $_.userPrincipalName -ieq $UPN } @@ -613,7 +658,7 @@ function Get-DeviceManagementScriptRunStates ($ManagedDevices){ $UserRunStates = Get-MsGraphCollection "deviceManagement/deviceManagementScripts/$($DeviceManagementScript.id)/userRunStates" $UserRunStatesForUser = @() - $UserRunStatesForUser += $UserRunStates | Where-Object { + $UserRunStatesForUser += $UserRunStates | Where-Object { $_.userPrincipalName -ieq $UPN } @@ -638,7 +683,7 @@ function Export-RemainingData{ if ($DataItem.data -ne $null) { $Entities = @($DataItem.data) - if ($Entities.Count -gt 0) + if ($Entities.Count -gt 0) { Log-Info "Found $($Entities.Count) $($DataItem.displayName)" $CollectionName = $DataItem.displayName @@ -648,7 +693,7 @@ function Export-RemainingData{ } $EntityName = $CollectionName.TrimEnd('s') - Export-Collection -CollectionType $CollectionName -ObjectType $EntityName -Collection $Entities + Export-Collection -CollectionType $CollectionName -ObjectType $EntityName -Collection $Entities } else { Log-Info "No $($DataItem.displayName) data found" @@ -712,20 +757,20 @@ function Get-ManagedAppConfigurationStatusReport { function Filter-ManagedAppReport { param($Report) #Filter the report summary to only the target user - if ($Report -ne $null -and $Report.content -ne $null) + if ($null -ne $Report -and $Report.content -ne $null) { $HeaderCount = $Report.content.header.Count $DataRows = $Report.content.body.values $FilteredDataRows = @() - if ($DataRows.Count -eq $HeaderCount) + if ($DataRows.Count -eq $HeaderCount) { # Special case for only one row of data if ($DataRows[0] -ieq $UserId) { $FilteredDataRows += @($DataRows) } - } elseif ($DataRows -ne $null -and $DataRows.Count -gt 0) { + } elseif ($null -ne $DataRows -and $DataRows.Count -gt 0) { foreach ($DataRow in $DataRows) { if ($DataRow[0] -ieq $UserId) { $FilteredDataRows += $DataRow @@ -794,7 +839,7 @@ function Export-ChromeOSDeviceReportData { $FilterString = "(MostRecentUserEmail eq '" + $UPN + "')" - $ChromeRequestBody = @{ + $ChromeRequestBody = @{ reportName = "ChromeOSDevices" localizationType = "LocalizedValuesAsAdditionalColumn" filter = $FilterString @@ -820,7 +865,7 @@ function Export-ObjectJson($ObjectType, $Object) { function Export-ObjectCSV($ObjectType, $Object) { Log-Info "Writing $ObjectType data to $(Join-Path $OutputPath "$ObjectType.csv")" - $Object | Export-Csv -NoTypeInformation -Path (Join-Path $OutputPath "$ObjectType.csv") -Encoding utf8 + $Object | Export-Csv -NoTypeInformation -Path (Join-Path $OutputPath "$ObjectType.csv") -Encoding utf8 } #################################################### @@ -880,7 +925,7 @@ function Export-Collection ($CollectionType, $ObjectType, $Collection) { if ($ExportFormat -eq "CSV") { $ExportPath = (Join-Path $OutputPath "$CollectionType.csv") - $Collection | Export-Csv -NoTypeInformation -Path $ExportPath -Encoding utf8 + $Collection | Export-Csv -NoTypeInformation -Path $ExportPath -Encoding utf8 Log-Info "Exported $($Collection.Count) $CollectionType to $ExportPath" } } @@ -897,7 +942,7 @@ function Filter-Entity { Log-Verbose "Filtering entity $EntityName" - if ($Entity -eq $null) { + if ($null -eq $Entity) { return } @@ -922,12 +967,12 @@ function Filter-Entity { foreach ($PropertyToRemove in $PropertiesToRemove) { $Entity.PSObject.Properties.Remove($PropertyToRemove) - } + } foreach ($PropertyToRename in $PropertiesToRename) { $OldPropertyName = $PropertyToRename | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name - #Check if the old property exists on the entity + #Check if the old property exists on the entity $OldPropertyExists = (($Entity | Get-Member -MemberType NoteProperty -Name $OldPropertyName) -ne $null) if (-not $OldPropertyExists) { @@ -989,11 +1034,11 @@ if ($All) { Log-Info "All data will be exported" } elseif ($IncludeAzureAD) { Log-Info "Including AzureAD data in export" -} +} #################################################### -$AuthHeader = Get-AuthToken -User $Username +$AuthHeader = Connect-GraphAPIr $Username #################################################### @@ -1009,7 +1054,7 @@ if ($IncludeNonAzureADUpn -or $All) { $User = Get-User -if ($User -eq $null) { +if ($null -eq $User) { Log-Warning "Azure AD User with UPN $UPN was not found" return } @@ -1077,7 +1122,7 @@ Export-Collection "iOSUpdateStatus" "iOSUpdateStatuses" $IosUpdateStatues $ManagedDeviceMobileAppConfigurationStatuses = Get-ManagedDeviceMobileAppConfigurationStatuses $ManagedDevices Export-Collection "MobileAppConfigurationStatuses" "MobileAppConfigurationStatus" $ManagedDeviceMobileAppConfigurationStatuses -$DeviceManagementScriptRunStates = Get-DeviceManagementScriptRunStates +$DeviceManagementScriptRunStates = Get-DeviceManagementScriptRunStates Export-Collection "DeviceManagementScriptRunState" "DeviceManagementScriptRunStates" $DeviceManagementScriptRunStates $AppProtectionUserStatus = Get-AppProtectionUserStatuses @@ -1099,3 +1144,4 @@ Export-RemainingData Log-Info "Export complete, files can be found at $OutputPath" Write-Host + diff --git a/LOB_Application/Application_LOB_Add.ps1 b/LOB_Application/Application_LOB_Add.ps1 index 9fe7b55..184135c 100644 --- a/LOB_Application/Application_LOB_Add.ps1 +++ b/LOB_Application/Application_LOB_Add.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,149 +7,201 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } -} -  #################################################### function CloneObject($object){ @@ -182,7 +234,7 @@ function MakeGetRequest($collectionPath){ $uri = "$baseUrl$collectionPath"; $request = "GET $uri"; - + if ($logRequestUris) { Write-Host $request; } if ($logHeaders) { WriteHeaders $authToken; } @@ -221,7 +273,7 @@ function MakeRequest($verb, $collectionPath, $body){ $uri = "$baseUrl$collectionPath"; $request = "$verb $uri"; - + $clonedHeaders = CloneObject $authToken; $clonedHeaders["content-length"] = $body.Length; $clonedHeaders["content-type"] = "application/json"; @@ -357,8 +409,8 @@ function GenerateKey{ } finally { - if ($aesProvider -ne $null) { $aesProvider.Dispose(); } - if ($aes -ne $null) { $aes.Dispose(); } + if ($null -ne $aesProvider) { $aesProvider.Dispose(); } + if ($null -ne $aes) { $aes.Dispose(); } } } @@ -373,7 +425,7 @@ function GenerateIV{ } finally { - if ($aes -ne $null) { $aes.Dispose(); } + if ($null -ne $aes) { $aes.Dispose(); } } } @@ -413,9 +465,9 @@ function EncryptFileWithIV($sourceFile, $targetFile, $encryptionKey, $hmacKey, $ } finally { - if ($cryptoStream -ne $null) { $cryptoStream.Dispose(); } - if ($sourceStream -ne $null) { $sourceStream.Dispose(); } - if ($encryptor -ne $null) { $encryptor.Dispose(); } + if ($null -ne $cryptoStream) { $cryptoStream.Dispose(); } + if ($null -ne $sourceStream) { $sourceStream.Dispose(); } + if ($null -ne $encryptor) { $encryptor.Dispose(); } } try @@ -434,13 +486,13 @@ function EncryptFileWithIV($sourceFile, $targetFile, $encryptionKey, $hmacKey, $ } finally { - if ($finalStream -ne $null) { $finalStream.Dispose(); } + if ($null -ne $finalStream) { $finalStream.Dispose(); } } } finally { - if ($targetStream -ne $null) { $targetStream.Dispose(); } - if ($aes -ne $null) { $aes.Dispose(); } + if ($null -ne $targetStream) { $targetStream.Dispose(); } + if ($null -ne $aes) { $aes.Dispose(); } } $computedMac; @@ -464,7 +516,7 @@ function EncryptFile($sourceFile, $targetFile){ { $fileDigestBytes[$i / 2] = [System.Convert]::ToByte($fileDigest.Substring($i, 2), 16); } - + # Return an object that will serialize correctly to the file commit Graph API. $encryptionInfo = @{}; $encryptionInfo.encryptionKey = [System.Convert]::ToBase64String($encryptionKey); @@ -512,7 +564,7 @@ function WaitForFileProcessing($fileUri, $stage){ $attempts--; } - if ($file -eq $null) + if ($null -eq $file) { throw "File request did not complete in the allotted time."; } @@ -533,17 +585,17 @@ function GetAndroidAppBody($displayName, $publisher, $description, $filename, $i $body.fileName = $filename; $body.identityName = $identityName; $body.identityVersion = $identityVersion; - - if ($minimumSupportedOperatingSystem -eq $null){ + + if ($null -eq $minimumSupportedOperatingSystem){ $body.minimumSupportedOperatingSystem = @{ "v4_4" = $true }; - + } - + else { $body.minimumSupportedOperatingSystem = $minimumSupportedOperatingSystem; - + } $body.informationUrl = $null; @@ -571,7 +623,7 @@ function GetiOSAppBody($displayName, $publisher, $description, $filename, $bundl $body.fileName = $filename; $body.bundleId = $bundleId; $body.identityVersion = $identityVersion; - if ($minimumSupportedOperatingSystem -eq $null) + if ($null -eq $minimumSupportedOperatingSystem) { $body.minimumSupportedOperatingSystem = @{ "v9_0" = $true }; } @@ -605,7 +657,7 @@ function GetMSIAppBody($displayName, $publisher, $description, $filename, $ident $body.informationUrl = $null; $body.isFeatured = $false; $body.privacyInformationUrl = $null; - $body.developer = ""; + $body.developer = ""; $body.notes = ""; $body.owner = ""; $body.productCode = "$ProductCode"; @@ -640,7 +692,7 @@ function GetAppCommitBody($contentVersionId, $LobType){ #################################################### -Function Get-MSIFileInformation(){ +function Get-MSIFileInformation { # https://www.scconfigmgr.com/2014/08/22/how-to-get-msi-file-information-with-powershell/ @@ -648,7 +700,7 @@ param(     [parameter(Mandatory=$true)]     [ValidateNotNullOrEmpty()]     [System.IO.FileInfo]$Path, -  +     [parameter(Mandatory=$true)]     [ValidateNotNullOrEmpty()]     [ValidateSet("ProductCode", "ProductVersion", "ProductName", "Manufacturer", "ProductLanguage", "FullVersion")] @@ -665,13 +717,13 @@ Process {         $View.GetType().InvokeMember("Execute", "InvokeMethod", $null, $View, $null)         $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $View, $null)         $Value = $Record.GetType().InvokeMember("StringData", "GetProperty", $null, $Record, 1) -  +         # Commit database and close view         $MSIDatabase.GetType().InvokeMember("Commit", "InvokeMethod", $null, $MSIDatabase, $null) -        $View.GetType().InvokeMember("Close", "InvokeMethod", $null, $View, $null)           +        $View.GetType().InvokeMember("Close", "InvokeMethod", $null, $View, $null)         $MSIDatabase = $null         $View = $null -  +         # Return the value         return $Value     } @@ -680,7 +732,7 @@ Process {      Write-Warning -Message $_.Exception.Message; break; -     + } } @@ -695,7 +747,7 @@ Process { #################################################### -Function Test-SourceFile(){ +function Test-SourceFile { param ( @@ -799,7 +851,7 @@ if ($logContent) { Write-Host -ForegroundColor Gray $PackageInfo[3].Split("'")[1 #################################################### -function Upload-AndroidLob(){ +function Upload-AndroidLob { <# .SYNOPSIS @@ -847,7 +899,7 @@ param try { - + $LOBType = "microsoft.graph.androidLOBApp" Write-Host "Testing if SourceFile '$SourceFile' Path is valid..." -ForegroundColor Yellow @@ -903,7 +955,7 @@ param Write-Host Write-Host "Creating JSON data to pass to the service..." -ForegroundColor Yellow $mobileAppBody = GetAndroidAppBody "$displayName" "$Publisher" "$Description" "$filename" "$identityName" "$identityVersion" "$versionName"; - + Write-Host Write-Host "Creating application in Intune..." -ForegroundColor Yellow $mobileApp = MakePostRequest "mobileApps" ($mobileAppBody | ConvertTo-Json); @@ -944,7 +996,7 @@ param $fileBody = GetAppFileBody "$filename" $Size $EncrySize "$EncodedText"; $filesUri = "mobileApps/$appId/$LOBType/contentVersions/$contentVersionId/files"; $file = MakePostRequest $filesUri ($fileBody | ConvertTo-Json); - + # Wait for the service to process the new file request. Write-Host Write-Host "Waiting for the file entry URI to be created..." -ForegroundColor Yellow @@ -955,7 +1007,7 @@ param # Upload the content to Azure Storage. Write-Host Write-Host "Uploading file to Azure Storage URI..." -ForegroundColor Yellow - + $sasUri = $file.azureStorageUri; UploadFileToAzureStorage $file.azureStorageUri $tempFile; @@ -995,7 +1047,7 @@ param #################################################### -function Upload-iOSLob(){ +function Upload-iOSLob { <# .SYNOPSIS @@ -1048,7 +1100,7 @@ param try { - + $LOBType = "microsoft.graph.iosLOBApp" Write-Host "Testing if SourceFile '$SourceFile' Path is valid..." -ForegroundColor Yellow @@ -1068,7 +1120,7 @@ param # Creating temp file name from Source File path $tempFile = [System.IO.Path]::GetDirectoryName("$SourceFile") + "\" + [System.IO.Path]::GetFileNameWithoutExtension("$SourceFile") + "_temp.bin" - + # Creating filename variable from Source File Path $filename = [System.IO.Path]::GetFileName("$SourceFile") @@ -1115,7 +1167,7 @@ param $fileBody = GetAppFileBody "$filename" $Size $EncrySize "$EncodedText"; $filesUri = "mobileApps/$appId/$LOBType/contentVersions/$contentVersionId/files"; $file = MakePostRequest $filesUri ($fileBody | ConvertTo-Json); - + # Wait for the service to process the new file request. Write-Host Write-Host "Waiting for the file entry URI to be created..." -ForegroundColor Yellow @@ -1166,7 +1218,7 @@ param #################################################### -function Upload-MSILob(){ +function Upload-MSILob { <# .SYNOPSIS @@ -1221,7 +1273,7 @@ param # Create a new MSI LOB app. $mobileAppBody = GetMSIAppBody -displayName "$PN" -publisher "$publisher" -description "$description" -filename "$FileName" -identityVersion "$PV" -ProductCode "$PC" - + Write-Host Write-Host "Creating application in Intune..." -ForegroundColor Yellow $mobileApp = MakePostRequest "mobileApps" ($mobileAppBody | ConvertTo-Json); @@ -1259,7 +1311,7 @@ param $fileBody = GetAppFileBody "$FileName" $Size $EncrySize "$EncodedText"; $filesUri = "mobileApps/$appId/$LOBType/contentVersions/$contentVersionId/files"; $file = MakePostRequest $filesUri ($fileBody | ConvertTo-Json); - + # Wait for the service to process the new file request. Write-Host Write-Host "Waiting for the file entry URI to be created..." -ForegroundColor Yellow @@ -1301,12 +1353,12 @@ param Write-Host } - + catch { Write-Host ""; Write-Host -ForegroundColor Red "Aborting with exception: $($_.Exception.ToString())"; - + } } @@ -1315,50 +1367,10 @@ param #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -1369,7 +1381,7 @@ $global:authToken = Get-AuthToken -User $User # Note: Don't specify direct location to build folder number, just to the build-tools folder as the script will find the latest SDK installed $AndroidSDKLocation = "C:\AndroidSDK\build-tools" -$baseUrl = "https://graph.microsoft.com/beta/deviceAppManagement/" +$baseUrl = "beta/deviceAppManagement/" $logRequestUris = $true; $logHeaders = $false; @@ -1390,3 +1402,4 @@ $sleep = 30 #### iOS # Upload-iOSLob -sourceFile "C:\Software\iOS\MyApp.ipa" -displayName "MyApp.ipa" -publisher "MyApp" -description "MyApp" -bundleId "com.microsoft.myApp" -identityVersion "1.0.0.0" -versionNumber "3.0.0" -expirationDateTime "2018-03-14T20:53:52Z" + diff --git a/LOB_Application/Win32_Application_Add.ps1 b/LOB_Application/Win32_Application_Add.ps1 index 2587701..4d1a15a 100644 --- a/LOB_Application/Win32_Application_Add.ps1 +++ b/LOB_Application/Win32_Application_Add.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,149 +7,201 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementApps.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } -} -  #################################################### function CloneObject($object){ @@ -182,7 +234,7 @@ function MakeGetRequest($collectionPath){ $uri = "$baseUrl$collectionPath"; $request = "GET $uri"; - + if ($logRequestUris) { Write-Host $request; } if ($logHeaders) { WriteHeaders $authToken; } @@ -222,7 +274,7 @@ function MakeRequest($verb, $collectionPath, $body){ $uri = "$baseUrl$collectionPath"; $request = "$verb $uri"; - + $clonedHeaders = CloneObject $authToken; $clonedHeaders["content-length"] = $body.Length; $clonedHeaders["content-type"] = "application/json"; @@ -310,16 +362,16 @@ function UploadFileToAzureStorage($sasUri, $filepath, $fileUri){ try { $chunkSizeInBytes = 1024l * 1024l * $azureStorageUploadChunkSizeInMb; - + # Start the timer for SAS URI renewal. $sasRenewalTimer = [System.Diagnostics.Stopwatch]::StartNew() - + # Find the file size and open the file. $fileSize = (Get-Item $filepath).length; $chunks = [Math]::Ceiling($fileSize / $chunkSizeInBytes); $reader = New-Object System.IO.BinaryReader([System.IO.File]::Open($filepath, [System.IO.FileMode]::Open)); $position = $reader.BaseStream.Seek(0, [System.IO.SeekOrigin]::Begin); - + # Upload each chunk. Check whether a SAS URI renewal is required after each chunk is uploaded and renew if needed. $ids = @(); @@ -331,20 +383,20 @@ function UploadFileToAzureStorage($sasUri, $filepath, $fileUri){ $start = $chunk * $chunkSizeInBytes; $length = [Math]::Min($chunkSizeInBytes, $fileSize - $start); $bytes = $reader.ReadBytes($length); - - $currentChunk = $chunk + 1; + + $currentChunk = $chunk + 1; Write-Progress -Activity "Uploading File to Azure Storage" -status "Uploading chunk $currentChunk of $chunks" ` -percentComplete ($currentChunk / $chunks*100) $uploadResponse = UploadAzureStorageChunk $sasUri $id $bytes; - + # Renew the SAS URI if 7 minutes have elapsed since the upload started or was renewed last. if ($currentChunk -lt $chunks -and $sasRenewalTimer.ElapsedMilliseconds -ge 450000){ $renewalResponse = RenewAzureStorageUpload $fileUri; $sasRenewalTimer.Restart(); - + } } @@ -357,10 +409,10 @@ function UploadFileToAzureStorage($sasUri, $filepath, $fileUri){ finally { - if ($reader -ne $null) { $reader.Dispose(); } - + if ($null -ne $reader) { $reader.Dispose(); } + } - + # Finalize the upload. $uploadResponse = FinalizeAzureStorageUpload $sasUri $ids; @@ -373,7 +425,7 @@ function RenewAzureStorageUpload($fileUri){ $renewalUri = "$fileUri/renewUpload"; $actionBody = ""; $rewnewUriResult = MakePostRequest $renewalUri $actionBody; - + $file = WaitForFileProcessing $fileUri "AzureStorageUriRenewal" $azureStorageRenewSasUriBackOffTimeInSeconds; } @@ -409,7 +461,7 @@ function WaitForFileProcessing($fileUri, $stage){ $attempts--; } - if ($file -eq $null -or $file.uploadState -ne $successState) + if ($null -eq $file -or $file.uploadState -ne $successState) { throw "File request did not complete in the allotted time."; } @@ -419,7 +471,7 @@ function WaitForFileProcessing($fileUri, $stage){ #################################################### -function GetWin32AppBody(){ +function GetWin32AppBody { param ( @@ -575,7 +627,7 @@ function GetAppCommitBody($contentVersionId, $LobType){ #################################################### -Function Test-SourceFile(){ +function Test-SourceFile { param ( @@ -608,7 +660,7 @@ param #################################################### -Function New-DetectionRule(){ +function New-DetectionRule { [cmdletbinding()] @@ -641,11 +693,11 @@ param [parameter(Mandatory=$true,ParameterSetName = "MSI")] [ValidateNotNullOrEmpty()] [String]$MSIproductCode, - + [parameter(Mandatory=$true,ParameterSetName = "File")] [ValidateNotNullOrEmpty()] [String]$Path, - + [parameter(Mandatory=$true,ParameterSetName = "File")] [ValidateNotNullOrEmpty()] [string]$FileOrFolderName, @@ -682,7 +734,7 @@ param if($PowerShell){ if(!(Test-Path "$ScriptFile")){ - + Write-Host Write-Host "Could not find file '$ScriptFile'..." -ForegroundColor Red Write-Host "Script can't continue..." -ForegroundColor Red @@ -690,18 +742,18 @@ param break } - + $ScriptContent = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes("$ScriptFile")); - + $DR = @{ "@odata.type" = "#microsoft.graph.win32LobAppPowerShellScriptDetection" } $DR.enforceSignatureCheck = $false; $DR.runAs32Bit = $false; $DR.scriptContent = "$ScriptContent"; } - + elseif($MSI){ - + $DR = @{ "@odata.type" = "#microsoft.graph.win32LobAppProductCodeDetection" } $DR.productVersionOperator = "notConfigured"; $DR.productCode = "$MsiProductCode"; @@ -710,7 +762,7 @@ param } elseif($File){ - + $DR = @{ "@odata.type" = "#microsoft.graph.win32LobAppFileSystemDetection" } $DR.check32BitOn64System = "$check32BitOn64System"; $DR.detectionType = "$FileDetectionType"; @@ -722,7 +774,7 @@ param } elseif($Registry){ - + $DR = @{ "@odata.type" = "#microsoft.graph.win32LobAppRegistryDetection" } $DR.check32BitOn64System = "$check32BitRegOn64System"; $DR.detectionType = "$RegistryDetectionType"; @@ -739,7 +791,7 @@ param #################################################### -function Get-DefaultReturnCodes(){ +function Get-DefaultReturnCodes { @{"returnCode" = 0;"type" = "success"}, ` @{"returnCode" = 1707;"type" = "success"}, ` @@ -751,7 +803,7 @@ function Get-DefaultReturnCodes(){ #################################################### -function New-ReturnCode(){ +function New-ReturnCode { param ( @@ -768,7 +820,7 @@ $type #################################################### -Function Get-IntuneWinXML(){ +function Get-IntuneWinXML { param ( @@ -808,7 +860,7 @@ if($removeitem -eq "true"){ remove-item "$Directory\$filename" } #################################################### -Function Get-IntuneWinFile(){ +function Get-IntuneWinFile { param ( @@ -849,7 +901,7 @@ $fileName, #################################################### -function Upload-Win32Lob(){ +function Upload-Win32Lob { <# .SYNOPSIS @@ -922,7 +974,7 @@ param # If displayName input don't use Name from detection.xml file if($displayName){ $DisplayName = $displayName } else { $DisplayName = $DetectionXML.ApplicationInfo.Name } - + $FileName = $DetectionXML.ApplicationInfo.FileName $SetupFileName = $DetectionXML.ApplicationInfo.SetupFile @@ -942,7 +994,7 @@ param $MsiPublisher = $DetectionXML.ApplicationInfo.MsiInfo.MsiPublisher $MsiRequiresReboot = $DetectionXML.ApplicationInfo.MsiInfo.MsiRequiresReboot $MsiUpgradeCode = $DetectionXML.ApplicationInfo.MsiInfo.MsiUpgradeCode - + if($MsiRequiresReboot -eq "false"){ $MsiRequiresReboot = $false } elseif($MsiRequiresReboot -eq "true"){ $MsiRequiresReboot = $true } @@ -992,7 +1044,7 @@ param #ReturnCodes if($returnCodes){ - + $mobileAppBody | Add-Member -MemberType NoteProperty -Name 'returnCodes' -Value @($returnCodes) } @@ -1047,7 +1099,7 @@ param $fileBody = GetAppFileBody "$FileName" $Size $EncrySize $null; $filesUri = "mobileApps/$appId/$LOBType/contentVersions/$contentVersionId/files"; $file = MakePostRequest $filesUri ($fileBody | ConvertTo-Json); - + # Wait for the service to process the new file request. Write-Host Write-Host "Waiting for the file entry URI to be created..." -ForegroundColor Yellow @@ -1087,20 +1139,20 @@ param Write-Host "Sleeping for $sleep seconds to allow patch completion..." -f Magenta Start-Sleep $sleep Write-Host - + } - + catch { Write-Host ""; Write-Host -ForegroundColor Red "Aborting with exception: $($_.Exception.ToString())"; - + } } #################################################### -Function Test-AuthToken(){ +function Test-AuthToken { # Checking if authToken exists before running authentication if($global:authToken){ @@ -1118,23 +1170,23 @@ Function Test-AuthToken(){ # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - if($User -eq $null -or $User -eq ""){ + if($null -eq $User -or "" -eq $User){ $Global:User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" Write-Host } - $global:authToken = Get-AuthToken -User $User + $global:authToken = Connect-GraphAPIr $User } } - # Authentication doesn't exist, calling Get-AuthToken function + # Authentication doesn't exist, calling Connect-GraphAPInction else { - if($User -eq $null -or $User -eq ""){ + if($null -eq $User -or "" -eq $User){ $Global:User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" Write-Host @@ -1142,7 +1194,7 @@ Function Test-AuthToken(){ } # Getting the authorization token - $global:authToken = Get-AuthToken -User $User + $global:authToken = Connect-GraphAPIr $User } } @@ -1153,7 +1205,7 @@ Test-AuthToken #################################################### -$baseUrl = "https://graph.microsoft.com/beta/deviceAppManagement/" +$baseUrl = "beta/deviceAppManagement/" $logRequestUris = $true; $logHeaders = $false; @@ -1196,3 +1248,4 @@ Upload-Win32Lob -SourceFile "$SourceFile" -publisher "Publisher" ` -uninstallCmdLine "powershell.exe .\uninstall.ps1" #################################################### + diff --git a/MDATP/Configure-MDATPIntuneSecAdminRole.ps1 b/MDATP/Configure-MDATPIntuneSecAdminRole.ps1 index d39df56..731f541 100644 --- a/MDATP/Configure-MDATPIntuneSecAdminRole.ps1 +++ b/MDATP/Configure-MDATPIntuneSecAdminRole.ps1 @@ -16,13 +16,13 @@ .DESCRIPTION Configures MDATP Intune environment by creating a custom role and assignment with permissions to read security baseline data and machine onboarding data. - Populates the role assignment with security groups provided by the SecurityGroupList parameter. + Populates the role assignment with security groups provided by the SecurityGroupList parameter. Any users or groups added to the new role assignment will inherit the permissions of the role and gain read access to security baseline data and machine onboarding data. - Use an elevated command prompt (run as local admin) from a machine with access to your Microsoft Defender ATP environment. + Use an elevated command prompt (run as local admin) from a machine with access to your Microsoft Defender ATP environment. The script needs to run as local admin to install the Azure AD PowerShell module if not already present. .PARAMETER AdminUser - User with global admin privileges in your Intune environment + User with global admin privileges in your Intune environment .PARAMETER SecAdminGroup Security group name - Security group that contains SecAdmin users. Supports only one group. Create a group first if needed. Specify SecAdminGroup param or SecurityGroupList param, but not both. @@ -62,6 +62,198 @@ Param( ) + +function Connect-GraphAPI { +<# +.SYNOPSIS +Connects to Microsoft Graph API with appropriate scopes for Intune operations +.DESCRIPTION +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment +.NOTES +Requires Microsoft.Graph.Authentication module +#> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) + + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } + + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } + + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } + } + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false + } +} + +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, + + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', + + [Parameter(Mandatory = $false)] + [object]$Body = $null, + + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) + + try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } + + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } + + $results = @() + $nextLink = $Uri + + do { + Write-Verbose "Making request to: $nextLink" + + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } + + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } + + $response = Invoke-MgGraphRequest @requestParams + + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null + } + + } while ($nextLink) + + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" + } + else { + Write-Error "Graph API request failed: $errorMessage" + } + throw + } +} + +#################################################### + #################################################### # Parameters #################################################### @@ -74,159 +266,19 @@ if ($SecurityGroupList){ $AADEnvironment = (New-Object "System.Net.Mail.MailAddress" -ArgumentList $AdminUser).Host -$RBACRoleName = "MDATP SecAdmin" -$SecurityGroup = "MDATP SecAdmin SG" +$RBACRoleName = "MDATP SecAdmin" +$SecurityGroup = "MDATP SecAdmin SG" $User = $AdminUser #################################################### # Functions #################################################### -function Get-AuthToken { - <# - .SYNOPSIS - This function is used to authenticate with the Graph API REST interface - .DESCRIPTION - The function authenticate with the Graph API Interface with the tenant name - .EXAMPLE - Get-AuthToken - Authenticates you with the Graph API interface - .NOTES - NAME: Get-AuthToken - #> - - [cmdletbinding()] - - param - ( - [Parameter(Mandatory=$true)] - $User - ) - - $userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - - $tenant = $userUpn.Host - - Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - Write-Host - Write-Host "AzureAD Powershell module not installed..." -f Red - Write-Host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - Write-Host "Script can't continue..." -f Red - Write-Host - exit - } - - # Getting path to ActiveDirectory Assemblies - # If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | Select-Object version | Sort-Object)[-1] - - $aadModule = $AadModule | Where-Object { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | Select-Object -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - - [System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - - [System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - - # Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information - # on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - - $clientId = "" - - $redirectUri = "urn:ietf:wg:oauth:2.0:oob" - - $resourceAppIdURI = "https://graph.microsoft.com" - - $authority = "https://login.microsoftonline.com/$Tenant" - - try { - - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" - - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result - - # If the accesstoken is valid then create the authentication header - - if($authResult.AccessToken){ - - # Creating header for Authorization token - - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn - } - - return $authHeader - - } - - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - - } - - } - - catch { - - Write-Host $_.Exception.Message -f Red - Write-Host $_.Exception.ItemName -f Red - Write-Host - break - - } - - } - #################################################### - -Function Test-JSON(){ - + +function Test-JSON { + <# .SYNOPSIS This function is used to test if the JSON passed to a REST Post request is valid @@ -238,39 +290,39 @@ Test if the JSON is valid before calling the Graph REST interface .NOTES NAME: Test-JSON #> - + param ( - + $JSON - + ) - + try { - + $TestJSON = ConvertFrom-Json $JSON -ErrorAction Stop $validJson = $true - + } - + catch { - + $validJson = $false $_.Exception - + } - + if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break - + } - + } - + #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -283,50 +335,50 @@ Function Get-AADGroup(){ .NOTES NAME: Get-AADGroup #> - + [cmdletbinding()] - + param ( $GroupName, $id, [switch]$Members ) - + # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ $GID = $Group.id @@ -334,13 +386,13 @@ Function Get-AADGroup(){ $Group.displayName Write-Host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } } @@ -359,12 +411,12 @@ Function Get-AADGroup(){ break } - + } #################################################### -Function Add-RBACRole(){ +function Add-RBACRole { <# .SYNOPSIS @@ -398,8 +450,8 @@ $Resource = "deviceManagement/roleDefinitions" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $Json -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -419,10 +471,10 @@ $Resource = "deviceManagement/roleDefinitions" } } - + #################################################### -Function Get-RBACRole(){ +function Get-RBACRole { <# .SYNOPSIS @@ -435,38 +487,38 @@ Function Get-RBACRole(){ .NOTES NAME: Get-RBACRole #> - + [cmdletbinding()] - + param ( $Name ) - + $graphApiVersion = "v1.0" $Resource = "deviceManagement/roleDefinitions" - + try { - + if($Name){ $QueryString = "?`$filter=contains(displayName, '$Name')" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)$($QueryString)" - $rbacRoles = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)$($QueryString)" + $rbacRoles = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value $customRbacRoles = $rbacRoles | Where-Object { $_isBuiltInRoleDefinition -eq $false } return $customRbacRoles } - + else { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -477,14 +529,14 @@ Function Get-RBACRole(){ Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" Write-Host break - + } - + } #################################################### - -Function Assign-RBACRole(){ + +function Assign-RBACRole { <# .SYNOPSIS @@ -510,7 +562,7 @@ param $graphApiVersion = "Beta" $Resource = "deviceManagement/roleAssignments" - + try { if(!$Id){ @@ -549,15 +601,15 @@ $JSON = @" "displayName":"$DisplayName", "members":["$MemberGroupId"], "scopeMembers":["$TargetGroupId"], - "roleDefinition@odata.bind":"https://graph.microsoft.com/beta/deviceManagement/roleDefinitions('$ID')" + "roleDefinition@odata.bind":"beta/deviceManagement/roleDefinitions('$ID')" } "@ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" - + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON + } - + catch { $ex = $_.Exception @@ -578,55 +630,15 @@ $JSON = @" #################################################### #region Authentication - -Write-Host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - Write-Host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - Write-Host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your Global Admin user for Azure Authentication (e.g. globaladmin@myenvironment.onmicrosoft.com):" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your Global Admin user for Azure Authentication (e.g. globaladmin@myenvironment.onmicrosoft.com):" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - + +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } - + #endregion - + #################################################### $JSON = @" @@ -654,7 +666,7 @@ $JSON = @" "isBuiltInRoleDefinition": false } "@ - + #################################################### # Main #################################################### @@ -709,7 +721,7 @@ if($SecAdminGroup){ Write-Host "AAD Group '$SecAdminGroup' exists" -ForegroundColor Green Write-Host "" Write-Host "Adding AAD group $SecAdminGroup - $ValidatedSecAdminGroup to MDATP Role..." -ForegroundColor Yellow - + # Verify security group list only contains valid GUIDs try { @@ -718,13 +730,13 @@ if($SecAdminGroup){ Write-Host } - + catch { - + Write-Host "ObjectId: $ValidatedSecAdminGroup is not a valid ObjectId" -ForegroundColor Red Write-Host "Verify that your security group list only contains valid ObjectIds and try again." -ForegroundColor Cyan exit -1 - + } Write-Host "Adding security group to RBAC role $RBACRoleName ..." -ForegroundColor Yellow @@ -733,13 +745,13 @@ if($SecAdminGroup){ # NOTE: TargetGroupID = Scope Group } - + else { Write-Host "Group '$SecAdminGroup' does not exist. Please run script again and specify a valid group." -ForegroundColor Red Write-Host break - + } } @@ -751,21 +763,21 @@ if($SecurityGroupList){ Write-Host "Validating Security Groups to add to Intune Role:" -ForegroundColor Yellow foreach ($SecurityGroup in $SecurityGroupList) { - + # Verify security group list only contains valid GUIDs try { [System.Guid]::Parse($SecurityGroup) | Out-Null Write-Host "ObjectId: $SecurityGroup" -ForegroundColor Green - + } - + catch { Write-Host "ObjectId: $SecurityGroup is not a valid ObjectId" -ForegroundColor Red Write-Host "Verify that your security group list only contains valid ObjectIds and try again." -ForegroundColor Cyan exit -1 - + } } @@ -808,3 +820,4 @@ Write-Host "Add users and groups to the new role assignment 'MDATP RBAC Assignme Write-Host Write-Host "Configuration of MDATP Intune SecAdmin Role complete..." -ForegroundColor Green Write-Host + diff --git a/MDATP/MacOS_MDATP_Deployment_Add.ps1 b/MDATP/MacOS_MDATP_Deployment_Add.ps1 index 4f4d171..5b4b2ea 100755 --- a/MDATP/MacOS_MDATP_Deployment_Add.ps1 +++ b/MDATP/MacOS_MDATP_Deployment_Add.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -13,154 +13,204 @@ param ( [string]$OnboardingXMLFilePath ) -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - - - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-MDMApplication(){ +function Add-MDMApplication { <# .SYNOPSIS @@ -195,7 +245,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -219,7 +269,7 @@ $App_resource = "deviceAppManagement/mobileApps" #################################################### -Function Add-DeviceConfigurationPolicy(){ +function Add-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -246,7 +296,7 @@ Write-Verbose "Resource: $DCP_resource" try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red @@ -256,13 +306,13 @@ Write-Verbose "Resource: $DCP_resource" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -282,7 +332,7 @@ Write-Verbose "Resource: $DCP_resource" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -317,7 +367,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -329,50 +379,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -388,11 +398,11 @@ if (!($OnboardingXMLFilePath)){ do { $OnboardingXMLFilePath = Read-Host -Prompt "Enter path to your WindowsDefenderATPOnboarding.XML File" - + if (!(Test-Path $OnboardingXMLFilePath)){ write-host " - Couldn't find $OnboardingXMLFilePath, try again" -f yellow - + } } @@ -570,3 +580,4 @@ $CreateResult_sysext = Add-DeviceConfigurationPolicy -JSON $MDATP_sysext Write-Host " + Device MDATP System Extension Policy created as" $CreateResult_sysext.id Write-Host + diff --git a/MDATP/MacOS_MDATP_Deployment_Add_Assign.ps1 b/MDATP/MacOS_MDATP_Deployment_Add_Assign.ps1 index d9e90a9..6619a6d 100755 --- a/MDATP/MacOS_MDATP_Deployment_Add_Assign.ps1 +++ b/MDATP/MacOS_MDATP_Deployment_Add_Assign.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -15,154 +15,204 @@ param ( [string]$AADGroup ) -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - - - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-MDMApplication(){ +function Add-MDMApplication { <# .SYNOPSIS @@ -197,7 +247,7 @@ $App_resource = "deviceAppManagement/mobileApps" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($App_resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($App_resource)" Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Body $JSON -Headers $authToken } @@ -221,7 +271,7 @@ $App_resource = "deviceAppManagement/mobileApps" #################################################### -Function Add-ApplicationAssignment(){ +function Add-ApplicationAssignment { <# .SYNOPSIS @@ -246,7 +296,7 @@ param $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assign" - + try { if(!$ApplicationId){ @@ -263,7 +313,7 @@ $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assign" } - + if(!$InstallIntent){ write-host "No Install Intent specified, specify a valid Install Intent - available, notApplicable, required, uninstall, availableWithoutEnrollment" -f Red @@ -288,11 +338,11 @@ $JSON = @" "@ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } - + catch { $ex = $_.Exception @@ -312,7 +362,7 @@ $JSON = @" #################################################### -Function Add-DeviceConfigurationPolicy(){ +function Add-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -339,7 +389,7 @@ Write-Verbose "Resource: $DCP_resource" try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red @@ -349,13 +399,13 @@ Write-Verbose "Resource: $DCP_resource" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -375,7 +425,7 @@ Write-Verbose "Resource: $DCP_resource" #################################################### -Function Add-DeviceConfigurationPolicyAssignment(){ +function Add-DeviceConfigurationPolicyAssignment { <# .SYNOPSIS @@ -399,7 +449,7 @@ param $graphApiVersion = "Beta" $Resource = "deviceManagement/deviceConfigurations/$ConfigurationPolicyId/assign" - + try { if(!$ConfigurationPolicyId){ @@ -432,11 +482,11 @@ $JSON = @" "@ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } - + catch { $ex = $_.Exception @@ -456,7 +506,7 @@ $JSON = @" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -491,7 +541,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -501,7 +551,7 @@ $JSON #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -527,37 +577,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ $GID = $Group.id @@ -565,13 +615,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } } @@ -597,50 +647,10 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -656,11 +666,11 @@ if (!($OnboardingXMLFilePath)){ do { $OnboardingXMLFilePath = Read-Host -Prompt "Enter path to your WindowsDefenderATPOnboarding.XML File" - + if (!(Test-Path $OnboardingXMLFilePath)){ write-host " - Couldn't find $OnboardingXMLFilePath, try again" -f yellow - + } } @@ -783,20 +793,20 @@ $MDATP_sysext = @" # Setting application AAD Group to assign Policy do { - + if(!($AADGroup)){ - + $AADGroup = Read-Host -Prompt "Enter the Azure AD Group name where apps and policies will be assigned" - + } $TargetGroupId = (get-AADGroup -GroupName "$AADGroup").id - if($TargetGroupId -eq $null -or $TargetGroupId -eq ""){ + if($null -eq $TargetGroupId -or "" -eq $TargetGroupId){ Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor yellow $AADGroup = $null - + } } @@ -890,3 +900,4 @@ $Assign_sysext = Add-DeviceConfigurationPolicyAssignment -ConfigurationPolicyId Write-Host " + Assigned '$AADGroup' to $($CreateResult_sysext.displayName)/$($CreateResult_sysext.id)" Write-Host + diff --git a/ManagedDevices/ExpiringCertJuly2020_All.ps1 b/ManagedDevices/ExpiringCertJuly2020_All.ps1 index c6bbf96..e31bee7 100644 --- a/ManagedDevices/ExpiringCertJuly2020_All.ps1 +++ b/ManagedDevices/ExpiringCertJuly2020_All.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -14,148 +14,200 @@ param( ) -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### @@ -165,7 +217,7 @@ param ( [Parameter(Mandatory = $true)] $Uri, - + [Parameter(Mandatory = $true)] $AuthHeader ) @@ -189,7 +241,7 @@ param } } $NextLink = $Result.'@odata.nextLink' - } + } catch { @@ -203,7 +255,7 @@ param } - } while ($NextLink -ne $null) + } while ($null -ne $NextLink) return $Collection } @@ -212,57 +264,17 @@ param #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion #################################################### -$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$filter=((managementAgent%20eq%20%27mdm%27%20or%20managementAgent%20eq%20%27easmdm%27%20or%20managementAgent%20eq%20%27configurationmanagerclientmdm%27%20or%20managementAgent%20eq%20%27configurationmanagerclientmdmeas%27)%20and%20managementState%20eq%20%27managed%27)" +$uri = "beta/deviceManagement/managedDevices?`$filter=((managementAgent%20eq%20%27mdm%27%20or%20managementAgent%20eq%20%27easmdm%27%20or%20managementAgent%20eq%20%27configurationmanagerclientmdm%27%20or%20managementAgent%20eq%20%27configurationmanagerclientmdmeas%27)%20and%20managementState%20eq%20%27managed%27)" $devices = Get-MsGraphCollection -Uri $uri -AuthHeader $authToken @@ -270,8 +282,9 @@ Write-Host Write-Host "Found" $devices.Count "devices:" Write-Host "Writing results to" $OutputFile -ForegroundColor Cyan -($devices | Select-Object Id, DeviceName, DeviceType, IMEI, UserPrincipalName, SerialNumber, LastSyncDateTime, ManagementCertificateExpirationDate) | Export-Csv -Path $OutputFile -NoTypeInformation +($devices | Select-Object Id, DeviceName, DeviceType, IMEI, UserPrincipalName, SerialNumber, LastSyncDateTime, ManagementCertificateExpirationDate) | Export-Csv -Path $OutputFile -NoTypeInformation $devices | Select-Object Id, DeviceName, DeviceType, IMEI, UserPrincipalName, SerialNumber, LastSyncDateTime, ManagementCertificateExpirationDate Write-Host "Results written to" $OutputFile -ForegroundColor Yellow + diff --git a/ManagedDevices/ExpiringCertJuly2020_Android.ps1 b/ManagedDevices/ExpiringCertJuly2020_Android.ps1 index 2563781..c51f32c 100644 --- a/ManagedDevices/ExpiringCertJuly2020_Android.ps1 +++ b/ManagedDevices/ExpiringCertJuly2020_Android.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -14,148 +14,200 @@ param( ) -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "DeviceManagementServiceConfig.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### @@ -165,7 +217,7 @@ param ( [Parameter(Mandatory = $true)] $Uri, - + [Parameter(Mandatory = $true)] $AuthHeader ) @@ -189,7 +241,7 @@ param } } $NextLink = $Result.'@odata.nextLink' - } + } catch { @@ -203,7 +255,7 @@ param } - } while ($NextLink -ne $null) + } while ($null -ne $NextLink) return $Collection } @@ -212,57 +264,17 @@ param #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion #################################################### -$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$filter=(((deviceType%20eq%20%27android%27)%20or%20(deviceType%20eq%20%27androidForWork%27)))" +$uri = "beta/deviceManagement/managedDevices?`$filter=(((deviceType%20eq%20%27android%27)%20or%20(deviceType%20eq%20%27androidForWork%27)))" $devices = Get-MsGraphCollection -Uri $uri -AuthHeader $authToken @@ -270,8 +282,9 @@ Write-Host Write-Host "Found" $devices.Count "devices:" Write-Host "Writing results to" $OutputFile -ForegroundColor Cyan -($devices | Select-Object Id, DeviceName, DeviceType, IMEI, UserPrincipalName, SerialNumber, LastSyncDateTime, ManagementCertificateExpirationDate) | Export-Csv -Path $OutputFile -NoTypeInformation +($devices | Select-Object Id, DeviceName, DeviceType, IMEI, UserPrincipalName, SerialNumber, LastSyncDateTime, ManagementCertificateExpirationDate) | Export-Csv -Path $OutputFile -NoTypeInformation $devices | Select-Object Id, DeviceName, DeviceType, IMEI, UserPrincipalName, SerialNumber, LastSyncDateTime, ManagementCertificateExpirationDate Write-Host "Results written to" $OutputFile -ForegroundColor Yellow + diff --git a/ManagedDevices/Invoke_DeviceAction_Set.ps1 b/ManagedDevices/Invoke_DeviceAction_Set.ps1 index 9b71823..ece25e7 100644 --- a/ManagedDevices/Invoke_DeviceAction_Set.ps1 +++ b/ManagedDevices/Invoke_DeviceAction_Set.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-AADUser(){ +function Get-AADUser { <# .SYNOPSIS @@ -183,28 +235,28 @@ $User_resource = "users" try { - if($userPrincipalName -eq "" -or $userPrincipalName -eq $null){ + if("" -eq $userPrincipalName -or $null -eq $userPrincipalName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } else { - if($Property -eq "" -or $Property -eq $null){ + if("" -eq $Property -or $null -eq $Property){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName" Write-Verbose $uri - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -231,7 +283,7 @@ $User_resource = "users" #################################################### -Function Get-AADUserDevices(){ +function Get-AADUserDevices { <# .SYNOPSIS @@ -259,9 +311,9 @@ $Resource = "users/$UserID/managedDevices" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -284,7 +336,7 @@ $Resource = "users/$UserID/managedDevices" #################################################### -Function Invoke-DeviceAction(){ +function Invoke-DeviceAction { <# .SYNOPSIS @@ -342,7 +394,7 @@ $graphApiVersion = "Beta" elseif($RemoteLock){ $Resource = "deviceManagement/managedDevices/$DeviceID/remoteLock" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" write-verbose $uri Write-Verbose "Sending remoteLock command to $DeviceID" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post @@ -358,7 +410,7 @@ $graphApiVersion = "Beta" if($Confirm -eq "y" -or $Confirm -eq "Y"){ $Resource = "deviceManagement/managedDevices/$DeviceID/resetPasscode" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" write-verbose $uri Write-Verbose "Sending remotePasscode command to $DeviceID" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post @@ -382,7 +434,7 @@ $graphApiVersion = "Beta" if($Confirm -eq "y" -or $Confirm -eq "Y"){ $Resource = "deviceManagement/managedDevices/$DeviceID/wipe" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" write-verbose $uri Write-Verbose "Sending wipe command to $DeviceID" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post @@ -406,7 +458,7 @@ $graphApiVersion = "Beta" if($Confirm -eq "y" -or $Confirm -eq "Y"){ $Resource = "deviceManagement/managedDevices/$DeviceID/retire" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" write-verbose $uri Write-Verbose "Sending retire command to $DeviceID" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post @@ -432,7 +484,7 @@ $graphApiVersion = "Beta" if($Confirm -eq "y" -or $Confirm -eq "Y"){ $Resource = "deviceManagement/managedDevices('$DeviceID')" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" write-verbose $uri Write-Verbose "Sending delete command to $DeviceID" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Delete @@ -446,7 +498,7 @@ $graphApiVersion = "Beta" } } - + elseif($Sync){ write-host @@ -456,7 +508,7 @@ $graphApiVersion = "Beta" if($Confirm -eq "y" -or $Confirm -eq "Y"){ $Resource = "deviceManagement/managedDevices('$DeviceID')/syncDevice" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" write-verbose $uri Write-Verbose "Sending sync command to $DeviceID" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post @@ -492,10 +544,10 @@ $JSON = @" if($Confirm -eq "y" -or $Confirm -eq "Y"){ $Resource = "deviceManagement/managedDevices('$DeviceID')/setDeviceName" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" write-verbose $uri Write-Verbose "Sending rename command to $DeviceID" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $Json -ContentType "application/json" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -530,50 +582,10 @@ $JSON = @" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -618,8 +630,8 @@ Write-Host $menu = @{} - for ($i=1;$i -le $Managed_Devices.count; $i++) - { Write-Host "$i. $($Managed_Devices[$i-1])" + for ($i=1;$i -le $Managed_Devices.count; $i++) + { Write-Host "$i. $($Managed_Devices[$i-1])" $menu.Add($i,($Managed_Devices[$i-1]))} Write-Host @@ -669,3 +681,4 @@ write-host + diff --git a/ManagedDevices/ManagedDeviceOverview_Get.ps1 b/ManagedDevices/ManagedDeviceOverview_Get.ps1 index 5520f8f..78ef809 100644 --- a/ManagedDevices/ManagedDeviceOverview_Get.ps1 +++ b/ManagedDevices/ManagedDeviceOverview_Get.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementManagedDevices.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-ManagedDeviceOverview(){ +function Get-ManagedDeviceOverview { <# .SYNOPSIS @@ -177,8 +228,8 @@ $Resource = "deviceManagement/managedDeviceOverview" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET @@ -205,50 +256,10 @@ $Resource = "deviceManagement/managedDeviceOverview" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -265,3 +276,4 @@ Write-Host "Device Operating System Summary:" -ForegroundColor Cyan Write-Host $MDO.deviceOperatingSystemSummary + diff --git a/ManagedDevices/ManagedDevices_Add_ToAADGroup.ps1 b/ManagedDevices/ManagedDevices_Add_ToAADGroup.ps1 index b7b1c76..3a11a9b 100644 --- a/ManagedDevices/ManagedDevices_Add_ToAADGroup.ps1 +++ b/ManagedDevices/ManagedDevices_Add_ToAADGroup.ps1 @@ -7,144 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementManagedDevices.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + $results = @() + $nextLink = $Uri - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-AADUser(){ +function Get-AADUser { <# .SYNOPSIS @@ -172,36 +232,36 @@ param # Defining Variables $graphApiVersion = "v1.0" $User_resource = "users" - + try { - - if($userPrincipalName -eq "" -or $userPrincipalName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + if("" -eq $userPrincipalName -or $null -eq $userPrincipalName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - - if($Property -eq "" -or $Property -eq $null){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName" + if("" -eq $Property -or $null -eq $Property){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName" Write-Verbose $uri - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } catch { @@ -223,7 +283,7 @@ $User_resource = "users" #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -249,37 +309,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ $GID = $Group.id @@ -287,13 +347,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } } @@ -317,7 +377,7 @@ $Group_resource = "groups" #################################################### -Function Get-AADDevice(){ +function Get-AADDevice { <# .SYNOPSIS @@ -341,12 +401,12 @@ param # Defining Variables $graphApiVersion = "v1.0" $Resource = "devices" - + try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)?`$filter=deviceId eq '$DeviceID'" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)?`$filter=deviceId eq '$DeviceID'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } @@ -369,7 +429,7 @@ $Resource = "devices" #################################################### -Function Add-AADGroupMember(){ +function Add-AADGroupMember { <# .SYNOPSIS @@ -394,20 +454,20 @@ param # Defining Variables $graphApiVersion = "v1.0" $Resource = "groups" - + try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource/$GroupId/members/`$ref" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource/$GroupId/members/`$ref" $JSON = @" { - "@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/$AADMemberId" + "@odata.id": "v1.0/directoryObjects/$AADMemberId" } "@ - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $Json -ContentType "application/json" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -430,7 +490,7 @@ $JSON = @" #################################################### -Function Get-ManagedDevices(){ +function Get-ManagedDevices { <# .SYNOPSIS @@ -465,7 +525,7 @@ try { if($IncludeEAS.IsPresent){ $Count_Params++ } if($ExcludeMDM.IsPresent){ $Count_Params++ } - + if($Count_Params -gt 1){ write-warning "Multiple parameters set, specify a single parameter -IncludeEAS, -ExcludeMDM or no parameter against the function" @@ -473,29 +533,29 @@ try { break } - + elseif($IncludeEAS){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" } elseif($ExcludeMDM){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" } - + else { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'" + + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'" Write-Warning "EAS Devices are excluded by default, please use -IncludeEAS if you want to include those devices" Write-Host } - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } catch { @@ -519,50 +579,10 @@ try { #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -573,11 +593,11 @@ $global:authToken = Get-AuthToken -User $User # Setting application AAD Group to assign application -$AADGroup = Read-Host -Prompt "Enter the Azure AD device group name where devices will be assigned as members" +$AADGroup = Read-Host -Prompt "Enter the Azure AD device group name where devices will be assigned as members" $GroupId = (get-AADGroup -GroupName "$AADGroup").id - if($GroupId -eq $null -or $GroupId -eq ""){ + if($null -eq $GroupId -or "" -eq $GroupId){ Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red Write-Host @@ -600,9 +620,9 @@ $GroupId = (get-AADGroup -GroupName "$AADGroup").id # Variable used for filter on users displayname # Note: The filter is case sensitive -$FilterName = Read-Host -Prompt "Specify the Azure AD display name search string" +$FilterName = Read-Host -Prompt "Specify the Azure AD display name search string" - if($FilterName -eq "" -or $FilterName -eq $null){ + if("" -eq $FilterName -or $null -eq $FilterName){ Write-Host Write-Host "A string is required to identify the set of users." -ForegroundColor Red @@ -670,7 +690,7 @@ if($Devices){ # Getting Device information from Azure AD Devices - $AAD_Device = Get-AADDevice -DeviceID $AAD_DeviceID + $AAD_Device = Get-AADDevice -DeviceID $AAD_DeviceID $AAD_Id = $AAD_Device.id @@ -697,7 +717,7 @@ if($Devices){ } } - + Write-Host "----------------------------------------------------" Write-Host Write-Host "$count devices added to AAD Group '$AADGroup' with filter '$filterName'..." -ForegroundColor Green @@ -712,3 +732,4 @@ write-host "No Intune Managed Devices found..." -f green Write-Host } + diff --git a/ManagedDevices/ManagedDevices_Apps_Get.ps1 b/ManagedDevices/ManagedDevices_Apps_Get.ps1 index aa649c9..95c149f 100644 --- a/ManagedDevices/ManagedDevices_Apps_Get.ps1 +++ b/ManagedDevices/ManagedDevices_Apps_Get.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementManagedDevices.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-AADUser(){ +function Get-AADUser { <# .SYNOPSIS @@ -183,28 +234,28 @@ $User_resource = "users" try { - if($userPrincipalName -eq "" -or $userPrincipalName -eq $null){ + if("" -eq $userPrincipalName -or $null -eq $userPrincipalName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } else { - if($Property -eq "" -or $Property -eq $null){ + if("" -eq $Property -or $null -eq $Property){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName" Write-Verbose $uri - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -231,7 +282,7 @@ $User_resource = "users" #################################################### -Function Get-ManagedDevices(){ +function Get-ManagedDevices { <# .SYNOPSIS @@ -277,26 +328,26 @@ try { elseif($IncludeEAS){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" } elseif($ExcludeMDM){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'" Write-Warning "EAS Devices are excluded by default, please use -IncludeEAS if you want to include those devices" Write-Host } - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -321,50 +372,10 @@ try { #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -396,8 +407,8 @@ if($Devices){ } - $uri = "https://graph.microsoft.com/beta/deviceManagement/manageddevices('$DeviceID')?`$expand=detectedApps" - $DetectedApps = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).detectedApps + $uri = "beta/deviceManagement/manageddevices('$DeviceID')?`$expand=detectedApps" + $DetectedApps = (Invoke-IntuneRestMethod -Uri $uri -Method GET).detectedApps $DetectedApps | select displayName,version | ft @@ -411,3 +422,4 @@ write-host "No Intune Managed Devices found..." -f green Write-Host } + diff --git a/ManagedDevices/ManagedDevices_DeviceOwnership_Set.ps1 b/ManagedDevices/ManagedDevices_DeviceOwnership_Set.ps1 index f12b77c..02f50a2 100644 --- a/ManagedDevices/ManagedDevices_DeviceOwnership_Set.ps1 +++ b/ManagedDevices/ManagedDevices_DeviceOwnership_Set.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-ManagedDevices(){ +function Get-ManagedDevices { <# .SYNOPSIS @@ -187,7 +239,7 @@ try { if($IncludeEAS.IsPresent){ $Count_Params++ } if($ExcludeMDM.IsPresent){ $Count_Params++ } - + if($Count_Params -gt 1){ write-warning "Multiple parameters set, specify a single parameter -IncludeEAS, -ExcludeMDM or no parameter against the function" @@ -195,29 +247,29 @@ try { break } - + elseif($IncludeEAS){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" } elseif($ExcludeMDM){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" } - + else { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'" + + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'" Write-Warning "EAS Devices are excluded by default, please use -IncludeEAS if you want to include those devices" Write-Host } - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } catch { @@ -239,7 +291,7 @@ try { #################################################### -Function Set-ManagedDevice(){ +function Set-ManagedDevice { <# .SYNOPSIS @@ -267,14 +319,14 @@ $Resource = "deviceManagement/managedDevices" try { - if($id -eq "" -or $id -eq $null){ + if("" -eq $id -or $null -eq $id){ write-host "No Device id specified, please provide a device id..." -f Red break } - - if($ownerType -eq "" -or $ownerType -eq $null){ + + if("" -eq $ownerType -or $null -eq $ownerType){ write-host "No ownerType parameter specified, please provide an ownerType. Supported value personal or company..." -f Red Write-Host @@ -297,9 +349,9 @@ $JSON = @" $Confirm = read-host if($Confirm -eq "y" -or $Confirm -eq "Y"){ - + # Send Patch command to Graph to change the ownertype - $uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices('$ID')" + $uri = "beta/deviceManagement/managedDevices('$ID')" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Patch -Body $Json -ContentType "application/json" } @@ -310,7 +362,7 @@ $JSON = @" Write-Host } - + } elseif($ownerType -eq "personal"){ @@ -328,9 +380,9 @@ $JSON = @" $Confirm = read-host if($Confirm -eq "y" -or $Confirm -eq "Y"){ - + # Send Patch command to Graph to change the ownertype - $uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices('$ID')" + $uri = "beta/deviceManagement/managedDevices('$ID')" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Patch -Body $Json -ContentType "application/json" } @@ -367,50 +419,10 @@ $JSON = @" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -457,3 +469,4 @@ Write-Host "No Managed Device found..." -ForegroundColor Red Write-Host } + diff --git a/ManagedDevices/ManagedDevices_Get.ps1 b/ManagedDevices/ManagedDevices_Get.ps1 index 541f1e3..91b3b35 100644 --- a/ManagedDevices/ManagedDevices_Get.ps1 +++ b/ManagedDevices/ManagedDevices_Get.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementManagedDevices.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-ManagedDevices(){ +function Get-ManagedDevices { <# .SYNOPSIS @@ -187,7 +238,7 @@ try { if($IncludeEAS.IsPresent){ $Count_Params++ } if($ExcludeMDM.IsPresent){ $Count_Params++ } - + if($Count_Params -gt 1){ write-warning "Multiple parameters set, specify a single parameter -IncludeEAS, -ExcludeMDM or no parameter against the function" @@ -195,29 +246,29 @@ try { break } - + elseif($IncludeEAS){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" } elseif($ExcludeMDM){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" } - + else { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'" + + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'" Write-Warning "EAS Devices are excluded by default, please use -IncludeEAS if you want to include those devices" Write-Host } - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } catch { @@ -239,7 +290,7 @@ try { #################################################### -Function Get-AADUser(){ +function Get-AADUser { <# .SYNOPSIS @@ -270,28 +321,28 @@ $User_resource = "users" try { - if($userPrincipalName -eq "" -or $userPrincipalName -eq $null){ + if("" -eq $userPrincipalName -or $null -eq $userPrincipalName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } else { - if($Property -eq "" -or $Property -eq $null){ + if("" -eq $Property -or $null -eq $Property){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName" Write-Verbose $uri - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -320,50 +371,10 @@ $User_resource = "users" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -406,3 +417,4 @@ Write-Host "No Managed Devices found..." -ForegroundColor Red Write-Host } + diff --git a/ManagedDevices/ManagedDevices_Hardware_Get.ps1 b/ManagedDevices/ManagedDevices_Hardware_Get.ps1 index fc473ac..01fb9bd 100644 --- a/ManagedDevices/ManagedDevices_Hardware_Get.ps1 +++ b/ManagedDevices/ManagedDevices_Hardware_Get.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementManagedDevices.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-ManagedDevices(){ +function Get-ManagedDevices { <# .SYNOPSIS @@ -198,25 +249,25 @@ try { elseif($IncludeEAS){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" } elseif($ExcludeMDM){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm' and managementAgent eq 'googleCloudDevicePolicyController'" + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm' and managementAgent eq 'googleCloudDevicePolicyController'" Write-Warning "EAS Devices are excluded by default, please use -IncludeEAS if you want to include those devices" Write-Host } - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -241,50 +292,10 @@ try { #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -336,9 +347,9 @@ if($Devices){ Write-Host "Device found:" $Device.deviceName -ForegroundColor Yellow Write-Host - $uri = "https://graph.microsoft.com/beta/deviceManagement/manageddevices('$DeviceID')?`$select=hardwareinformation,iccid,udid,ethernetMacAddress" + $uri = "beta/deviceManagement/manageddevices('$DeviceID')?`$select=hardwareinformation,iccid,udid,ethernetMacAddress" - $DeviceInfo = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) + $DeviceInfo = (Invoke-IntuneRestMethod -Uri $uri -Method GET) $DeviceNoHardware = $Device | select * -ExcludeProperty hardwareInformation,deviceActionResults,userId,imei,manufacturer,model,isSupervised,isEncrypted,serialNumber,meid,subscriberCarrier,iccid,udid,ethernetMacAddress $HardwareExcludes = $DeviceInfo.hardwareInformation | select * -ExcludeProperty sharedDeviceCachedUsers,phoneNumber @@ -386,3 +397,4 @@ write-host "No Intune Managed Devices found..." -f green Write-Host } + diff --git a/ManagedDevices/ManagedDevices_iOS_PasscodeReset_Export.ps1 b/ManagedDevices/ManagedDevices_iOS_PasscodeReset_Export.ps1 index 7df7819..c57023d 100644 --- a/ManagedDevices/ManagedDevices_iOS_PasscodeReset_Export.ps1 +++ b/ManagedDevices/ManagedDevices_iOS_PasscodeReset_Export.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementManagedDevices.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Export-iOSDevices(){ +function Export-iOSDevices { <# .SYNOPSIS @@ -182,7 +233,7 @@ $Resource = "deviceManagement/reports/exportJobs" reportName = 'Devices' select = @('DeviceId',"DeviceName","OSVersion", "HasUnlockToken") filter = "((DeviceType eq '14') or (DeviceType eq '9') or (DeviceType eq '8') or (DeviceType eq '10'))" - + } $psObj = New-Object -TypeName psobject -Property $properties @@ -191,15 +242,15 @@ $Resource = "deviceManagement/reports/exportJobs" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - $result = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json") + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + $result = (Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON) $id = $result.id @@ -235,7 +286,7 @@ $Resource = "deviceManagement/reports/exportJobs" Write-Host "In progress, waiting..." -ForegroundColor Yellow Start-Sleep -Seconds 5 Write-Host - + } } @@ -266,50 +317,10 @@ $Resource = "deviceManagement/reports/exportJobs" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -317,3 +328,4 @@ $global:authToken = Get-AuthToken -User $User #################################################### Export-iOSDevices + diff --git a/ManagedDevices/Win10_PrimaryUser_Delete.ps1 b/ManagedDevices/Win10_PrimaryUser_Delete.ps1 index c4adb34..528904a 100644 --- a/ManagedDevices/Win10_PrimaryUser_Delete.ps1 +++ b/ManagedDevices/Win10_PrimaryUser_Delete.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,157 +6,208 @@ See LICENSE in the project root for license information. #> -#################################################### - -param -( -[parameter(Mandatory=$false)] -$DeviceName - -) - -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "User.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +param +( +[parameter(Mandatory=$false)] +$DeviceName + +) + +#################################################### -} #################################################### @@ -181,7 +232,7 @@ param [ValidateNotNullOrEmpty()] [string]$deviceName ) - + $graphApiVersion = "beta" try { @@ -189,18 +240,18 @@ param if($deviceName){ $Resource = "deviceManagement/managedDevices?`$filter=deviceName eq '$deviceName'" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } else { $Resource = "deviceManagement/managedDevices?`$filter=(((deviceType%20eq%20%27desktop%27)%20or%20(deviceType%20eq%20%27windowsRT%27)%20or%20(deviceType%20eq%20%27winEmbedded%27)%20or%20(deviceType%20eq%20%27surfaceHub%27)))" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } @@ -242,14 +293,14 @@ param ) $graphApiVersion = "beta" $Resource = "deviceManagement/managedDevices" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + "/" + $deviceId + "/users" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + "/" + $deviceId + "/users" try { - - $primaryUser = Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + + $primaryUser = Invoke-IntuneRestMethod -Uri $uri -Method GET return $primaryUser.value."id" - + } catch { $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() @@ -286,13 +337,13 @@ param [ValidateNotNullOrEmpty()] $IntuneDeviceId ) - + $graphApiVersion = "beta" $Resource = "deviceManagement/managedDevices('$IntuneDeviceId')/users/`$ref" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Delete @@ -309,7 +360,7 @@ $IntuneDeviceId Write-Host "Response content:`n$responseBody" -f Red Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" throw "Delete-IntuneDevicePrimaryUser error" - + } } @@ -318,44 +369,10 @@ $IntuneDeviceId #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - } - - $global:authToken = Get-AuthToken -User $User - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq "") { - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - } - - # Getting the authorization token - $global:authToken = Get-AuthToken -User $User +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -382,7 +399,7 @@ if($Device){ $DeleteIntuneDevicePrimaryUser = Delete-IntuneDevicePrimaryUser -IntuneDeviceId $Device.id - if($DeleteIntuneDevicePrimaryUser -eq ""){ + if("" -eq $DeleteIntuneDevicePrimaryUser){ Write-Host "User deleted as Primary User from the device '$DeviceName'..." -ForegroundColor Green @@ -397,3 +414,4 @@ else { } Write-Host + diff --git a/ManagedDevices/Win10_PrimaryUser_Get.ps1 b/ManagedDevices/Win10_PrimaryUser_Get.ps1 index dbbc9ab..b28a723 100644 --- a/ManagedDevices/Win10_PrimaryUser_Get.ps1 +++ b/ManagedDevices/Win10_PrimaryUser_Get.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,157 +6,208 @@ See LICENSE in the project root for license information. #> -#################################################### - -param -( -[parameter(Mandatory=$false)] -$DeviceName - -) - -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "User.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +param +( +[parameter(Mandatory=$false)] +$DeviceName + +) + +#################################################### -} #################################################### @@ -181,7 +232,7 @@ param [ValidateNotNullOrEmpty()] [string]$deviceName ) - + $graphApiVersion = "beta" try { @@ -189,18 +240,18 @@ param if($deviceName){ $Resource = "deviceManagement/managedDevices?`$filter=deviceName eq '$deviceName'" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } else { $Resource = "deviceManagement/managedDevices?`$filter=(((deviceType%20eq%20%27desktop%27)%20or%20(deviceType%20eq%20%27windowsRT%27)%20or%20(deviceType%20eq%20%27winEmbedded%27)%20or%20(deviceType%20eq%20%27surfaceHub%27)))" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } @@ -242,10 +293,10 @@ param ) $graphApiVersion = "beta" $Resource = "devices" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)?`$filter=deviceId eq '$deviceId'" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)?`$filter=deviceId eq '$deviceId'" try { - $device = Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + $device = Invoke-IntuneRestMethod -Uri $uri -Method GET return $device.value."id" @@ -284,17 +335,17 @@ param [Parameter(Mandatory=$true)] [string] $deviceId ) - + $graphApiVersion = "beta" $Resource = "deviceManagement/managedDevices" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + "/" + $deviceId + "/users" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + "/" + $deviceId + "/users" try { - - $primaryUser = Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + + $primaryUser = Invoke-IntuneRestMethod -Uri $uri -Method GET return $primaryUser.value."id" - + } catch { $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() @@ -332,21 +383,21 @@ param ) $graphApiVersion = "beta" $Resource = "devices" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)/$deviceId/registeredOwners" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)/$deviceId/registeredOwners" try { - - $registeredOwners = Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + + $registeredOwners = Invoke-IntuneRestMethod -Uri $uri -Method GET Write-Host "AAD Registered Owner:" -ForegroundColor Yellow if(@($registeredOwners.value).count -ge 1){ for($i=0; $i -lt $registeredOwners.value.Count; $i++){ - + Write-Host "Id:" $registeredOwners.value[$i]."id" Write-Host "Name:" $registeredOwners.value[$i]."displayName" - + } } @@ -354,7 +405,7 @@ param else { Write-Host "No registered Owner found in Azure Active Directory..." -ForegroundColor Red - + } } catch { @@ -394,10 +445,10 @@ param ) $graphApiVersion = "beta" $Resource = "devices" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + "/$deviceId/registeredUsers" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + "/$deviceId/registeredUsers" try { - $registeredUsers = Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + $registeredUsers = Invoke-IntuneRestMethod -Uri $uri -Method GET Write-Host "RegisteredUsers:" -ForegroundColor Yellow @@ -415,7 +466,7 @@ param else { Write-Host "No registered User found in Azure Active Directory..." -ForegroundColor Red - + } } catch { @@ -435,44 +486,10 @@ param #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - } - - $global:authToken = Get-AuthToken -User $User - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq "") { - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - } - - # Getting the authorization token - $global:authToken = Get-AuthToken -User $User +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -500,12 +517,12 @@ if($Devices){ Write-Host Write-Host "Device name:" $device."deviceName" -ForegroundColor Cyan Write-Host "Intune device id:" $device."id" - + $IntuneDevicePrimaryUser = Get-IntuneDevicePrimaryUser -deviceId $device.id - if($IntuneDevicePrimaryUser -eq $null){ + if($null -eq $IntuneDevicePrimaryUser){ - Write-Host "No Intune Primary User Id set for Intune Managed Device" $Device."deviceName" -f Red + Write-Host "No Intune Primary User Id set for Intune Managed Device" $Device."deviceName" -f Red } @@ -523,7 +540,7 @@ if($Devices){ Write-Host Write-Host "-------------------------------------------------------------------" - + } } @@ -535,3 +552,4 @@ else { } Write-Host + diff --git a/ManagedDevices/Win10_PrimaryUser_Set.ps1 b/ManagedDevices/Win10_PrimaryUser_Set.ps1 index b21cb53..511c268 100644 --- a/ManagedDevices/Win10_PrimaryUser_Set.ps1 +++ b/ManagedDevices/Win10_PrimaryUser_Set.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,159 +6,211 @@ See LICENSE in the project root for license information. #> -#################################################### - -param -( -[parameter(Mandatory=$false)] -$DeviceName, -[parameter(Mandatory=$false)] -$UserPrincipalName - -) - -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +param +( +[parameter(Mandatory=$false)] +$DeviceName, +[parameter(Mandatory=$false)] +$UserPrincipalName + +) + +#################################################### -} #################################################### @@ -183,7 +235,7 @@ param [ValidateNotNullOrEmpty()] [string]$deviceName ) - + $graphApiVersion = "beta" try { @@ -191,18 +243,18 @@ param if($deviceName){ $Resource = "deviceManagement/managedDevices?`$filter=deviceName eq '$deviceName'" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } else { $Resource = "deviceManagement/managedDevices?`$filter=(((deviceType%20eq%20%27desktop%27)%20or%20(deviceType%20eq%20%27windowsRT%27)%20or%20(deviceType%20eq%20%27winEmbedded%27)%20or%20(deviceType%20eq%20%27surfaceHub%27)))" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } @@ -222,7 +274,7 @@ param #################################################### -Function Get-AADUser(){ +function Get-AADUser { <# .SYNOPSIS @@ -250,36 +302,36 @@ param # Defining Variables $graphApiVersion = "v1.0" $User_resource = "users" - + try { - - if($userPrincipalName -eq "" -or $userPrincipalName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + if("" -eq $userPrincipalName -or $null -eq $userPrincipalName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - - if($Property -eq "" -or $Property -eq $null){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName" + if("" -eq $Property -or $null -eq $Property){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName" Write-Verbose $uri - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } catch { @@ -323,14 +375,14 @@ param ) $graphApiVersion = "beta" $Resource = "deviceManagement/managedDevices" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + "/" + $deviceId + "/users" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + "/" + $deviceId + "/users" try { - - $primaryUser = Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + + $primaryUser = Invoke-IntuneRestMethod -Uri $uri -Method GET return $primaryUser.value."id" - + } catch { $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() @@ -374,15 +426,15 @@ $userId $Resource = "deviceManagement/managedDevices('$IntuneDeviceId')/users/`$ref" try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" $userUri = "https://graph.microsoft.com/$graphApiVersion/users/" + $userId $id = "@odata.id" $JSON = @{ $id="$userUri" } | ConvertTo-Json -Compress - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } catch { $ex = $_.Exception @@ -402,44 +454,10 @@ $userId #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - } - - $global:authToken = Get-AuthToken -User $User - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq "") { - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - } - - # Getting the authorization token - $global:authToken = Get-AuthToken -User $User +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -469,14 +487,14 @@ if($Device){ Write-Host "Device name:" $device."deviceName" -ForegroundColor Cyan $IntuneDevicePrimaryUser = Get-IntuneDevicePrimaryUser -deviceId $Device.id - if($IntuneDevicePrimaryUser -eq $null){ + if($null -eq $IntuneDevicePrimaryUser){ - Write-Host "No Intune Primary User Id set for Intune Managed Device" $Device."deviceName" -f Red + Write-Host "No Intune Primary User Id set for Intune Managed Device" $Device."deviceName" -f Red } else { - + Write-Host "Intune Device Primary User:" $IntuneDevicePrimaryUser } @@ -484,12 +502,12 @@ if($Device){ $User = Get-AADUser -userPrincipalName $UserPrincipalName $AADUserName = $User.displayName - + if($IntuneDevicePrimaryUser -notmatch $User.id){ $SetIntuneDevicePrimaryUser = Set-IntuneDevicePrimaryUser -IntuneDeviceId $Device.id -userId $User.id - if($SetIntuneDevicePrimaryUser -eq ""){ + if("" -eq $SetIntuneDevicePrimaryUser){ Write-Host "User"$User.displayName"set as Primary User for device '$DeviceName'..." -ForegroundColor Green @@ -512,3 +530,4 @@ else { } Write-Host + diff --git a/Paging/ManagedDevices_Get_Paging.ps1 b/Paging/ManagedDevices_Get_Paging.ps1 index f36285e..a4e92e6 100644 --- a/Paging/ManagedDevices_Get_Paging.ps1 +++ b/Paging/ManagedDevices_Get_Paging.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementManagedDevices.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-ManagedDevices(){ +function Get-ManagedDevices { <# .SYNOPSIS @@ -173,15 +224,15 @@ $Resource = "deviceManagement/managedDevices" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" - $DevicesResponse = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) + $DevicesResponse = (Invoke-IntuneRestMethod -Uri $uri -Method GET) $Devices = $DevicesResponse.value $DevicesNextLink = $DevicesResponse."@odata.nextLink" - while ($DevicesNextLink -ne $null){ + while ($null -ne $DevicesNextLink){ $DevicesResponse = (Invoke-RestMethod -Uri $DevicesNextLink -Headers $authToken -Method Get) $DevicesNextLink = $DevicesResponse."@odata.nextLink" @@ -214,50 +265,10 @@ $Resource = "deviceManagement/managedDevices" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -285,3 +296,4 @@ Write-Host "No Managed Devices found..." -ForegroundColor Red Write-Host } + diff --git a/RBAC/RBAC_Add.ps1 b/RBAC/RBAC_Add.ps1 index deb748d..994aca4 100644 --- a/RBAC/RBAC_Add.ps1 +++ b/RBAC/RBAC_Add.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementRBAC.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -197,7 +249,7 @@ $JSON #################################################### -Function Add-RBACRole(){ +function Add-RBACRole { <# .SYNOPSIS @@ -231,8 +283,8 @@ $Resource = "deviceManagement/roleDefinitions" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $Json -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -257,50 +309,10 @@ $Resource = "deviceManagement/roleDefinitions" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -334,3 +346,4 @@ $JSON = @" "@ Add-RBACRole -JSON $JSON + diff --git a/RBAC/RBAC_Add_Assign.ps1 b/RBAC/RBAC_Add_Assign.ps1 index d4e702e..c58130a 100644 --- a/RBAC/RBAC_Add_Assign.ps1 +++ b/RBAC/RBAC_Add_Assign.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementRBAC.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -187,7 +239,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -197,7 +249,7 @@ $JSON #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -223,37 +275,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ $GID = $Group.id @@ -261,13 +313,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } } @@ -291,7 +343,7 @@ $Group_resource = "groups" #################################################### -Function Add-RBACRole(){ +function Add-RBACRole { <# .SYNOPSIS @@ -313,7 +365,7 @@ param $graphApiVersion = "Beta" $Resource = "deviceManagement/roleDefinitions" - + try { if(!$JSON){ @@ -324,12 +376,12 @@ $Resource = "deviceManagement/roleDefinitions" } Test-JSON -JSON $JSON - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $Json -ContentType "application/json" - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON + } - + catch { $ex = $_.Exception @@ -349,7 +401,7 @@ $Resource = "deviceManagement/roleDefinitions" #################################################### -Function Assign-RBACRole(){ +function Assign-RBACRole { <# .SYNOPSIS @@ -375,7 +427,7 @@ param $graphApiVersion = "Beta" $Resource = "deviceManagement/roleAssignments" - + try { if(!$Id){ @@ -415,16 +467,16 @@ $JSON = @" "displayName":"$DisplayName", "members":["$MemberGroupId"], "scopeMembers":["$TargetGroupId"], - "roleDefinition@odata.bind":"https://graph.microsoft.com/beta/deviceManagement/roleDefinitions('$ID')" + "roleDefinition@odata.bind":"beta/deviceManagement/roleDefinitions('$ID')" } "@ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" - + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON + } - + catch { $ex = $_.Exception @@ -446,50 +498,10 @@ $JSON = @" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -502,7 +514,7 @@ $MemberAADGroup = Read-Host -Prompt "Enter the Azure AD Group name for Intune Ro $MemberGroupId = (get-AADGroup -GroupName "$MemberAADGroup").id - if($MemberGroupId -eq $null -or $MemberGroupId -eq ""){ + if($null -eq $MemberGroupId -or "" -eq $MemberGroupId){ Write-Host "AAD Group - '$MemberAADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red Write-Host @@ -520,7 +532,7 @@ $AADGroup = Read-Host -Prompt "Enter the Azure AD Group name for Intune Role Sco $TargetGroupId = (get-AADGroup -GroupName "$AADGroup").id - if($TargetGroupId -eq $null -or $TargetGroupId -eq ""){ + if($null -eq $TargetGroupId -or "" -eq $TargetGroupId){ Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red Write-Host @@ -576,3 +588,4 @@ $AssignmentIntuneRole = Assign-RBACRole -Id $IntuneRoleID -DisplayName "Assignme write-host "Intune Role Assigment created with id" $AssignmentIntuneRole.id Write-Host + diff --git a/RBAC/RBAC_DuplicateRole.ps1 b/RBAC/RBAC_DuplicateRole.ps1 index 5a35072..8194c33 100644 --- a/RBAC/RBAC_DuplicateRole.ps1 +++ b/RBAC/RBAC_DuplicateRole.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementRBAC.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-RBACRole(){ +function Get-RBACRole { <# .SYNOPSIS @@ -168,14 +219,14 @@ NAME: Get-RBACRole $graphApiVersion = "Beta" $Resource = "deviceManagement/roleDefinitions" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { $ex = $_.Exception @@ -195,7 +246,7 @@ $Resource = "deviceManagement/roleDefinitions" #################################################### -Function Add-RBACRole(){ +function Add-RBACRole { <# .SYNOPSIS @@ -217,7 +268,7 @@ param $graphApiVersion = "Beta" $Resource = "deviceManagement/roleDefinitions" - + try { if(!$JSON){ @@ -228,12 +279,12 @@ $Resource = "deviceManagement/roleDefinitions" } Test-JSON -JSON $JSON - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $Json -ContentType "application/json" - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON + } - + catch { $ex = $_.Exception @@ -253,7 +304,7 @@ $Resource = "deviceManagement/roleDefinitions" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -288,7 +339,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -300,50 +351,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -357,8 +368,8 @@ $RBAC_Roles = (Get-RBACRole | Where-Object { $_.isBuiltInRoleDefinition -eq $tru $menu = @{} -for ($i=1;$i -le $RBAC_Roles.count; $i++) -{ Write-Host "$i. $($RBAC_Roles[$i-1])" +for ($i=1;$i -le $RBAC_Roles.count; $i++) +{ Write-Host "$i. $($RBAC_Roles[$i-1])" $menu.Add($i,($RBAC_Roles[$i-1]))} Write-Host @@ -376,7 +387,7 @@ $selection = $menu.Item($ans) $RBAC_DN = Read-Host "Please specify a displayName for the duplicated Intune Role" - if($RBAC_DN -eq ""){ + if("" -eq $RBAC_DN){ Write-Host "Intune Role DisplayName can't be null, please specify a valid DisplayName..." -ForegroundColor Red Write-Host @@ -398,19 +409,19 @@ $JSON = @" "displayName": "$RBAC_DN", "description": "$RBAC_DN", "permissions": [ - { + { "actions": $RBAC_Actions } - + ], - "isBuiltInRoleDefinition": false + "isBuiltInRoleDefinition": false } "@ Write-Host - + $JSON - + Write-Host Write-Host "Duplicating Intune Role and Adding to the Intune Service..." -ForegroundColor Yellow @@ -429,3 +440,4 @@ $JSON = @" } Write-Host + diff --git a/RBAC/RBAC_Export.ps1 b/RBAC/RBAC_Export.ps1 index dc6faae..ff57e33 100644 --- a/RBAC/RBAC_Export.ps1 +++ b/RBAC/RBAC_Export.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementRBAC.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-RBACRole(){ +function Get-RBACRole { <# .SYNOPSIS @@ -167,14 +218,14 @@ NAME: Get-RBACRole $graphApiVersion = "v1.0" $Resource = "deviceManagement/roleDefinitions" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { $ex = $_.Exception @@ -196,50 +247,10 @@ $Resource = "deviceManagement/roleDefinitions" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -304,7 +315,7 @@ $JSON = @" ] } ], - "isBuiltIn": false + "isBuiltIn": false } "@ @@ -314,3 +325,4 @@ $JSON = @" } + diff --git a/RBAC/RBAC_Get.ps1 b/RBAC/RBAC_Get.ps1 index 45fca76..6bbc848 100644 --- a/RBAC/RBAC_Get.ps1 +++ b/RBAC/RBAC_Get.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementRBAC.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-RBACRole(){ +function Get-RBACRole { <# .SYNOPSIS @@ -180,15 +231,15 @@ $Resource = "deviceManagement/roleDefinitions" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") -and $_.isBuiltInRoleDefinition -eq $false } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") -and $_.isBuiltInRoleDefinition -eq $false } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -215,50 +266,10 @@ $Resource = "deviceManagement/roleDefinitions" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -274,3 +285,4 @@ $RBAC_Role.RolePermissions.resourceActions.allowedResourceActions Write-Host } + diff --git a/RBAC/RBAC_Import_FromJSON.ps1 b/RBAC/RBAC_Import_FromJSON.ps1 index 68b3cb3..ddac06d 100644 --- a/RBAC/RBAC_Import_FromJSON.ps1 +++ b/RBAC/RBAC_Import_FromJSON.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementRBAC.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -186,7 +237,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -196,7 +247,7 @@ $JSON #################################################### -Function Add-RBACRole(){ +function Add-RBACRole { <# .SYNOPSIS @@ -218,7 +269,7 @@ param $graphApiVersion = "v1.0" $Resource = "deviceManagement/roleDefinitions" - + try { if(!$JSON){ @@ -229,12 +280,12 @@ $Resource = "deviceManagement/roleDefinitions" } Test-JSON -JSON $JSON - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $Json -ContentType "application/json" - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON + } - + catch { $ex = $_.Exception @@ -256,50 +307,10 @@ $Resource = "deviceManagement/roleDefinitions" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -335,10 +346,11 @@ $JSON_Data $JSON_Convert = $JSON_Data | ConvertFrom-Json $DisplayName = $JSON_Convert.displayName - + write-host write-host "RBAC Intune Role '$DisplayName' Found..." -ForegroundColor Cyan write-host Write-Host "Adding RBAC Intune Role '$DisplayName'" -ForegroundColor Yellow Add-RBACRole -JSON $RAW_JSON - + + diff --git a/RBAC/RBAC_Remove.ps1 b/RBAC/RBAC_Remove.ps1 index b88db4f..2b27652 100644 --- a/RBAC/RBAC_Remove.ps1 +++ b/RBAC/RBAC_Remove.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "DeviceManagementApps.ReadWrite.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-RBACRole(){ +function Get-RBACRole { <# .SYNOPSIS @@ -180,15 +232,15 @@ $Resource = "deviceManagement/roleDefinitions" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") -and $_.isBuiltInRoleDefinition -eq $false } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") -and $_.isBuiltInRoleDefinition -eq $false } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -213,7 +265,7 @@ $Resource = "deviceManagement/roleDefinitions" #################################################### -Function Remove-RBACRole(){ +function Remove-RBACRole { <# .SYNOPSIS @@ -239,7 +291,7 @@ $Resource = "deviceManagement/roleDefinitions/$roleDefinitionId" try { - if($roleDefinitionId -eq "" -or $roleDefinitionId -eq $null){ + if("" -eq $roleDefinitionId -or $null -eq $roleDefinitionId){ Write-Host "roleDefinitionId hasn't been passed as a paramater to the function..." -ForegroundColor Red write-host "Please specify a valid roleDefinitionId..." -ForegroundColor Red @@ -249,7 +301,7 @@ $Resource = "deviceManagement/roleDefinitions/$roleDefinitionId" else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Delete } @@ -277,50 +329,10 @@ $Resource = "deviceManagement/roleDefinitions/$roleDefinitionId" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -355,3 +367,4 @@ $RBAC_Role = Get-RBACRole -Name "Graph" Write-Host } + diff --git a/RBAC/RBAC_ScopeTags_ApplicationAssign.ps1 b/RBAC/RBAC_ScopeTags_ApplicationAssign.ps1 index af26df5..d73c75a 100644 --- a/RBAC/RBAC_ScopeTags_ApplicationAssign.ps1 +++ b/RBAC/RBAC_ScopeTags_ApplicationAssign.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementRBAC.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-RBACScopeTag(){ +function Get-RBACScopeTag { <# .SYNOPSIS @@ -166,7 +217,7 @@ NAME: Get-RBACScopeTag #> [cmdletbinding()] - + param ( [Parameter(Mandatory=$false)] @@ -181,15 +232,15 @@ $Resource = "deviceManagement/roleScopeTags" if($DisplayName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=displayName eq '$DisplayName'" - $Result = (Invoke-RestMethod -Uri $uri -Method Get -Headers $authToken).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=displayName eq '$DisplayName'" + $Result = Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" - $Result = (Invoke-RestMethod -Uri $uri -Method Get -Headers $authToken).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" + $Result = Invoke-IntuneRestMethod -Uri $uri -Method GET } @@ -198,7 +249,7 @@ $Resource = "deviceManagement/roleScopeTags" } catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -215,7 +266,7 @@ $Resource = "deviceManagement/roleScopeTags" #################################################### -Function Get-IntuneApplication(){ +function Get-IntuneApplication { <# .SYNOPSIS @@ -239,31 +290,31 @@ param $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps" - + try { - + if($displayName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)?`$filter=displayName eq '$displayName'" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)?`$filter=displayName eq '$displayName'" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value } - + elseif($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)/$id" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get) + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)/$id" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value | ? { (!($_.'@odata.type').Contains("managed")) } - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | ? { (!($_.'@odata.type').Contains("managed")) } + } } - + catch { $ex = $_.Exception @@ -284,7 +335,7 @@ $Resource = "deviceAppManagement/mobileApps" #################################################### -Function Update-IntuneApplication(){ +function Update-IntuneApplication { <# .SYNOPSIS @@ -317,10 +368,10 @@ $Resource = "deviceAppManagement/mobileApps/$id" Write-Warning "Scope Tags aren't available on '$Type' application Type..." } - + else { - - if($ScopeTags -eq "" -or $ScopeTags -eq $null){ + + if("" -eq $ScopeTags -or $null -eq $ScopeTags){ $JSON = @" @@ -334,14 +385,14 @@ $JSON = @" else { - $object = New-Object –TypeName PSObject + $object = New-Object -TypeName PSObject $object | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value "$Type" $object | Add-Member -MemberType NoteProperty -Name 'roleScopeTagIds' -Value @($ScopeTags) $JSON = $object | ConvertTo-Json } - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Patch -Body $JSON -ContentType "application/json" Start-Sleep -Milliseconds 100 @@ -372,49 +423,10 @@ $JSON = @" #region Authentication -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $Global:User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - Write-Host - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -433,14 +445,14 @@ Write-Host "Please specify Scope Tag you want to add to all users / devices in A $menu = @{} -for ($i=1;$i -le $ScopeTags.count; $i++) -{ Write-Host "$i. $($ScopeTags[$i-1])" +for ($i=1;$i -le $ScopeTags.count; $i++) +{ Write-Host "$i. $($ScopeTags[$i-1])" $menu.Add($i,($ScopeTags[$i-1]))} Write-Host $ans = Read-Host 'Enter Scope Tag id (Numerical value)' -if($ans -eq "" -or $ans -eq $null){ +if("" -eq $ans -or $null -eq $ans){ Write-Host "Scope Tag can't be null, please specify a valid Scope Tag..." -ForegroundColor Red Write-Host @@ -513,10 +525,10 @@ if(@($Application).count -eq 1){ $Result = Update-IntuneApplication -id $IA.id -Type $IA.'@odata.type' -ScopeTags $ST - if($Result -eq ""){ + if("" -eq $Result){ Write-Host "Intune Application '$ADN' patched with ScopeTag '$selection'..." -ForegroundColor Green - + } } @@ -535,7 +547,7 @@ if(@($Application).count -eq 1){ $Result = Update-IntuneApplication -id $IA.id -Type $IA.'@odata.type' -ScopeTags $ST - if($Result -eq ""){ + if("" -eq $Result){ Write-Host "Intune Application '$ADN' patched with ScopeTag '$selection'..." -ForegroundColor Green @@ -561,3 +573,4 @@ Write-Host #################################################### + diff --git a/RBAC/RBAC_ScopeTags_ApplicationUnAssign.ps1 b/RBAC/RBAC_ScopeTags_ApplicationUnAssign.ps1 index 7686a29..ff04fdc 100644 --- a/RBAC/RBAC_ScopeTags_ApplicationUnAssign.ps1 +++ b/RBAC/RBAC_ScopeTags_ApplicationUnAssign.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementRBAC.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-RBACScopeTag(){ +function Get-RBACScopeTag { <# .SYNOPSIS @@ -166,7 +217,7 @@ NAME: Get-RBACScopeTag #> [cmdletbinding()] - + param ( [Parameter(Mandatory=$false)] @@ -181,15 +232,15 @@ $Resource = "deviceManagement/roleScopeTags" if($DisplayName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=displayName eq '$DisplayName'" - $Result = (Invoke-RestMethod -Uri $uri -Method Get -Headers $authToken).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=displayName eq '$DisplayName'" + $Result = Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" - $Result = (Invoke-RestMethod -Uri $uri -Method Get -Headers $authToken).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" + $Result = Invoke-IntuneRestMethod -Uri $uri -Method GET } @@ -198,7 +249,7 @@ $Resource = "deviceManagement/roleScopeTags" } catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -215,7 +266,7 @@ $Resource = "deviceManagement/roleScopeTags" #################################################### -Function Get-IntuneApplication(){ +function Get-IntuneApplication { <# .SYNOPSIS @@ -239,31 +290,31 @@ param $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileApps" - + try { - + if($displayName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)?`$filter=displayName eq '$displayName'" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)?`$filter=displayName eq '$displayName'" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value } - + elseif($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)/$id" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get) + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)/$id" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value | ? { (!($_.'@odata.type').Contains("managed")) } - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | ? { (!($_.'@odata.type').Contains("managed")) } + } } - + catch { $ex = $_.Exception @@ -284,7 +335,7 @@ $Resource = "deviceAppManagement/mobileApps" #################################################### -Function Update-IntuneApplication(){ +function Update-IntuneApplication { <# .SYNOPSIS @@ -317,10 +368,10 @@ $Resource = "deviceAppManagement/mobileApps/$id" Write-Warning "Scope Tags aren't available on '$Type' application Type..." } - + else { - - if($ScopeTags -eq "" -or $ScopeTags -eq $null){ + + if("" -eq $ScopeTags -or $null -eq $ScopeTags){ $JSON = @" @@ -334,14 +385,14 @@ $JSON = @" else { - $object = New-Object –TypeName PSObject + $object = New-Object -TypeName PSObject $object | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value "$Type" $object | Add-Member -MemberType NoteProperty -Name 'roleScopeTagIds' -Value @($ScopeTags) $JSON = $object | ConvertTo-Json } - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Patch -Body $JSON -ContentType "application/json" Start-Sleep -Milliseconds 100 @@ -372,49 +423,10 @@ $JSON = @" #region Authentication -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $Global:User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - Write-Host - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -438,10 +450,10 @@ if(@($Application).count -eq 1){ $Result = Update-IntuneApplication -id $IA.id -Type $IA.'@odata.type' -ScopeTags "" - if($Result -eq ""){ + if("" -eq $Result){ Write-Host "Intune Application '$ADN' patched..." -ForegroundColor Gray - + } Write-Host @@ -461,3 +473,4 @@ else { } #################################################### + diff --git a/RBAC/RBAC_ScopeTags_DeviceAssign.ps1 b/RBAC/RBAC_ScopeTags_DeviceAssign.ps1 index 77588ad..bbaefbe 100644 --- a/RBAC/RBAC_ScopeTags_DeviceAssign.ps1 +++ b/RBAC/RBAC_ScopeTags_DeviceAssign.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementRBAC.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-RBACScopeTag(){ +function Get-RBACScopeTag { <# .SYNOPSIS @@ -166,7 +217,7 @@ NAME: Get-RBACScopeTag #> [cmdletbinding()] - + param ( [Parameter(Mandatory=$false)] @@ -181,15 +232,15 @@ $Resource = "deviceManagement/roleScopeTags" if($DisplayName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=displayName eq '$DisplayName'" - $Result = (Invoke-RestMethod -Uri $uri -Method Get -Headers $authToken).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=displayName eq '$DisplayName'" + $Result = Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" - $Result = (Invoke-RestMethod -Uri $uri -Method Get -Headers $authToken).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" + $Result = Invoke-IntuneRestMethod -Uri $uri -Method GET } @@ -198,7 +249,7 @@ $Resource = "deviceManagement/roleScopeTags" } catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -215,7 +266,7 @@ $Resource = "deviceManagement/roleScopeTags" #################################################### -Function Get-ManagedDevices(){ +function Get-ManagedDevices { <# .SYNOPSIS @@ -254,7 +305,7 @@ try { if($ExcludeMDM.IsPresent){ $Count_Params++ } if($DeviceName.IsPresent){ $Count_Params++ } if($id.IsPresent){ $Count_Params++ } - + if($Count_Params -gt 1){ write-warning "Multiple parameters set, specify a single parameter -IncludeEAS, -ExcludeMDM, -deviceName, -id or no parameter against the function" @@ -262,44 +313,44 @@ try { break } - + elseif($IncludeEAS){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } elseif($ExcludeMDM){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } elseif($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource('$id')" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource('$id')" + (Invoke-IntuneRestMethod -Uri $uri -Method GET) } elseif($DeviceName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=deviceName eq '$DeviceName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=deviceName eq '$DeviceName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - + else { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'" + + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'" Write-Warning "EAS Devices are excluded by default, please use -IncludeEAS if you want to include those devices" Write-Host - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - + } catch { @@ -321,7 +372,7 @@ try { #################################################### -Function Update-ManagedDevices(){ +function Update-ManagedDevices { <# .SYNOPSIS @@ -348,7 +399,7 @@ $Resource = "deviceManagement/managedDevices('$id')" try { - if($ScopeTags -eq "" -or $ScopeTags -eq $null){ + if("" -eq $ScopeTags -or $null -eq $ScopeTags){ $JSON = @" @@ -361,13 +412,13 @@ $JSON = @" else { - $object = New-Object –TypeName PSObject + $object = New-Object -TypeName PSObject $object | Add-Member -MemberType NoteProperty -Name 'roleScopeTagIds' -Value @($ScopeTags) $JSON = $object | ConvertTo-Json } - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Patch -Body $JSON -ContentType "application/json" Start-Sleep -Milliseconds 100 @@ -396,49 +447,10 @@ $JSON = @" #region Authentication -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $Global:User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - Write-Host - $Global:User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -457,14 +469,14 @@ Write-Host "Please specify Scope Tag you want to assign:" -ForegroundColor Yello $menu = @{} -for ($i=1;$i -le $ScopeTags.count; $i++) -{ Write-Host "$i. $($ScopeTags[$i-1])" +for ($i=1;$i -le $ScopeTags.count; $i++) +{ Write-Host "$i. $($ScopeTags[$i-1])" $menu.Add($i,($ScopeTags[$i-1]))} Write-Host $ans = Read-Host 'Enter Scope Tag id (Numerical value)' -if($ans -eq "" -or $ans -eq $null){ +if("" -eq $ans -or $null -eq $ans){ Write-Host "Scope Tag can't be null, please specify a valid Scope Tag..." -ForegroundColor Red Write-Host @@ -539,10 +551,10 @@ if($IntuneDevice){ $Result = Update-ManagedDevices -id $MD.id -ScopeTags $ST - if($Result -eq ""){ + if("" -eq $Result){ Write-Host "Managed Device '$DeviceName' patched with ScopeTag '$selection'..." -ForegroundColor Green - + } } @@ -561,7 +573,7 @@ if($IntuneDevice){ $Result = Update-ManagedDevices -id $MD.id -ScopeTags $ST - if($Result -eq ""){ + if("" -eq $Result){ Write-Host "Managed Device '$DeviceName' patched with ScopeTag '$selection'..." -ForegroundColor Green @@ -597,3 +609,4 @@ Write-Host #################################################### + diff --git a/RBAC/RBAC_ScopeTags_DeviceUnAssign.ps1 b/RBAC/RBAC_ScopeTags_DeviceUnAssign.ps1 index cac6d68..c1ea975 100644 --- a/RBAC/RBAC_ScopeTags_DeviceUnAssign.ps1 +++ b/RBAC/RBAC_ScopeTags_DeviceUnAssign.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementRBAC.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-ManagedDevices(){ +function Get-ManagedDevices { <# .SYNOPSIS @@ -190,7 +241,7 @@ try { if($ExcludeMDM.IsPresent){ $Count_Params++ } if($DeviceName.IsPresent){ $Count_Params++ } if($id.IsPresent){ $Count_Params++ } - + if($Count_Params -gt 1){ write-warning "Multiple parameters set, specify a single parameter -IncludeEAS, -ExcludeMDM, -deviceName, -id or no parameter against the function" @@ -198,44 +249,44 @@ try { break } - + elseif($IncludeEAS){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } elseif($ExcludeMDM){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } elseif($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource('$id')" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource('$id')" + (Invoke-IntuneRestMethod -Uri $uri -Method GET) } elseif($DeviceName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=deviceName eq '$DeviceName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=deviceName eq '$DeviceName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - + else { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'" + + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'" Write-Warning "EAS Devices are excluded by default, please use -IncludeEAS if you want to include those devices" Write-Host - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - + } catch { @@ -257,7 +308,7 @@ try { #################################################### -Function Update-ManagedDevices(){ +function Update-ManagedDevices { <# .SYNOPSIS @@ -284,7 +335,7 @@ $Resource = "deviceManagement/managedDevices('$id')" try { - if($ScopeTags -eq "" -or $ScopeTags -eq $null){ + if("" -eq $ScopeTags -or $null -eq $ScopeTags){ $JSON = @" @@ -297,7 +348,7 @@ $JSON = @" else { - $object = New-Object –TypeName PSObject + $object = New-Object -TypeName PSObject $object | Add-Member -MemberType NoteProperty -Name 'roleScopeTagIds' -Value @($ScopeTags) $JSON = $object | ConvertTo-Json @@ -305,7 +356,7 @@ $JSON = @" } - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Patch -Body $JSON -ContentType "application/json" } @@ -332,50 +383,10 @@ $JSON = @" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -394,7 +405,7 @@ if($IntuneDevice){ $Confirm = read-host if($Confirm -eq "y" -or $Confirm -eq "Y"){ - + $DeviceID = $IntuneDevice.id $DeviceName = $IntuneDevice.deviceName @@ -402,12 +413,12 @@ if($IntuneDevice){ $Result = Update-ManagedDevices -id $DeviceID -ScopeTags "" - if($Result -eq ""){ + if("" -eq $Result){ Write-Host "Managed Device '$DeviceName' patched with No Scope Tag assigned..." -ForegroundColor Gray } - + } else { @@ -434,3 +445,4 @@ Write-Host "No Intune Managed Device found with name '$deviceName'..." -Foregrou Write-Host } + diff --git a/RBAC/RBAC_ScopeTags_PolicyAssign.ps1 b/RBAC/RBAC_ScopeTags_PolicyAssign.ps1 index 1d138aa..2a36230 100644 --- a/RBAC/RBAC_ScopeTags_PolicyAssign.ps1 +++ b/RBAC/RBAC_ScopeTags_PolicyAssign.ps1 @@ -1,158 +1,209 @@ - + <# -  + .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. #> -#################################################### -  -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementRBAC.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - } + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null + [Parameter(Mandatory = $false)] + [object]$Body = $null, -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) -$clientId = "" -  -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" -  -$resourceAppIdURI = "https://graph.microsoft.com" -  -$authority = "https://login.microsoftonline.com/$Tenant" -  try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } -} -  #################################################### -Function Get-DeviceCompliancePolicy(){ +function Get-DeviceCompliancePolicy { <# .SYNOPSIS @@ -203,43 +254,43 @@ $Resource = "deviceManagement/deviceCompliancePolicies" elseif($Android){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("android") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("android") } } elseif($iOS){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("ios") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("ios") } } elseif($Win10){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") } } elseif($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } elseif($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)/$id`?`$expand=assignments,scheduledActionsForRule(`$expand=scheduledActionConfigurations)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)/$id`?`$expand=assignments,scheduledActionsForRule(`$expand=scheduledActionConfigurations)" + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -264,7 +315,7 @@ $Resource = "deviceManagement/deviceCompliancePolicies" #################################################### -Function Get-DeviceConfigurationPolicy(){ +function Get-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -293,22 +344,22 @@ $DCP_resource = "deviceManagement/deviceConfigurations" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } elseif($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)/$id" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)/$id" + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -333,7 +384,7 @@ $DCP_resource = "deviceManagement/deviceConfigurations" #################################################### -Function Update-DeviceCompliancePolicy(){ +function Update-DeviceCompliancePolicy { <# .SYNOPSIS @@ -363,8 +414,8 @@ $graphApiVersion = "beta" $Resource = "deviceManagement/deviceCompliancePolicies/$id" try { - - if($ScopeTags -eq "" -or $ScopeTags -eq $null){ + + if("" -eq $ScopeTags -or $null -eq $ScopeTags){ $JSON = @" @@ -378,14 +429,14 @@ $JSON = @" else { - $object = New-Object –TypeName PSObject + $object = New-Object -TypeName PSObject $object | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value "$Type" $object | Add-Member -MemberType NoteProperty -Name 'roleScopeTagIds' -Value @($ScopeTags) $JSON = $object | ConvertTo-Json } - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Patch -Body $JSON -ContentType "application/json" Start-Sleep -Milliseconds 100 @@ -412,7 +463,7 @@ $JSON = @" #################################################### -Function Update-DeviceConfigurationPolicy(){ +function Update-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -442,8 +493,8 @@ $graphApiVersion = "beta" $Resource = "deviceManagement/deviceConfigurations/$id" try { - - if($ScopeTags -eq "" -or $ScopeTags -eq $null){ + + if("" -eq $ScopeTags -or $null -eq $ScopeTags){ $JSON = @" @@ -457,14 +508,14 @@ $JSON = @" else { - $object = New-Object –TypeName PSObject + $object = New-Object -TypeName PSObject $object | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value "$Type" $object | Add-Member -MemberType NoteProperty -Name 'roleScopeTagIds' -Value @($ScopeTags) $JSON = $object | ConvertTo-Json } - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Patch -Body $JSON -ContentType "application/json" Start-Sleep -Milliseconds 100 @@ -491,7 +542,7 @@ $JSON = @" #################################################### -Function Get-RBACScopeTag(){ +function Get-RBACScopeTag { <# .SYNOPSIS @@ -506,7 +557,7 @@ NAME: Get-RBACScopeTag #> [cmdletbinding()] - + param ( [Parameter(Mandatory=$false)] @@ -521,15 +572,15 @@ $Resource = "deviceManagement/roleScopeTags" if($DisplayName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=displayName eq '$DisplayName'" - $Result = (Invoke-RestMethod -Uri $uri -Method Get -Headers $authToken).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource`?`$filter=displayName eq '$DisplayName'" + $Result = Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" - $Result = (Invoke-RestMethod -Uri $uri -Method Get -Headers $authToken).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$Resource" + $Result = Invoke-IntuneRestMethod -Uri $uri -Method GET } @@ -538,7 +589,7 @@ $Resource = "deviceManagement/roleScopeTags" } catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -557,50 +608,10 @@ $Resource = "deviceManagement/roleScopeTags" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -630,7 +641,7 @@ if($Confirm -eq "y" -or $Confirm -eq "Y"){ Write-Host "Checking Scope Tag '$ScopeTag_DN'..." -ForegroundColor Cyan #################################################### - + #region Device Compliance Policies Write-Host "Testing if '$ScopeTag_DN' exists in Device Compliance Policy Display Name..." @@ -640,7 +651,7 @@ if($Confirm -eq "y" -or $Confirm -eq "Y"){ if($CPs){ foreach($Policy in $CPs){ - + $CP = Get-DeviceCompliancePolicy -id $Policy.id $CP_DN = $CP.displayName @@ -653,7 +664,7 @@ if($Confirm -eq "y" -or $Confirm -eq "Y"){ $Result = Update-DeviceCompliancePolicy -id $Policy.id -Type $Policy.'@odata.type' -ScopeTags $ST - if($Result -eq ""){ + if("" -eq $Result){ Write-Host "Compliance Policy '$CP_DN' patched with '$ScopeTag_DN' ScopeTag..." -ForegroundColor Green @@ -675,7 +686,7 @@ if($Confirm -eq "y" -or $Confirm -eq "Y"){ $Result = Update-DeviceCompliancePolicy -id $Policy.id -Type $Policy.'@odata.type' -ScopeTags $ST - if($Result -eq ""){ + if("" -eq $Result){ Write-Host "Compliance Policy '$CP_DN' patched with '$ScopeTag_DN' ScopeTag..." -ForegroundColor Green @@ -702,7 +713,7 @@ if($Confirm -eq "y" -or $Confirm -eq "Y"){ if($DCPs){ foreach($Policy in $DCPs){ - + $DCP = Get-DeviceConfigurationPolicy -id $Policy.id $DCP_DN = $DCP.displayName @@ -715,7 +726,7 @@ if($Confirm -eq "y" -or $Confirm -eq "Y"){ $Result = Update-DeviceConfigurationPolicy -id $Policy.id -Type $Policy.'@odata.type' -ScopeTags $ST - if($Result -eq ""){ + if("" -eq $Result){ Write-Host "Configuration Policy '$DCP_DN' patched with '$ScopeTag_DN' ScopeTag..." -ForegroundColor Green @@ -737,7 +748,7 @@ if($Confirm -eq "y" -or $Confirm -eq "Y"){ $Result = Update-DeviceConfigurationPolicy -id $Policy.id -Type $Policy.'@odata.type' -ScopeTags $ST - if($Result -eq ""){ + if("" -eq $Result){ Write-Host "Configuration Policy '$DCP_DN' patched with '$ScopeTag_DN' ScopeTag..." -ForegroundColor Green @@ -774,3 +785,4 @@ else { } Write-Host + diff --git a/RBAC/RBAC_ScopeTags_PolicyUnAssign.ps1 b/RBAC/RBAC_ScopeTags_PolicyUnAssign.ps1 index 7ee60b1..cf67cd2 100644 --- a/RBAC/RBAC_ScopeTags_PolicyUnAssign.ps1 +++ b/RBAC/RBAC_ScopeTags_PolicyUnAssign.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementRBAC.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - } + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null + [Parameter(Mandatory = $false)] + [object]$Body = $null, -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) -$clientId = "" - -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" - -$resourceAppIdURI = "https://graph.microsoft.com" - -$authority = "https://login.microsoftonline.com/$Tenant" - try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } -} - #################################################### -Function Get-DeviceCompliancePolicy(){ +function Get-DeviceCompliancePolicy { <# .SYNOPSIS @@ -202,43 +253,43 @@ $Resource = "deviceManagement/deviceCompliancePolicies" elseif($Android){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("android") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("android") } } elseif($iOS){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("ios") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("ios") } } elseif($Win10){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") } } elseif($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } elseif($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)/$id`?`$expand=assignments,scheduledActionsForRule(`$expand=scheduledActionConfigurations)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)/$id`?`$expand=assignments,scheduledActionsForRule(`$expand=scheduledActionConfigurations)" + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -263,7 +314,7 @@ $Resource = "deviceManagement/deviceCompliancePolicies" #################################################### -Function Get-DeviceConfigurationPolicy(){ +function Get-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -292,22 +343,22 @@ $DCP_resource = "deviceManagement/deviceConfigurations" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } elseif($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)/$id" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)/$id" + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -332,7 +383,7 @@ $DCP_resource = "deviceManagement/deviceConfigurations" #################################################### -Function Update-DeviceCompliancePolicy(){ +function Update-DeviceCompliancePolicy { <# .SYNOPSIS @@ -362,8 +413,8 @@ $graphApiVersion = "beta" $Resource = "deviceManagement/deviceCompliancePolicies/$id" try { - - if($ScopeTags -eq "" -or $ScopeTags -eq $null){ + + if("" -eq $ScopeTags -or $null -eq $ScopeTags){ $JSON = @" @@ -377,14 +428,14 @@ $JSON = @" else { - $object = New-Object –TypeName PSObject + $object = New-Object -TypeName PSObject $object | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value "$Type" $object | Add-Member -MemberType NoteProperty -Name 'roleScopeTagIds' -Value @($ScopeTags) $JSON = $object | ConvertTo-Json } - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Patch -Body $JSON -ContentType "application/json" Start-Sleep -Milliseconds 100 @@ -411,7 +462,7 @@ $JSON = @" #################################################### -Function Update-DeviceConfigurationPolicy(){ +function Update-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -441,8 +492,8 @@ $graphApiVersion = "beta" $Resource = "deviceManagement/deviceConfigurations/$id" try { - - if($ScopeTags -eq "" -or $ScopeTags -eq $null){ + + if("" -eq $ScopeTags -or $null -eq $ScopeTags){ $JSON = @" @@ -456,14 +507,14 @@ $JSON = @" else { - $object = New-Object –TypeName PSObject + $object = New-Object -TypeName PSObject $object | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value "$Type" $object | Add-Member -MemberType NoteProperty -Name 'roleScopeTagIds' -Value @($ScopeTags) $JSON = $object | ConvertTo-Json } - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Patch -Body $JSON -ContentType "application/json" Start-Sleep -Milliseconds 100 @@ -492,50 +543,10 @@ $JSON = @" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -561,7 +572,7 @@ if($Confirm -eq "y" -or $Confirm -eq "Y"){ $Result = Update-DeviceCompliancePolicy -id $Policy.id -Type $Policy.'@odata.type' -ScopeTags "" - if($Result -eq ""){ + if("" -eq $Result){ Write-Host "Compliance Policy '$PolicyDN' patched..." -ForegroundColor Gray @@ -585,10 +596,10 @@ if($Confirm -eq "y" -or $Confirm -eq "Y"){ foreach($Policy in $DCPs){ $PolicyDN = $Policy.displayName - + $Result = Update-DeviceConfigurationPolicy -id $Policy.id -Type $Policy.'@odata.type' -ScopeTags "" - if($Result -eq ""){ + if("" -eq $Result){ Write-Host "Configuration Policy '$PolicyDN' patched..." -ForegroundColor Gray @@ -607,3 +618,4 @@ else { } Write-Host + diff --git a/RBAC/RBAC_UserStatus.ps1 b/RBAC/RBAC_UserStatus.ps1 index 89bdde4..14a7174 100644 --- a/RBAC/RBAC_UserStatus.ps1 +++ b/RBAC/RBAC_UserStatus.ps1 @@ -7,153 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { - <# - .SYNOPSIS - This function is used to authenticate with the Graph API REST interface - .DESCRIPTION - The function authenticate with the Graph API Interface with the tenant name - .EXAMPLE - Get-AuthToken - Authenticates you with the Graph API interface - .NOTES - NAME: Get-AuthToken - #> - - [cmdletbinding()] - - param - ( - [Parameter(Mandatory=$true)] - $User +function Connect-GraphAPI { +<# +.SYNOPSIS +Connects to Microsoft Graph API with appropriate scopes for Intune operations +.DESCRIPTION +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment +.NOTES +Requires Microsoft.Graph.Authentication module +#> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementRBAC.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" ) - - $userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - - $tenant = $userUpn.Host - - Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - + + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit + + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false } - - # Getting path to ActiveDirectory Assemblies - # If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true } - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + Write-Error "Failed to establish connection to Microsoft Graph" + return $false } - - [System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - - [System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - - # Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information - # on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - - $clientId = "" - - $redirectUri = "urn:ietf:wg:oauth:2.0:oob" - - $resourceAppIdURI = "https://graph.microsoft.com" - - $authority = "https://login.microsoftonline.com/$Tenant" - - try { - - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" - - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result - - # If the accesstoken is valid then create the authentication header - - if($authResult.AccessToken){ - - # Creating header for Authorization token - - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + } + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false + } +} + +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, + + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', + + [Parameter(Mandatory = $false)] + [object]$Body = $null, + + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) + + try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } + + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } + + $results = @() + $nextLink = $Uri + + do { + Write-Verbose "Making request to: $nextLink" + + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } + + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" } - - return $authHeader - } - + + $response = Invoke-MgGraphRequest @requestParams + + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + $results += $response + $nextLink = $null } - + + } while ($nextLink) + + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break - + else { + Write-Error "Graph API request failed: $errorMessage" } - + throw } - +} + +#################################################### + +#################################################### + + #################################################### - - Function Get-AADUser(){ - + + function Get-AADUser { + <# .SYNOPSIS This function is used to get AAD Users from the Graph API REST interface @@ -168,52 +219,52 @@ function Get-AuthToken { .NOTES NAME: Get-AADUser #> - + [cmdletbinding()] - + param ( $userPrincipalName, $Property ) - + # Defining Variables $graphApiVersion = "v1.0" $User_resource = "users" - + try { - - if($userPrincipalName -eq "" -or $userPrincipalName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + if("" -eq $userPrincipalName -or $null -eq $userPrincipalName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + else { - - if($Property -eq "" -or $Property -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName" + + if("" -eq $Property -or $null -eq $Property){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName" Write-Verbose $uri - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + Invoke-IntuneRestMethod -Uri $uri -Method GET + } - + else { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + } - + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -224,15 +275,15 @@ function Get-AuthToken { Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } - + #################################################### - - Function Get-AADGroup(){ - + + function Get-AADGroup { + <# .SYNOPSIS This function is used to get AAD Groups from the Graph API REST interface @@ -244,70 +295,70 @@ function Get-AuthToken { .NOTES NAME: Get-AADGroup #> - + [cmdletbinding()] - + param ( $GroupName, $id, [switch]$Members ) - + # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { - + if($id){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + else { - + if(!$Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ - + $GID = $Group.id - + $Group.displayName write-host - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + } - + } - + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -318,15 +369,15 @@ function Get-AuthToken { Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } - + #################################################### - - Function Get-RBACRole(){ - + + function Get-RBACRole { + <# .SYNOPSIS This function is used to get RBAC Role Definitions from the Graph API REST interface @@ -338,19 +389,19 @@ function Get-AuthToken { .NOTES NAME: Get-RBACRole #> - + $graphApiVersion = "Beta" $Resource = "deviceManagement/roleDefinitions" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -361,15 +412,15 @@ function Get-AuthToken { Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } - + #################################################### - - Function Get-RBACRoleDefinition(){ - + + function Get-RBACRoleDefinition { + <# .SYNOPSIS This function is used to get an RBAC Role Definition from the Graph API REST interface @@ -381,33 +432,33 @@ function Get-AuthToken { .NOTES NAME: Get-RBACRoleDefinition #> - + [cmdletbinding()] - + param ( $id ) - + $graphApiVersion = "Beta" $Resource = "deviceManagement/roleDefinitions('$id')?`$expand=roleassignments" - + try { - + if(!$id){ - + write-host "No Role ID was passed to the function, provide an ID variable" -f Red break - + } - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).roleAssignments - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).roleAssignments + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -418,15 +469,15 @@ function Get-AuthToken { Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } - + #################################################### - - Function Get-RBACRoleAssignment(){ - + + function Get-RBACRoleAssignment { + <# .SYNOPSIS This function is used to get an RBAC Role Assignment from the Graph API REST interface @@ -438,33 +489,33 @@ function Get-AuthToken { .NOTES NAME: Get-RBACRoleAssignment #> - + [cmdletbinding()] - + param ( $id ) - + $graphApiVersion = "Beta" $Resource = "deviceManagement/roleAssignments('$id')?`$expand=microsoft.graph.deviceAndAppManagementRoleAssignment/roleScopeTags" - + try { - + if(!$id){ - + write-host "No Role Assignment ID was passed to the function, provide an ID variable" -f Red break - + } - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET) + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -475,83 +526,43 @@ function Get-AuthToken { Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } - + #################################################### - + #region Authentication - - write-host - - # Checking if authToken exists before running authentication - if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } - } - - # Authentication doesn't exist, calling Get-AuthToken function - - else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - # Getting the authorization token - $global:authToken = Get-AuthToken -User $User - - } - - #endregion - + +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 +} + +#endregion + #################################################### - + write-host write-host "Please specify the User Principal Name you want to query:" -f Yellow $UPN = Read-Host - - if($UPN -eq $null -or $UPN -eq ""){ - + + if($null -eq $UPN -or "" -eq $UPN){ + Write-Host "Valid UPN not specified, script can't continue..." -f Red Write-Host break - + } - + $User = Get-AADUser -userPrincipalName $UPN - + $UserID = $User.id $UserDN = $User.displayName $UserPN = $User.userPrincipalName - + Write-Host write-host "-------------------------------------------------------------------" write-host @@ -559,199 +570,200 @@ function Get-AuthToken { write-host "User ID:"$User.id write-host "User Principal Name:"$User.userPrincipalName write-host - + #################################################### - + $MemberOf = Get-AADUser -userPrincipalName $UPN -Property MemberOf - + $DirectoryRole = $MemberOf | ? { $_.'@odata.type' -eq "#microsoft.graph.directoryRole" } - + if($DirectoryRole){ - + $DirRole = $DirectoryRole.displayName - + write-host "Directory Role:" -f Yellow $DirectoryRole.displayName write-host - + } - + else { - + write-host "Directory Role:" -f Yellow Write-Host "User" write-host - + } - + #################################################### - + $AADGroups = $MemberOf | ? { $_.'@odata.type' -eq "#microsoft.graph.group" } | sort displayName - + if($AADGroups){ - + write-host "AAD Group Membership:" -f Yellow - + foreach($AADGroup in $AADGroups){ - + $GroupDN = (Get-AADGroup -id $AADGroup.id).displayName - + $GroupDN - + } - + write-host - + } - + else { - + write-host "AAD Group Membership:" -f Yellow write-host "No Group Membership in AAD Groups" Write-Host - + } - + #################################################### - + write-host "-------------------------------------------------------------------" - + # Getting all Intune Roles defined $RBAC_Roles = Get-RBACRole - + $UserRoleCount = 0 - + $Permissions = @() - + # Looping through all Intune Roles defined foreach($RBAC_Role in $RBAC_Roles){ - + $RBAC_id = $RBAC_Role.id - + $RoleAssignments = Get-RBACRoleDefinition -id $RBAC_id - + # If an Intune Role has an Assignment check if the user is a member of members group if($RoleAssignments){ - + $RoleAssignments | foreach { - + $RBAC_Role_Assignments = $_.id - + $Assignment = Get-RBACRoleAssignment -id $RBAC_Role_Assignments - + $RA_Names = @() - + $Members = $Assignment.members $ScopeMembers = $Assignment.scopeMembers $ScopeTags = $Assignment.roleScopeTags - + $Members | foreach { - + if($AADGroups.id -contains $_){ - + $RA_Names += (Get-AADGroup -id $_).displayName - + } - + } - + if($RA_Names){ - + $UserRoleCount++ - + Write-Host write-host "RBAC Role Assigned: " $RBAC_Role.displayName -ForegroundColor Cyan $Permissions += $RBAC_Role.permissions.actions Write-Host - + write-host "Assignment Display Name:" $Assignment.displayName -ForegroundColor Yellow Write-Host - - Write-Host "Assignment - Members:" -f Yellow + + Write-Host "Assignment - Members:" -f Yellow $RA_Names - + Write-Host Write-Host "Assignment - Scope (Groups):" -f Yellow - + if($Assignment.scopeType -eq "resourceScope"){ - + $ScopeMembers | foreach { - + (Get-AADGroup -id $_).displayName - + } - + } - + else { - + Write-Host ($Assignment.ScopeType -creplace '([A-Z\W_]|\d+)(? -#################################################### -  -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - } + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique - - } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null + [Parameter(Mandatory = $false)] + [object]$Body = $null, -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) -$clientId = "" -  -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" -  -$resourceAppIdURI = "https://graph.microsoft.com" -  -$authority = "https://login.microsoftonline.com/$Tenant" -  try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } -} -  #################################################### -Function Get-RemoteActionAudit(){ +function Get-RemoteActionAudit { <# .SYNOPSIS @@ -173,8 +225,8 @@ $Resource = "deviceManagement/remoteActionAudits" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } @@ -199,50 +251,10 @@ $Resource = "deviceManagement/remoteActionAudits" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -250,3 +262,4 @@ $global:authToken = Get-AuthToken -User $User #################################################### Get-RemoteActionAudit + diff --git a/SettingsCatalog/SettingsCatalog_Export.ps1 b/SettingsCatalog/SettingsCatalog_Export.ps1 index aedbce8..bb388d8 100644 --- a/SettingsCatalog/SettingsCatalog_Export.ps1 +++ b/SettingsCatalog/SettingsCatalog_Export.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-SettingsCatalogPolicy(){ +function Get-SettingsCatalogPolicy { <# .SYNOPSIS @@ -183,7 +235,7 @@ param $graphApiVersion = "beta" if($Platform){ - + $Resource = "deviceManagement/configurationPolicies?`$filter=platforms has '$Platform' and technologies has 'mdm'" } @@ -196,8 +248,8 @@ $graphApiVersion = "beta" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -220,7 +272,7 @@ $graphApiVersion = "beta" #################################################### -Function Get-SettingsCatalogPolicySettings(){ +function Get-SettingsCatalogPolicySettings { <# .SYNOPSIS @@ -248,15 +300,15 @@ $Resource = "deviceManagement/configurationPolicies('$policyid')/settings?`$expa try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" - $Response = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) + $Response = (Invoke-IntuneRestMethod -Uri $uri -Method GET) $AllResponses = $Response.value - + $ResponseNextLink = $Response."@odata.nextLink" - while ($ResponseNextLink -ne $null){ + while ($null -ne $ResponseNextLink){ $Response = (Invoke-RestMethod -Uri $ResponseNextLink -Headers $authToken -Method Get) $ResponseNextLink = $Response."@odata.nextLink" @@ -287,7 +339,7 @@ $Resource = "deviceManagement/configurationPolicies('$policyid')/settings?`$expa #################################################### -Function Export-JSONData(){ +function Export-JSONData { <# .SYNOPSIS @@ -310,7 +362,7 @@ $ExportPath try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON..." -f Red @@ -345,7 +397,7 @@ $ExportPath $JSON1 | Set-Content -LiteralPath "$ExportPath\$FileName_JSON" write-host "JSON created in $ExportPath\$FileName_JSON..." -f cyan - + } } @@ -362,50 +414,10 @@ $ExportPath #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -508,3 +520,4 @@ else { Write-Host } + diff --git a/SettingsCatalog/SettingsCatalog_Get.ps1 b/SettingsCatalog/SettingsCatalog_Get.ps1 index c7bd324..fe37688 100644 --- a/SettingsCatalog/SettingsCatalog_Get.ps1 +++ b/SettingsCatalog/SettingsCatalog_Get.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-SettingsCatalogPolicy(){ +function Get-SettingsCatalogPolicy { <# .SYNOPSIS @@ -183,7 +235,7 @@ param $graphApiVersion = "beta" if($Platform){ - + $Resource = "deviceManagement/configurationPolicies?`$filter=platforms has '$Platform' and technologies has 'mdm'" } @@ -196,8 +248,8 @@ $graphApiVersion = "beta" try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -220,7 +272,7 @@ $graphApiVersion = "beta" #################################################### -Function Get-SettingsCatalogPolicySettings(){ +function Get-SettingsCatalogPolicySettings { <# .SYNOPSIS @@ -248,15 +300,15 @@ $Resource = "deviceManagement/configurationPolicies('$policyid')/settings?`$expa try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" - $Response = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) + $Response = (Invoke-IntuneRestMethod -Uri $uri -Method GET) $AllResponses = $Response.value - + $ResponseNextLink = $Response."@odata.nextLink" - while ($ResponseNextLink -ne $null){ + while ($null -ne $ResponseNextLink){ $Response = (Invoke-RestMethod -Uri $ResponseNextLink -Headers $authToken -Method Get) $ResponseNextLink = $Response."@odata.nextLink" @@ -289,50 +341,10 @@ $Resource = "deviceManagement/configurationPolicies('$policyid')/settings?`$expa #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -405,3 +417,4 @@ else { Write-Host } + diff --git a/SettingsCatalog/SettingsCatalog_Import_FromJSON.ps1 b/SettingsCatalog/SettingsCatalog_Import_FromJSON.ps1 index 98a9eac..321f847 100644 --- a/SettingsCatalog/SettingsCatalog_Import_FromJSON.ps1 +++ b/SettingsCatalog/SettingsCatalog_Import_FromJSON.ps1 @@ -1,4 +1,4 @@ - + <# .COPYRIGHT @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-SettingsCatalogPolicy(){ +function Add-SettingsCatalogPolicy { <# .SYNOPSIS @@ -180,7 +232,7 @@ $Resource = "deviceManagement/configurationPolicies" try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the Endpoint Security Disk Encryption Policy..." -f Red @@ -190,13 +242,13 @@ $Resource = "deviceManagement/configurationPolicies" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { $ex = $_.Exception @@ -216,7 +268,7 @@ $Resource = "deviceManagement/configurationPolicies" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -251,7 +303,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -263,50 +315,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -337,7 +349,7 @@ $JSON_Convert = $JSON_Data | ConvertFrom-Json | Select-Object -Property * -Exclu $DisplayName = $JSON_Convert.name $JSON_Output = $JSON_Convert | ConvertTo-Json -Depth 20 - + write-host write-host "Settings Catalog Policy '$DisplayName' Found..." -ForegroundColor Yellow write-host @@ -345,3 +357,4 @@ $JSON_Output write-host Write-Host "Adding Settings Catalog Policy '$DisplayName'" -ForegroundColor Yellow Add-SettingsCatalogPolicy -JSON $JSON_Output + diff --git a/SoftwareUpdates/SoftwareUpdates_Export.ps1 b/SoftwareUpdates/SoftwareUpdates_Export.ps1 index dd8edaa..3fe9a35 100644 --- a/SoftwareUpdates/SoftwareUpdates_Export.ps1 +++ b/SoftwareUpdates/SoftwareUpdates_Export.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-SoftwareUpdatePolicy(){ +function Get-SoftwareUpdatePolicy { <# .SYNOPSIS @@ -160,7 +212,7 @@ This function is used to get Software Update policies from the Graph API REST in .DESCRIPTION The function connects to the Graph API Interface and gets any Software Update policies .EXAMPLE -Get-SoftwareUpdatePolicy -Windows10 +Get-SoftwareUpdatePolicy -Windows Returns Windows 10 Software Update policies configured in Intune .EXAMPLE Get-SoftwareUpdatePolicy -iOS @@ -173,7 +225,7 @@ NAME: Get-SoftwareUpdatePolicy param ( - [switch]$Windows10, + [switch]$Windows, [switch]$iOS ) @@ -184,28 +236,28 @@ $graphApiVersion = "Beta" $Count_Params = 0 if($iOS.IsPresent){ $Count_Params++ } - if($Windows10.IsPresent){ $Count_Params++ } + if($Windows.IsPresent){ $Count_Params++ } if($Count_Params -gt 1){ - write-host "Multiple parameters set, specify a single parameter -iOS or -Windows10 against the function" -f Red + write-host "Multiple parameters set, specify a single parameter -iOS or -Windows against the function" -f Red } elseif($Count_Params -eq 0){ - Write-Host "Parameter -iOS or -Windows10 required against the function..." -ForegroundColor Red + Write-Host "Parameter -iOS or -Windows required against the function..." -ForegroundColor Red Write-Host break } - elseif($Windows10){ + elseif($Windows){ $Resource = "deviceManagement/deviceConfigurations?`$filter=isof('microsoft.graph.windowsUpdateForBusinessConfiguration')&`$expand=groupAssignments" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } @@ -213,8 +265,8 @@ $graphApiVersion = "Beta" $Resource = "deviceManagement/deviceConfigurations?`$filter=isof('microsoft.graph.iosUpdateConfiguration')&`$expand=groupAssignments" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -239,7 +291,7 @@ $graphApiVersion = "Beta" #################################################### -Function Export-JSONData(){ +function Export-JSONData { <# .SYNOPSIS @@ -262,7 +314,7 @@ $ExportPath try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON..." -f Red @@ -327,50 +379,10 @@ $ExportPath #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -408,7 +420,7 @@ $ExportPath = Read-Host -Prompt "Please specify a path to export the policy data #################################################### -$WSUPs = Get-SoftwareUpdatePolicy -Windows10 +$WSUPs = Get-SoftwareUpdatePolicy -Windows if($WSUPs){ @@ -452,3 +464,4 @@ else { } + diff --git a/SoftwareUpdates/SoftwareUpdates_Get.ps1 b/SoftwareUpdates/SoftwareUpdates_Get.ps1 index d4457cf..7f1a1d8 100644 --- a/SoftwareUpdates/SoftwareUpdates_Get.ps1 +++ b/SoftwareUpdates/SoftwareUpdates_Get.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-SoftwareUpdatePolicy(){ +function Get-SoftwareUpdatePolicy { <# .SYNOPSIS @@ -160,7 +212,7 @@ This function is used to get Software Update policies from the Graph API REST in .DESCRIPTION The function connects to the Graph API Interface and gets any Software Update policies .EXAMPLE -Get-SoftwareUpdatePolicy -Windows10 +Get-SoftwareUpdatePolicy -Windows Returns Windows 10 Software Update policies configured in Intune .EXAMPLE Get-SoftwareUpdatePolicy -iOS @@ -173,7 +225,7 @@ NAME: Get-SoftwareUpdatePolicy param ( - [switch]$Windows10, + [switch]$Windows, [switch]$iOS ) @@ -184,28 +236,28 @@ $graphApiVersion = "Beta" $Count_Params = 0 if($iOS.IsPresent){ $Count_Params++ } - if($Windows10.IsPresent){ $Count_Params++ } + if($Windows.IsPresent){ $Count_Params++ } if($Count_Params -gt 1){ - write-host "Multiple parameters set, specify a single parameter -iOS or -Windows10 against the function" -f Red + write-host "Multiple parameters set, specify a single parameter -iOS or -Windows against the function" -f Red } elseif($Count_Params -eq 0){ - Write-Host "Parameter -iOS or -Windows10 required against the function..." -ForegroundColor Red + Write-Host "Parameter -iOS or -Windows required against the function..." -ForegroundColor Red Write-Host break } - elseif($Windows10){ + elseif($Windows){ $Resource = "deviceManagement/deviceConfigurations?`$filter=isof('microsoft.graph.windowsUpdateForBusinessConfiguration')&`$expand=groupAssignments" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).value } @@ -213,8 +265,8 @@ $graphApiVersion = "Beta" $Resource = "deviceManagement/deviceConfigurations?`$filter=isof('microsoft.graph.iosUpdateConfiguration')&`$expand=groupAssignments" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -239,7 +291,7 @@ $graphApiVersion = "Beta" #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -270,15 +322,15 @@ $Group_resource = "groups" if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - elseif($GroupName -eq "" -or $GroupName -eq $null){ + elseif("" -eq $GroupName -or $null -eq $GroupName){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -286,15 +338,15 @@ $Group_resource = "groups" if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } elseif($Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value if($Group){ @@ -303,8 +355,8 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -335,57 +387,17 @@ $Group_resource = "groups" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion #################################################### -$WSUPs = Get-SoftwareUpdatePolicy -Windows10 +$WSUPs = Get-SoftwareUpdatePolicy -Windows Write-Host "Software updates - Windows 10 Update Rings" -ForegroundColor Cyan Write-Host @@ -477,3 +489,4 @@ Write-Host } Write-Host + diff --git a/SoftwareUpdates/SoftwareUpdates_Import_FromJSON.ps1 b/SoftwareUpdates/SoftwareUpdates_Import_FromJSON.ps1 index 33cd709..0e507c8 100644 --- a/SoftwareUpdates/SoftwareUpdates_Import_FromJSON.ps1 +++ b/SoftwareUpdates/SoftwareUpdates_Import_FromJSON.ps1 @@ -1,4 +1,4 @@ -<# +<# .COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. @@ -6,152 +6,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Add-DeviceConfigurationPolicy(){ +function Add-DeviceConfigurationPolicy { <# .SYNOPSIS @@ -178,7 +230,7 @@ Write-Verbose "Resource: $DCP_resource" try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red @@ -188,8 +240,8 @@ Write-Verbose "Resource: $DCP_resource" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -214,7 +266,7 @@ Write-Verbose "Resource: $DCP_resource" #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -261,50 +313,10 @@ $JSON #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -343,3 +355,4 @@ $JSON_Output write-host Write-Host "Adding Software Update Policy '$DisplayName'" -ForegroundColor Yellow Add-DeviceConfigurationPolicy -JSON $JSON_Output + diff --git a/SoftwareUpdates/Windows10_SoftwareUpdates_Add.ps1 b/SoftwareUpdates/Windows10_SoftwareUpdates_Add.ps1 deleted file mode 100644 index 6dccc31..0000000 --- a/SoftwareUpdates/Windows10_SoftwareUpdates_Add.ps1 +++ /dev/null @@ -1,339 +0,0 @@ - - -<# - -.COPYRIGHT -Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -See LICENSE in the project root for license information. - -#> - -#################################################### - -function Get-AuthToken { - -<# -.SYNOPSIS -This function is used to authenticate with the Graph API REST interface -.DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name -.EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface -.NOTES -NAME: Get-AuthToken -#> - -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User - -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null -  -# Client ID used for Intune scopes - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" - -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" - -$resourceAppIdURI = "https://graph.microsoft.com" - -$authority = "https://login.microsoftonline.com/$Tenant" - - try { - - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" - - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result - - # If the accesstoken is valid then create the authentication header - - if($authResult.AccessToken){ - - # Creating header for Authorization token - - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn - } - - return $authHeader - - } - - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - - break - - } - - } - - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break - - } - -} -  -#################################################### - -Function Add-DeviceConfigurationPolicy(){ - -<# -.SYNOPSIS -This function is used to add an device configuration policy using the Graph API REST interface -.DESCRIPTION -The function connects to the Graph API Interface and adds a device configuration policy -.EXAMPLE -Add-DeviceConfigurationPolicy -JSON $JSON -Adds a device configuration policy in Intune -.NOTES -NAME: Add-DeviceConfigurationPolicy -#> - -[cmdletbinding()] - -param -( - $JSON -) - -$graphApiVersion = "Beta" -$DCP_resource = "deviceManagement/deviceConfigurations" - - try { - - if($JSON -eq "" -or $JSON -eq $null){ - - write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red - - } - - else { - - Test-JSON -JSON $JSON - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" - - } - - } - - catch { - - $ex = $_.Exception - $errorResponse = $ex.Response.GetResponseStream() - $reader = New-Object System.IO.StreamReader($errorResponse) - $reader.BaseStream.Position = 0 - $reader.DiscardBufferedData() - $responseBody = $reader.ReadToEnd(); - Write-Host "Response content:`n$responseBody" -f Red - Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" - write-host - break - - } - -} - -#################################################### - -Function Test-JSON(){ - -<# -.SYNOPSIS -This function is used to test if the JSON passed to a REST Post request is valid -.DESCRIPTION -The function tests if the JSON passed to the REST Post is valid -.EXAMPLE -Test-JSON -JSON $JSON -Test if the JSON is valid before calling the Graph REST interface -.NOTES -NAME: Test-AuthHeader -#> - -param ( - -$JSON - -) - - try { - - $TestJSON = ConvertFrom-Json $JSON -ErrorAction Stop - $validJson = $true - - } - - catch { - - $validJson = $false - $_.Exception - - } - - if (!$validJson){ - - Write-Host "Provided JSON isn't in valid JSON format" -f Red - break - - } - -} - -#################################################### - -#region Authentication - -Write-Host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - Write-Host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - Write-Host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - -} - -#endregion - -#################################################### - -$JSON = @" - - { - - "displayName":"Windows 10 - Semi-Annual (Targeted)", - "description":"Windows 10 - Semi-Annual (Targeted)", - "@odata.type":"#microsoft.graph.windowsUpdateForBusinessConfiguration", - "businessReadyUpdatesOnly":"all", - "microsoftUpdateServiceAllowed":true, - "driversExcluded":false, - "featureUpdatesDeferralPeriodInDays":0, - "qualityUpdatesDeferralPeriodInDays":0, - "automaticUpdateMode":"autoInstallAtMaintenanceTime", - "deliveryOptimizationMode":"httpOnly", - - "installationSchedule":{ - "@odata.type":"#microsoft.graph.windowsUpdateActiveHoursInstall", - "activeHoursStart":"08:00:00.0000000", - "activeHoursEnd":"17:00:00.0000000" - } - - } - -"@ - -#################################################### - -Add-DeviceConfigurationPolicy -JSON $JSON - diff --git a/SoftwareUpdates/Windows10_SoftwareUpdates_Add_Assign.ps1 b/SoftwareUpdates/Windows10_SoftwareUpdates_Add_Assign.ps1 deleted file mode 100644 index 561d8d2..0000000 --- a/SoftwareUpdates/Windows10_SoftwareUpdates_Add_Assign.ps1 +++ /dev/null @@ -1,541 +0,0 @@ - - -<# - -.COPYRIGHT -Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -See LICENSE in the project root for license information. - -#> - -#################################################### - -function Get-AuthToken { - -<# -.SYNOPSIS -This function is used to authenticate with the Graph API REST interface -.DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name -.EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface -.NOTES -NAME: Get-AuthToken -#> - -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User - -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - - } - -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null -  -# Client ID used for Intune scopes - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" - -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" - -$resourceAppIdURI = "https://graph.microsoft.com" - -$authority = "https://login.microsoftonline.com/$Tenant" - - try { - - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" - - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") - - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result - - # If the accesstoken is valid then create the authentication header - - if($authResult.AccessToken){ - - # Creating header for Authorization token - - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn - } - - return $authHeader - - } - - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - - break - - } - - } - - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break - - } - -} -  -#################################################### - -Function Add-DeviceConfigurationPolicy(){ - -<# -.SYNOPSIS -This function is used to add an device configuration policy using the Graph API REST interface -.DESCRIPTION -The function connects to the Graph API Interface and adds a device configuration policy -.EXAMPLE -Add-DeviceConfigurationPolicy -JSON $JSON -Adds a device configuration policy in Intune -.NOTES -NAME: Add-DeviceConfigurationPolicy -#> - -[cmdletbinding()] - -param -( - $JSON -) - -$graphApiVersion = "Beta" -$DCP_resource = "deviceManagement/deviceConfigurations" - - try { - - if($JSON -eq "" -or $JSON -eq $null){ - - write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red - - } - - else { - - Test-JSON -JSON $JSON - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" - - } - - } - - catch { - - $ex = $_.Exception - $errorResponse = $ex.Response.GetResponseStream() - $reader = New-Object System.IO.StreamReader($errorResponse) - $reader.BaseStream.Position = 0 - $reader.DiscardBufferedData() - $responseBody = $reader.ReadToEnd(); - Write-Host "Response content:`n$responseBody" -f Red - Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" - write-host - break - - } - -} - -#################################################### - -Function Add-DeviceConfigurationPolicyAssignment(){ - -<# -.SYNOPSIS -This function is used to add a device configuration policy assignment using the Graph API REST interface -.DESCRIPTION -The function connects to the Graph API Interface and adds a device configuration policy assignment -.EXAMPLE -Add-DeviceConfigurationPolicyAssignment -ConfigurationPolicyId $ConfigurationPolicyId -TargetGroupId $TargetGroupId -Adds a device configuration policy assignment in Intune -.NOTES -NAME: Add-DeviceConfigurationPolicyAssignment -#> - -[cmdletbinding()] - -param -( - $ConfigurationPolicyId, - $TargetGroupId -) - -$graphApiVersion = "Beta" -$Resource = "deviceManagement/deviceConfigurations/$ConfigurationPolicyId/assign" - - try { - - if(!$ConfigurationPolicyId){ - - write-host "No Configuration Policy Id specified, specify a valid Configuration Policy Id" -f Red - break - - } - - if(!$TargetGroupId){ - - write-host "No Target Group Id specified, specify a valid Target Group Id" -f Red - break - - } - - $ConfPolAssign = "$ConfigurationPolicyId" + "_" + "$TargetGroupId" - -$JSON = @" - -{ - "deviceConfigurationGroupAssignments": [ - { - "@odata.type": "#microsoft.graph.deviceConfigurationGroupAssignment", - "id": "$ConfPolAssign", - "targetGroupId": "$TargetGroupId" - } - ] -} - -"@ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" - - } - - catch { - - $ex = $_.Exception - $errorResponse = $ex.Response.GetResponseStream() - $reader = New-Object System.IO.StreamReader($errorResponse) - $reader.BaseStream.Position = 0 - $reader.DiscardBufferedData() - $responseBody = $reader.ReadToEnd(); - Write-Host "Response content:`n$responseBody" -f Red - Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" - write-host - break - - } - -} - -#################################################### - -Function Test-JSON(){ - -<# -.SYNOPSIS -This function is used to test if the JSON passed to a REST Post request is valid -.DESCRIPTION -The function tests if the JSON passed to the REST Post is valid -.EXAMPLE -Test-JSON -JSON $JSON -Test if the JSON is valid before calling the Graph REST interface -.NOTES -NAME: Test-AuthHeader -#> - -param ( - -$JSON - -) - - try { - - $TestJSON = ConvertFrom-Json $JSON -ErrorAction Stop - $validJson = $true - - } - - catch { - - $validJson = $false - $_.Exception - - } - - if (!$validJson){ - - Write-Host "Provided JSON isn't in valid JSON format" -f Red - break - - } - -} - -#################################################### - -Function Get-AADGroup(){ - -<# -.SYNOPSIS -This function is used to get AAD Groups from the Graph API REST interface -.DESCRIPTION -The function connects to the Graph API Interface and gets any Groups registered with AAD -.EXAMPLE -Get-AADGroup -Returns all users registered with Azure AD -.NOTES -NAME: Get-AADGroup -#> - -[cmdletbinding()] - -param -( - $GroupName, - $id, - [switch]$Members -) - -# Defining Variables -$graphApiVersion = "v1.0" -$Group_resource = "groups" - - try { - - if($id){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - - } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - - } - - else { - - if(!$Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - - } - - elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - - if($Group){ - - $GID = $Group.id - - $Group.displayName - write-host - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).Value - - } - - } - - } - - } - - catch { - - $ex = $_.Exception - $errorResponse = $ex.Response.GetResponseStream() - $reader = New-Object System.IO.StreamReader($errorResponse) - $reader.BaseStream.Position = 0 - $reader.DiscardBufferedData() - $responseBody = $reader.ReadToEnd(); - Write-Host "Response content:`n$responseBody" -f Red - Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" - write-host - break - - } - -} - -#################################################### - -#region Authentication - -Write-Host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - Write-Host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - Write-Host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - -} - -#endregion - -#################################################### - -# Setting application AAD Group - -$AADGroup = Read-Host -Prompt "Enter the Azure AD Group name where policies will be assigned" - -$TargetGroupId = (get-AADGroup -GroupName "$AADGroup").id - - if($TargetGroupId -eq $null -or $TargetGroupId -eq ""){ - - Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red - Write-Host - exit - - } - -Write-Host - -#################################################### - -$JSON = @" - - { - - "displayName":"Windows 10 - Semi-Annual (Targeted) - Assigned", - "description":"Windows 10 - Semi-Annual (Targeted) - Assigned", - "@odata.type":"#microsoft.graph.windowsUpdateForBusinessConfiguration", - "businessReadyUpdatesOnly":"all", - "microsoftUpdateServiceAllowed":true, - "driversExcluded":false, - "featureUpdatesDeferralPeriodInDays":0, - "qualityUpdatesDeferralPeriodInDays":0, - "automaticUpdateMode":"autoInstallAtMaintenanceTime", - "deliveryOptimizationMode":"httpOnly", - - "installationSchedule":{ - "@odata.type":"#microsoft.graph.windowsUpdateActiveHoursInstall", - "activeHoursStart":"08:00:00.0000000", - "activeHoursEnd":"17:00:00.0000000" - } - - } - -"@ - -#################################################### - -$CreateResult = Add-DeviceConfigurationPolicy -JSON $JSON - -Write-Host "Software Update Policy created as" $CreateResult.id -write-host -write-host "Assigning Software Update Policy to AAD Group '$AADGroup'" -f Cyan - -$Assign = Add-DeviceConfigurationPolicyAssignment -ConfigurationPolicyId $CreateResult.id -TargetGroupId $TargetGroupId - -Write-Host "Assigned '$AADGroup' to $($CreateResult.displayName)/$($CreateResult.id)" -Write-Host - diff --git a/SoftwareUpdates/Windows_SoftwareUpdates_Add.ps1 b/SoftwareUpdates/Windows_SoftwareUpdates_Add.ps1 new file mode 100644 index 0000000..1a1a82d --- /dev/null +++ b/SoftwareUpdates/Windows_SoftwareUpdates_Add.ps1 @@ -0,0 +1,356 @@ + + +<# + +.COPYRIGHT +Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +See LICENSE in the project root for license information. + +#> + + +function Connect-GraphAPI { +<# +.SYNOPSIS +Connects to Microsoft Graph API with appropriate scopes for Intune operations +.DESCRIPTION +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment +.NOTES +Requires Microsoft.Graph.Authentication module +#> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) + + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } + + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } + + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } + } + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false + } +} + +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, + + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', + + [Parameter(Mandatory = $false)] + [object]$Body = $null, + + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) + + try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } + + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } + + $results = @() + $nextLink = $Uri + + do { + Write-Verbose "Making request to: $nextLink" + + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } + + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } + + $response = Invoke-MgGraphRequest @requestParams + + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null + } + + } while ($nextLink) + + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" + } + else { + Write-Error "Graph API request failed: $errorMessage" + } + throw + } +} + +#################################################### + +#################################################### + + +#################################################### + +function Add-DeviceConfigurationPolicy { + +<# +.SYNOPSIS +This function is used to add an device configuration policy using the Graph API REST interface +.DESCRIPTION +The function connects to the Graph API Interface and adds a device configuration policy +.EXAMPLE +Add-DeviceConfigurationPolicy -JSON $JSON +Adds a device configuration policy in Intune +.NOTES +NAME: Add-DeviceConfigurationPolicy +#> + +[cmdletbinding()] + +param +( + $JSON +) + +$graphApiVersion = "Beta" +$DCP_resource = "deviceManagement/deviceConfigurations" + + try { + + if("" -eq $JSON -or $null -eq $JSON){ + + write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red + + } + + else { + + Test-JSON -JSON $JSON + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON + + } + + } + + catch { + + $ex = $_.Exception + $errorResponse = $ex.Response.GetResponseStream() + $reader = New-Object System.IO.StreamReader($errorResponse) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $responseBody = $reader.ReadToEnd(); + Write-Host "Response content:`n$responseBody" -f Red + Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" + write-host + break + + } + +} + +#################################################### + +function Test-JSON { + +<# +.SYNOPSIS +This function is used to test if the JSON passed to a REST Post request is valid +.DESCRIPTION +The function tests if the JSON passed to the REST Post is valid +.EXAMPLE +Test-JSON -JSON $JSON +Test if the JSON is valid before calling the Graph REST interface +.NOTES +NAME: Test-AuthHeader +#> + +param ( + +$JSON + +) + + try { + + $TestJSON = ConvertFrom-Json $JSON -ErrorAction Stop + $validJson = $true + + } + + catch { + + $validJson = $false + $_.Exception + + } + + if (!$validJson){ + + Write-Host "Provided JSON isn't in valid JSON format" -f Red + break + + } + +} + +#################################################### + +#region Authentication + +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 +} + +#endregion + +#################################################### + +$JSON = @" + + { + + "displayName":"Windows 10 - Semi-Annual (Targeted)", + "description":"Windows 10 - Semi-Annual (Targeted)", + "@odata.type":"#microsoft.graph.windowsUpdateForBusinessConfiguration", + "businessReadyUpdatesOnly":"all", + "microsoftUpdateServiceAllowed":true, + "driversExcluded":false, + "featureUpdatesDeferralPeriodInDays":0, + "qualityUpdatesDeferralPeriodInDays":0, + "automaticUpdateMode":"autoInstallAtMaintenanceTime", + "deliveryOptimizationMode":"httpOnly", + + "installationSchedule":{ + "@odata.type":"#microsoft.graph.windowsUpdateActiveHoursInstall", + "activeHoursStart":"08:00:00.0000000", + "activeHoursEnd":"17:00:00.0000000" + } + + } + +"@ + +#################################################### + +Add-DeviceConfigurationPolicy -JSON $JSON + + diff --git a/SoftwareUpdates/Windows_SoftwareUpdates_Add_Assign.ps1 b/SoftwareUpdates/Windows_SoftwareUpdates_Add_Assign.ps1 new file mode 100644 index 0000000..d8529f5 --- /dev/null +++ b/SoftwareUpdates/Windows_SoftwareUpdates_Add_Assign.ps1 @@ -0,0 +1,558 @@ + + +<# + +.COPYRIGHT +Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +See LICENSE in the project root for license information. + +#> + + +function Connect-GraphAPI { +<# +.SYNOPSIS +Connects to Microsoft Graph API with appropriate scopes for Intune operations +.DESCRIPTION +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment +.NOTES +Requires Microsoft.Graph.Authentication module +#> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) + + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } + + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } + + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } + } + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false + } +} + +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, + + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', + + [Parameter(Mandatory = $false)] + [object]$Body = $null, + + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) + + try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } + + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } + + $results = @() + $nextLink = $Uri + + do { + Write-Verbose "Making request to: $nextLink" + + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } + + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } + + $response = Invoke-MgGraphRequest @requestParams + + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null + } + + } while ($nextLink) + + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" + } + else { + Write-Error "Graph API request failed: $errorMessage" + } + throw + } +} + +#################################################### + +#################################################### + + +#################################################### + +function Add-DeviceConfigurationPolicy { + +<# +.SYNOPSIS +This function is used to add an device configuration policy using the Graph API REST interface +.DESCRIPTION +The function connects to the Graph API Interface and adds a device configuration policy +.EXAMPLE +Add-DeviceConfigurationPolicy -JSON $JSON +Adds a device configuration policy in Intune +.NOTES +NAME: Add-DeviceConfigurationPolicy +#> + +[cmdletbinding()] + +param +( + $JSON +) + +$graphApiVersion = "Beta" +$DCP_resource = "deviceManagement/deviceConfigurations" + + try { + + if("" -eq $JSON -or $null -eq $JSON){ + + write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red + + } + + else { + + Test-JSON -JSON $JSON + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON + + } + + } + + catch { + + $ex = $_.Exception + $errorResponse = $ex.Response.GetResponseStream() + $reader = New-Object System.IO.StreamReader($errorResponse) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $responseBody = $reader.ReadToEnd(); + Write-Host "Response content:`n$responseBody" -f Red + Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" + write-host + break + + } + +} + +#################################################### + +function Add-DeviceConfigurationPolicyAssignment { + +<# +.SYNOPSIS +This function is used to add a device configuration policy assignment using the Graph API REST interface +.DESCRIPTION +The function connects to the Graph API Interface and adds a device configuration policy assignment +.EXAMPLE +Add-DeviceConfigurationPolicyAssignment -ConfigurationPolicyId $ConfigurationPolicyId -TargetGroupId $TargetGroupId +Adds a device configuration policy assignment in Intune +.NOTES +NAME: Add-DeviceConfigurationPolicyAssignment +#> + +[cmdletbinding()] + +param +( + $ConfigurationPolicyId, + $TargetGroupId +) + +$graphApiVersion = "Beta" +$Resource = "deviceManagement/deviceConfigurations/$ConfigurationPolicyId/assign" + + try { + + if(!$ConfigurationPolicyId){ + + write-host "No Configuration Policy Id specified, specify a valid Configuration Policy Id" -f Red + break + + } + + if(!$TargetGroupId){ + + write-host "No Target Group Id specified, specify a valid Target Group Id" -f Red + break + + } + + $ConfPolAssign = "$ConfigurationPolicyId" + "_" + "$TargetGroupId" + +$JSON = @" + +{ + "deviceConfigurationGroupAssignments": [ + { + "@odata.type": "#microsoft.graph.deviceConfigurationGroupAssignment", + "id": "$ConfPolAssign", + "targetGroupId": "$TargetGroupId" + } + ] +} + +"@ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON + + } + + catch { + + $ex = $_.Exception + $errorResponse = $ex.Response.GetResponseStream() + $reader = New-Object System.IO.StreamReader($errorResponse) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $responseBody = $reader.ReadToEnd(); + Write-Host "Response content:`n$responseBody" -f Red + Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" + write-host + break + + } + +} + +#################################################### + +function Test-JSON { + +<# +.SYNOPSIS +This function is used to test if the JSON passed to a REST Post request is valid +.DESCRIPTION +The function tests if the JSON passed to the REST Post is valid +.EXAMPLE +Test-JSON -JSON $JSON +Test if the JSON is valid before calling the Graph REST interface +.NOTES +NAME: Test-AuthHeader +#> + +param ( + +$JSON + +) + + try { + + $TestJSON = ConvertFrom-Json $JSON -ErrorAction Stop + $validJson = $true + + } + + catch { + + $validJson = $false + $_.Exception + + } + + if (!$validJson){ + + Write-Host "Provided JSON isn't in valid JSON format" -f Red + break + + } + +} + +#################################################### + +function Get-AADGroup { + +<# +.SYNOPSIS +This function is used to get AAD Groups from the Graph API REST interface +.DESCRIPTION +The function connects to the Graph API Interface and gets any Groups registered with AAD +.EXAMPLE +Get-AADGroup +Returns all users registered with Azure AD +.NOTES +NAME: Get-AADGroup +#> + +[cmdletbinding()] + +param +( + $GroupName, + $id, + [switch]$Members +) + +# Defining Variables +$graphApiVersion = "v1.0" +$Group_resource = "groups" + + try { + + if($id){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + + } + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + + } + + else { + + if(!$Members){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + + } + + elseif($Members){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + + if($Group){ + + $GID = $Group.id + + $Group.displayName + write-host + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + + } + + } + + } + + } + + catch { + + $ex = $_.Exception + $errorResponse = $ex.Response.GetResponseStream() + $reader = New-Object System.IO.StreamReader($errorResponse) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $responseBody = $reader.ReadToEnd(); + Write-Host "Response content:`n$responseBody" -f Red + Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" + write-host + break + + } + +} + +#################################################### + +#region Authentication + +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 +} + +#endregion + +#################################################### + +# Setting application AAD Group + +$AADGroup = Read-Host -Prompt "Enter the Azure AD Group name where policies will be assigned" + +$TargetGroupId = (get-AADGroup -GroupName "$AADGroup").id + + if($null -eq $TargetGroupId -or "" -eq $TargetGroupId){ + + Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red + Write-Host + exit + + } + +Write-Host + +#################################################### + +$JSON = @" + + { + + "displayName":"Windows 10 - Semi-Annual (Targeted) - Assigned", + "description":"Windows 10 - Semi-Annual (Targeted) - Assigned", + "@odata.type":"#microsoft.graph.windowsUpdateForBusinessConfiguration", + "businessReadyUpdatesOnly":"all", + "microsoftUpdateServiceAllowed":true, + "driversExcluded":false, + "featureUpdatesDeferralPeriodInDays":0, + "qualityUpdatesDeferralPeriodInDays":0, + "automaticUpdateMode":"autoInstallAtMaintenanceTime", + "deliveryOptimizationMode":"httpOnly", + + "installationSchedule":{ + "@odata.type":"#microsoft.graph.windowsUpdateActiveHoursInstall", + "activeHoursStart":"08:00:00.0000000", + "activeHoursEnd":"17:00:00.0000000" + } + + } + +"@ + +#################################################### + +$CreateResult = Add-DeviceConfigurationPolicy -JSON $JSON + +Write-Host "Software Update Policy created as" $CreateResult.id +write-host +write-host "Assigning Software Update Policy to AAD Group '$AADGroup'" -f Cyan + +$Assign = Add-DeviceConfigurationPolicyAssignment -ConfigurationPolicyId $CreateResult.id -TargetGroupId $TargetGroupId + +Write-Host "Assigned '$AADGroup' to $($CreateResult.displayName)/$($CreateResult.id)" +Write-Host + + diff --git a/TermsAndConditions/TermsAndConditions_Add.ps1 b/TermsAndConditions/TermsAndConditions_Add.ps1 index 656708c..2c959ae 100644 --- a/TermsAndConditions/TermsAndConditions_Add.ps1 +++ b/TermsAndConditions/TermsAndConditions_Add.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementServiceConfig.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -197,7 +249,7 @@ $JSON #################################################### -Function Add-TermsAndConditions(){ +function Add-TermsAndConditions { <# .SYNOPSIS @@ -223,7 +275,7 @@ $Resource = "deviceManagement/termsAndConditions" try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red @@ -233,8 +285,8 @@ $Resource = "deviceManagement/termsAndConditions" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -262,50 +314,10 @@ $Resource = "deviceManagement/termsAndConditions" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -328,3 +340,4 @@ $JSON = @" "@ Add-TermsAndConditions -JSON $JSON + diff --git a/TermsAndConditions/TermsAndConditions_Add_Assign.ps1 b/TermsAndConditions/TermsAndConditions_Add_Assign.ps1 index a9098fe..5bbbd81 100644 --- a/TermsAndConditions/TermsAndConditions_Add_Assign.ps1 +++ b/TermsAndConditions/TermsAndConditions_Add_Assign.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementServiceConfig.ReadWrite.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Test-JSON(){ +function Test-JSON { <# .SYNOPSIS @@ -187,7 +239,7 @@ $JSON } if (!$validJson){ - + Write-Host "Provided JSON isn't in valid JSON format" -f Red break @@ -197,7 +249,7 @@ $JSON #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -223,37 +275,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ $GID = $Group.id @@ -261,13 +313,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } } @@ -291,7 +343,7 @@ $Group_resource = "groups" #################################################### -Function Add-TermsAndConditions(){ +function Add-TermsAndConditions { <# .SYNOPSIS @@ -314,10 +366,10 @@ param $graphApiVersion = "Beta" $Resource = "deviceManagement/termsAndConditions" - + try { - if($JSON -eq "" -or $JSON -eq $null){ + if("" -eq $JSON -or $null -eq $JSON){ write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red @@ -327,13 +379,13 @@ $Resource = "deviceManagement/termsAndConditions" Test-JSON -JSON $JSON - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } } - + catch { Write-Host @@ -354,7 +406,7 @@ $Resource = "deviceManagement/termsAndConditions" #################################################### -Function Assign-TermsAndConditions(){ +function Assign-TermsAndConditions { <# .SYNOPSIS @@ -365,7 +417,7 @@ The function connects to the Graph API Interface and assigns terms and condition Assign-TermsAndConditions -id $id -TargetGroupId .NOTES NAME: Assign-TermsAndConditions -#> +#> [cmdletbinding()] @@ -406,8 +458,8 @@ $JSON = @" "@ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method POST -Body $JSON } @@ -434,50 +486,10 @@ $JSON = @" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -506,7 +518,7 @@ $AADGroup = Read-Host -Prompt "Enter the Azure AD Group name where terms and con $TargetGroupId = (get-AADGroup -GroupName "$AADGroup").id - if($TargetGroupId -eq $null -or $TargetGroupId -eq ""){ + if($null -eq $TargetGroupId -or "" -eq $TargetGroupId){ Write-Host "AAD Group - '$AADGroup' doesn't exist, please specify a valid AAD Group..." -ForegroundColor Red Write-Host @@ -528,3 +540,4 @@ write-host "Assigning Terms and Conditions to AAD Group '$AADGroup'" -f Yellow $Assign_Policy = Assign-TermsAndConditions -id $CreateResult.id -TargetGroupId $TargetGroupId Write-Host "Assigned '$AADGroup' to $($CreateResult.displayName)/$($CreateResult.id)" Write-Host + diff --git a/TermsAndConditions/TermsAndConditions_Get.ps1 b/TermsAndConditions/TermsAndConditions_Get.ps1 index c7430d6..3c63089 100644 --- a/TermsAndConditions/TermsAndConditions_Get.ps1 +++ b/TermsAndConditions/TermsAndConditions_Get.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementServiceConfig.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-TermsAndConditions(){ +function Get-TermsAndConditions { <# .SYNOPSIS @@ -180,15 +231,15 @@ $resource = "deviceManagement/termsAndConditions" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -215,50 +266,10 @@ $resource = "deviceManagement/termsAndConditions" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -266,3 +277,4 @@ $global:authToken = Get-AuthToken -User $User #################################################### Get-TermsAndConditions + diff --git a/TermsAndConditions/TermsAndConditions_Remove.ps1 b/TermsAndConditions/TermsAndConditions_Remove.ps1 index f09331f..290bbcd 100644 --- a/TermsAndConditions/TermsAndConditions_Remove.ps1 +++ b/TermsAndConditions/TermsAndConditions_Remove.ps1 @@ -7,152 +7,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.ReadWrite.All", + "DeviceManagementApps.ReadWrite.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-TermsAndConditions(){ +function Get-TermsAndConditions { <# .SYNOPSIS @@ -180,15 +232,15 @@ $resource = "deviceManagement/termsAndConditions" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -213,7 +265,7 @@ $resource = "deviceManagement/termsAndConditions" #################################################### -Function Remove-TermsAndCondition(){ +function Remove-TermsAndCondition { <# .SYNOPSIS @@ -239,7 +291,7 @@ $Resource = "deviceManagement/termsAndConditions/$termsAndConditionId" try { - if($termsAndConditionId -eq "" -or $termsAndConditionId -eq $null){ + if("" -eq $termsAndConditionId -or $null -eq $termsAndConditionId){ Write-Host "termsAndConditionId hasn't been passed as a paramater to the function..." -ForegroundColor Red write-host "Please specify a valid termsAndConditionsId..." -ForegroundColor Red @@ -249,7 +301,7 @@ $Resource = "deviceManagement/termsAndConditions/$termsAndConditionId" else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Invoke-RestMethod -Uri $uri -Headers $authToken -Method Delete } @@ -277,50 +329,10 @@ $Resource = "deviceManagement/termsAndConditions/$termsAndConditionId" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -347,3 +359,4 @@ $TC = Get-TermsAndConditions -Name "Customer" } } + diff --git a/TermsAndConditions/TermsAndConditions_export.ps1 b/TermsAndConditions/TermsAndConditions_export.ps1 index 99ee49b..d5c0039 100644 --- a/TermsAndConditions/TermsAndConditions_export.ps1 +++ b/TermsAndConditions/TermsAndConditions_export.ps1 @@ -1,155 +1,206 @@ <# -.COPYRIGHT +.COPYRIGHT Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. - -#> -#################################################### -function Get-AuthToken { +#> +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementServiceConfig.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-TermsAndConditions(){ +function Get-TermsAndConditions { <# .SYNOPSIS @@ -177,15 +228,15 @@ $resource = "deviceManagement/termsAndConditions" if($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -210,8 +261,8 @@ $resource = "deviceManagement/termsAndConditions" #################################################### -Function Export-JSONData(){ - +function Export-JSONData { + <# .SYNOPSIS This function is used to export JSON data returned from Graph @@ -223,132 +274,92 @@ Export the JSON inputted on the function .NOTES NAME: Export-JSONData #> - + param ( - + $JSON, $ExportPath - + ) - + try { - - if ($JSON -eq "" -or $JSON -eq $null) + + if ("" -eq $JSON -or $null -eq $JSON) { - + write-host "No JSON specified, please specify valid JSON..." -f Red - + } - + elseif (!$ExportPath) { - + write-host "No export path parameter set, please provide a path to export the file" -f Red - + } - + elseif (!(Test-Path $ExportPath)) { - + write-host "$ExportPath doesn't exist, can't export JSON Data" -f Red - + } - + else { - + $JSON1 = ConvertTo-Json $JSON - + $JSON_Convert = $JSON1 | ConvertFrom-Json - + $displayName = $JSON_Convert.displayName # Updating display name to follow file naming conventions - https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx $DisplayName = $DisplayName -replace '\<|\>|:|"|/|\\|\||\?|\*', "_" - + $Properties = ($JSON_Convert | Get-Member | ? { $_.MemberType -eq "NoteProperty" }).Name - + $FileName_CSV = "$DisplayName" + "_" + $(get-date -f dd-MM-yyyy-H-mm-ss) + ".csv" $FileName_JSON = "$DisplayName" + "_" + $(get-date -f dd-MM-yyyy-H-mm-ss) + ".json" - + $Object = New-Object System.Object - + foreach ($Property in $Properties) { - + $Object | Add-Member -MemberType NoteProperty -Name $Property -Value $JSON_Convert.$Property - + } - + write-host "Export Path:" "$ExportPath" - + $Object | Export-Csv -LiteralPath "$ExportPath\$FileName_CSV" -Delimiter "," -NoTypeInformation -Append $JSON1 | Set-Content -LiteralPath "$ExportPath\$FileName_JSON" write-host "CSV created in $ExportPath\$FileName_CSV..." -f cyan write-host "JSON created in $ExportPath\$FileName_JSON..." -f cyan - + } - + } - + catch { - - $_.Exception - - } - -} - -#################################################### -#region Authentication - -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } + $_.Exception - $global:authToken = Get-AuthToken -User $User + } - } } -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } +#################################################### -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User +#region Authentication +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -361,29 +372,29 @@ $ExportPath = Read-Host -Prompt "Please specify a path to export the policy data if (!(Test-Path "$ExportPath")) { - + Write-Host Write-Host "Path '$ExportPath' doesn't exist, do you want to create this directory? Y or N?" -ForegroundColor Yellow - + $Confirm = read-host - + if ($Confirm -eq "y" -or $Confirm -eq "Y") { - + new-item -ItemType Directory -Path "$ExportPath" | Out-Null Write-Host - + } - + else { - + Write-Host "Creation of directory path was cancelled..." -ForegroundColor Red Write-Host break - + } - + } Write-Host @@ -394,9 +405,10 @@ $TCs = Get-TermsAndConditions foreach ($TC in $TCs) { - + write-host "Terms and Conditions Policy:"$TSc.displayName -f Yellow Export-JSONData -JSON $TC -ExportPath "$ExportPath" Write-Host - + } + diff --git a/Updating App Registration b/Updating App Registration.md similarity index 51% rename from Updating App Registration rename to Updating App Registration.md index 9ae5d3e..679ce37 100644 --- a/Updating App Registration +++ b/Updating App Registration.md @@ -1,8 +1,8 @@ -If you use "**Connect-msgraph**" or use the ClientID “**d1ddf0e4-d672-4dae-b554-9d5bdfd93547”** in your PowerShell scripts, you need to update your ClientID. +If you use legacy authentication methods or the deprecated ClientID "**d1ddf0e4-d672-4dae-b554-9d5bdfd93547"** in your PowerShell scripts, you need to update to use the modern Microsoft.Graph.Authentication module with **Connect-MgGraph**. if your using "**Connect-msgraph**" or use the ClientID “**d1ddf0e4-d672-4dae-b554-9d5bdfd93547”** in your PowerShell scripts, you need to update your ClientID. -Option 1: Migrate your old application (Microsoft Intune PowerShell) to your own application to access Graph. [Update to Microsoft Intune PowerShell example script repository on GitHub - Microsoft Community Hub](https://techcommunity.microsoft.com/t5/intune-customer-success/update-to-microsoft-intune-powershell-example-script-repository/ba-p/3842452) +Option 1: Migrate your existing application to use the SDK's _Microsoft.Graph.Authentication_ module. [Update to Microsoft Graph PowerShell SDK - Microsoft Graph | Microsoft Learn](https://learn.microsoft.com/en-us/powershell/microsoftgraph/installation?view=graph-powershell-1.0) -Option 2: Register app in Entra ID and give Intune Graph permission in it: +Option 2: Register a new app in Entra ID and configure it for Microsoft Graph access: [Quickstart: Register an app in the Microsoft identity platform - Microsoft identity platform | Microsoft Learn](https://learn.microsoft.com/en-au/entra/identity-platform/quickstart-register-app) @@ -36,31 +36,71 @@ Your App registration is done. To modify your PowerShell scripts: -**If you are using the legacy Intune PowerShell module (MsGraph)** +**Using the Microsoft.Graph.Authentication Module (Recommended)** -Add the following before the line in your script: "connect-MsGraph": +Install the Microsoft Graph PowerShell SDK if not already installed: +```powershell +Install-Module Microsoft.Graph.Authentication -Scope CurrentUser +``` -**_Update-MSGraphEnvironment -AppId {replace here with your app id}_** +**For Delegated Authentication (Interactive Login):** +```powershell +# Connect interactively +Connect-MgGraph -Scopes "DeviceManagementConfiguration.ReadWrite.All", "DeviceManagementApps.ReadWrite.All" -Sample script for delegated access. +# Verify connection +Get-MgContext +``` -**_Update-MSGraphEnvironment -AppId {replace here with your app ID}_** +**For Application Authentication (Client Credentials):** +```powershell +# Define the Tenant ID, Client ID, and Client Secret +$TenantId = "your-tenant-id" +$ClientId = "your-client-id" +$ClientSecret = "your-client-secret" -**_$adminUPN = Read-Host -Prompt "Enter UPN" -$adminPwd = Read-Host -AsSecureString -Prompt "Enter password for $adminUPN"_** +# Convert the Client Secret to a Secure String +$SecureClientSecret = ConvertTo-SecureString -String $ClientSecret -AsPlainText -Force -$credential = New-Object System.Management.Automation.PsCredential($adminUPN, $adminPwd) +# Create a PSCredential object +$ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $ClientId, $SecureClientSecret -Connect-MSGraph -PSCredential $credential +# Connect to Microsoft Graph using the Tenant ID and Client Secret Credential +Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $ClientSecretCredential -If we use MSAL to access, we can replace clientId with our new application ID. +# Verify connection +Get-MgContext +``` -\[System.Reflection.Assembly\]::LoadFrom($adal) | Out-Null +**For Certificate-Based Authentication:** +```powershell +# Connect using certificate thumbprint +Connect-MgGraph -ClientId "your-client-id" -TenantId "your-tenant-id" -CertificateThumbprint "your-cert-thumbprint" +``` -\[System.Reflection.Assembly\]::LoadFrom($adalforms) | Out-Null +**Legacy Methods (Deprecated - Update Required)** -$clientId = "<replace with your clientID>" +~~The following methods are deprecated and should be updated to use Connect-MgGraph:~~ +**Legacy MSGraph Module (Deprecated):** +```powershell +# OLD METHOD - DO NOT USE +Update-MSGraphEnvironment -AppId {your app id} +$adminUPN = Read-Host -Prompt "Enter UPN" +$adminPwd = Read-Host -AsSecureString -Prompt "Enter password for $adminUPN" +$credential = New-Object System.Management.Automation.PsCredential($adminUPN, $adminPwd) +Connect-MSGraph -PSCredential $credential +``` + +**Legacy MSAL Method (Deprecated):** +```powershell +# OLD METHOD - DO NOT USE +[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null +[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null +$clientId = "" $redirectUri = "urn:ietf:wg:oauth:2.0:oob" +$resourceAppIdURI = "https://graph.microsoft.com" +``` -$resourceAppIdURI = "" +**Migration Path:** +Replace all legacy authentication methods with the modern `Connect-MgGraph` examples shown above. The Microsoft.Graph.Authentication module provides better security, multi-cloud support, and is actively maintained. diff --git a/UserPolicyReport/User_MAM_Report_Get.ps1 b/UserPolicyReport/User_MAM_Report_Get.ps1 index df0b810..1f9586b 100644 --- a/UserPolicyReport/User_MAM_Report_Get.ps1 +++ b/UserPolicyReport/User_MAM_Report_Get.ps1 @@ -6,152 +6,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI +Connects to Microsoft Graph with default scopes +.EXAMPLE +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "User.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-AADUser(){ +function Get-AADUser { <# .SYNOPSIS @@ -179,36 +230,36 @@ param # Defining Variables $graphApiVersion = "v1.0" $User_resource = "users" - + try { - - if($userPrincipalName -eq "" -or $userPrincipalName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + if("" -eq $userPrincipalName -or $null -eq $userPrincipalName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - - if($Property -eq "" -or $Property -eq $null){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName" + if("" -eq $Property -or $null -eq $Property){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName" Write-Verbose $uri - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } catch { @@ -230,7 +281,7 @@ $User_resource = "users" #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -256,37 +307,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ $GID = $Group.id @@ -294,13 +345,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } } @@ -324,7 +375,7 @@ $Group_resource = "groups" #################################################### -Function Get-ManagedAppPolicy(){ +function Get-ManagedAppPolicy { <# .SYNOPSIS @@ -349,15 +400,15 @@ $Resource = "deviceAppManagement/managedAppPolicies" try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("ManagedAppProtection") -or ($_.'@odata.type').contains("InformationProtectionPolicy") } - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("ManagedAppProtection") -or ($_.'@odata.type').contains("InformationProtectionPolicy") } + } - - + + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -368,14 +419,14 @@ $Resource = "deviceAppManagement/managedAppPolicies" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### -Function Get-ManagedAppProtection(){ +function Get-ManagedAppProtection { <# .SYNOPSIS @@ -398,64 +449,64 @@ NAME: Get-ManagedAppProtection param ( $id, - $OS + $OS ) $graphApiVersion = "Beta" try { - - if($id -eq "" -or $id -eq $null){ - + + if("" -eq $id -or $null -eq $id){ + write-host "No Managed App Policy id specified, please provide a policy id..." -f Red break - + } - + else { - - if($OS -eq "" -or $OS -eq $null){ - + + if("" -eq $OS -or $null -eq $OS){ + write-host "No OS parameter specified, please provide an OS. Supported values are Android,iOS, and Windows..." -f Red Write-Host break - + } - + elseif($OS -eq "Android"){ - + $Resource = "deviceAppManagement/androidManagedAppProtections('$id')/?`$expand=deploymentSummary,apps,assignments" - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET + } - + elseif($OS -eq "iOS"){ - + $Resource = "deviceAppManagement/iosManagedAppProtections('$id')/?`$expand=deploymentSummary,apps,assignments" - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET + } elseif($OS -eq "Windows"){ - + $Resource = "deviceAppManagement/windowsInformationProtectionPolicies('$id')?`$expand=protectedAppLockerFiles,exemptAppLockerFiles,assignments" - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET + } - + } - + } catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -466,14 +517,14 @@ $graphApiVersion = "Beta" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } } #################################################### -Function Get-ApplicationAssignment(){ +function Get-ApplicationAssignment { <# .SYNOPSIS @@ -508,8 +559,8 @@ $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assignments" else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -534,8 +585,8 @@ $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assignments" #################################################### -Function Get-MobileAppConfigurations(){ - +function Get-MobileAppConfigurations { + <# .SYNOPSIS This function is used to get all Mobile App Configuration Policies (managed device) using the Graph API REST interface @@ -549,21 +600,21 @@ NAME: Get-MobileAppConfigurations #> [cmdletbinding()] - + $graphApiVersion = "Beta" $Resource = "deviceAppManagement/mobileAppConfigurations?`$expand=assignments" - + try { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Method Get -Headers $authToken).value + Invoke-IntuneRestMethod -Uri $uri -Method GET } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -574,15 +625,15 @@ $Resource = "deviceAppManagement/mobileAppConfigurations?`$expand=assignments" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### -Function Get-TargetedManagedAppConfigurations(){ - +function Get-TargetedManagedAppConfigurations { + <# .SYNOPSIS This function is used to get all Targeted Managed App Configuration Policies using the Graph API REST interface @@ -602,31 +653,31 @@ param [Parameter(Mandatory=$false)] $PolicyId ) - + $graphApiVersion = "Beta" - + try { if($PolicyId){ $Resource = "deviceAppManagement/targetedManagedAppConfigurations('$PolicyId')?`$expand=apps,assignments" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Method Get -Headers $authToken) + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET) } else { $Resource = "deviceAppManagement/targetedManagedAppConfigurations" - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Method Get -Headers $authToken).value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + Invoke-IntuneRestMethod -Uri $uri -Method GET } } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -637,14 +688,14 @@ $graphApiVersion = "Beta" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### -Function Get-IntuneApplication(){ +function Get-IntuneApplication { <# .SYNOPSIS @@ -673,23 +724,23 @@ $Resource = "deviceAppManagement/mobileApps" if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)/$id" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)/$id" + (Invoke-IntuneRestMethod -Uri $uri -Method GET) } - - + + elseif($Name){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") -and (!($_.'@odata.type').Contains("managed")) } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'displayName').contains("$Name") -and (!($_.'@odata.type').Contains("managed")) } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { (!($_.'@odata.type').Contains("managed")) } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { (!($_.'@odata.type').Contains("managed")) } } @@ -715,7 +766,7 @@ $Resource = "deviceAppManagement/mobileApps" #################################################### -Function Get-IntuneMAMApplication(){ +function Get-IntuneMAMApplication { <# .SYNOPSIS @@ -744,22 +795,22 @@ $Resource = "deviceAppManagement/mobileApps" if($packageid){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | ? { ($_.'@odata.type').Contains("managed") -and ($_.'appAvailability' -eq "Global") -and ($_.'packageid' -eq "$packageid") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | ? { ($_.'@odata.type').Contains("managed") -and ($_.'appAvailability' -eq "Global") -and ($_.'packageid' -eq "$packageid") } } elseif($bundleid){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | ? { ($_.'@odata.type').Contains("managed") -and ($_.'appAvailability' -eq "Global") -and ($_.'bundleid' -eq "$bundleid") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | ? { ($_.'@odata.type').Contains("managed") -and ($_.'appAvailability' -eq "Global") -and ($_.'bundleid' -eq "$bundleid") } } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | ? { ($_.'@odata.type').Contains("managed") -and ($_.'appAvailability' -eq "Global") } + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | ? { ($_.'@odata.type').Contains("managed") -and ($_.'appAvailability' -eq "Global") } } @@ -786,50 +837,10 @@ $Resource = "deviceAppManagement/mobileApps" #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -845,7 +856,7 @@ Write-Host write-host "Enter the UPN:" -f Yellow $UPN = Read-Host -if($UPN -eq $null -or $UPN -eq ""){ +if($null -eq $UPN -or "" -eq $UPN){ write-host "User Principal Name is Null..." -ForegroundColor Red Write-Host "Script can't continue..." -ForegroundColor Red @@ -879,14 +890,14 @@ $OSChoicesCount = "2" $menu = @{} - for ($i=1;$i -le $OSChoices.count; $i++) - { Write-Host "$i. $($OSChoices[$i-1])" + for ($i=1;$i -le $OSChoices.count; $i++) + { Write-Host "$i. $($OSChoices[$i-1])" $menu.Add($i,($OSChoices[$i-1]))} Write-Host $ans = Read-Host 'Choose an OS (numerical value)' - if($ans -eq "" -or $ans -eq $null){ + if("" -eq $ans -or $null -eq $ans){ Write-Host "OS choice can't be null, please specify a valid OS..." -ForegroundColor Red Write-Host @@ -950,17 +961,17 @@ $AssignmentCount = 0 foreach($ManagedAppPolicy in $ManagedAppPolicies){ # If Android Managed App Policy - + if($ManagedAppPolicy.'@odata.type' -eq "#microsoft.graph.androidManagedAppProtection"){ $AndroidManagedAppProtection = Get-ManagedAppProtection -id $ManagedAppPolicy.id -OS "Android" - + $MAMApps = $AndroidManagedAppProtection.apps $AndroidAssignments = ($AndroidManagedAppProtection | select assignments).assignments - + if($AndroidAssignments){ - + foreach($Group in $AndroidAssignments.target){ if($AADGroups.id -contains $Group.groupId){ @@ -986,17 +997,17 @@ $AssignmentCount = 0 write-host (get-aadgroup -id $GroupID).displayname if($GroupTargetType -eq "exclusionGroupAssignmentTarget"){ - + Write-Host "Group Target: " -NoNewline Write-Host "Excluded" -ForegroundColor Red - + } elseif($GroupTargetType -eq "GroupAssignmentTarget"){ - + Write-Host "Group Target: " -NoNewline Write-Host "Included" -ForegroundColor Green - + } Write-Host @@ -1024,29 +1035,29 @@ $AssignmentCount = 0 Write-Host write-host "-------------------------------------------------------------------" write-host - + } - + } } - - } + + } # If iOS Managed App Policy - + elseif($ManagedAppPolicy.'@odata.type' -eq "#microsoft.graph.iosManagedAppProtection"){ - + $iOSManagedAppProtection = Get-ManagedAppProtection -id $ManagedAppPolicy.id -OS "iOS" $MAMApps = $iOSManagedAppProtection.apps - + $iOSAssignments = ($iOSManagedAppProtection | select assignments).assignments - + if($iOSAssignments){ - + foreach($Group in $iOSAssignments.target){ - + if($AADGroups.id -contains $Group.groupId){ $AssignmentCount++ @@ -1070,17 +1081,17 @@ $AssignmentCount = 0 write-host (get-aadgroup -id $GroupID).displayname if($GroupTargetType -eq "exclusionGroupAssignmentTarget"){ - + Write-Host "Group Target: " -NoNewline Write-Host "Excluded" -ForegroundColor Red - + } elseif($GroupTargetType -eq "GroupAssignmentTarget"){ - + Write-Host "Group Target: " -NoNewline Write-Host "Included" -ForegroundColor Green - + } Write-Host @@ -1114,7 +1125,7 @@ $AssignmentCount = 0 } } - + } } @@ -1183,17 +1194,17 @@ $TMACCount = @($TargetedManagedAppConfigurations).count write-host (get-aadgroup -id $GroupID).displayname if($GroupTargetType -eq "exclusionGroupAssignmentTarget"){ - + Write-Host "Group Target: " -NoNewline Write-Host "Excluded" -ForegroundColor Red - + } elseif($GroupTargetType -eq "GroupAssignmentTarget"){ - + Write-Host "Group Target: " -NoNewline Write-Host "Included" -ForegroundColor Green - + } Write-Host @@ -1202,39 +1213,39 @@ $TMACCount = @($TargetedManagedAppConfigurations).count foreach($MAMApp in $MAMApps){ if($MAMApp.mobileAppIdentifier.'@odata.type' -eq "#microsoft.graph.androidMobileAppIdentifier"){ - + $AppName = (Get-IntuneMAMApplication -packageId $MAMApp.mobileAppIdentifier.packageId) - + if($AppName.'@odata.type' -like "*$OS*"){ Write-Host $AppName.displayName "-" $AppName.'@odata.type' -ForegroundColor Green - + } - + else { - + Write-Host $AppName.displayName "-" $AppName.'@odata.type' - + } } - + elseif($MAMApp.mobileAppIdentifier.'@odata.type' -eq "#microsoft.graph.iosMobileAppIdentifier"){ - + $AppName = (Get-IntuneMAMApplication -bundleId $MAMApp.mobileAppIdentifier.bundleId) - + if($AppName.'@odata.type' -like "*$OS*"){ Write-Host $AppName.displayName "-" $AppName.'@odata.type' -ForegroundColor Green - + } - + else { - + Write-Host $AppName.displayName "-" $AppName.'@odata.type' - + } - + } } @@ -1243,7 +1254,7 @@ $TMACCount = @($TargetedManagedAppConfigurations).count Write-Host "Configuration Settings:" -ForegroundColor yellow $ExcludeGroup = $Group.target.'@odata.type' - + $AppConfigNames = $ManagedAppConfiguration.customsettings foreach($Config in $AppConfigNames){ @@ -1251,41 +1262,41 @@ $TMACCount = @($TargetedManagedAppConfigurations).count $searchName = $config.name if ($Config.name -like "*.*") { - + $Name = ($config.name).split(".")[-1] - + } elseif ($Config.name -like "*_*"){ - + $_appConfigName = ($config.name).replace("_"," ") $Name = (Get-Culture).TextInfo.ToTitleCase($_appConfigName.tolower()) } else { - + $Name = $config.name - + } $Value = ($TargetedManagedAppConfiguration.customSettings | ? { $_.Name -eq "$searchName" } | select value).value if ($name -like "*ListURLs*"){ - + $value = $Value.replace("|",", ") Write-Host Write-Host "$($Name):" -ForegroundColor Yellow Write-Host $($Value) - + } else { - + Write-Host "$($Name): $($Value)" - + } } @@ -1294,7 +1305,7 @@ $TMACCount = @($TargetedManagedAppConfigurations).count write-host "-------------------------------------------------------------------" write-host - } + } } @@ -1358,17 +1369,17 @@ if($AppConfigurations){ write-host (get-aadgroup -id $GroupID).displayname if($GroupTargetType -eq "exclusionGroupAssignmentTarget"){ - + Write-Host "Group Target: " -NoNewline Write-Host "Excluded" -ForegroundColor Red - + } elseif($GroupTargetType -eq "GroupAssignmentTarget"){ - + Write-Host "Group Target: " -NoNewline Write-Host "Included" -ForegroundColor Green - + } $TargetedApp = Get-IntuneApplication -id $AppConfiguration.targetedMobileApps @@ -1389,32 +1400,32 @@ if($AppConfigurations){ foreach($Config in $AppConfigNames){ if ($Config.appConfigKey -like "*.*") { - + if($config.appConfigKey -like "*userChangeAllowed*"){ - + $appConfigKey = ($config.appConfigKey).split(".")[-2,-1] $appConfigKey = $($appConfigKey)[-2] + " - " + $($appConfigKey)[-1] - + } else { - + $appConfigKey = ($config.appConfigKey).split(".")[-1] - + } } elseif ($Config.appConfigKey -like "*_*"){ - + $appConfigKey = ($config.appConfigKey).replace("_"," ") } - + else { - + $appConfigKey = ($config.appConfigKey) - + } Write-Host "$($appConfigKey): $($config.appConfigKeyValue)" @@ -1432,13 +1443,13 @@ if($AppConfigurations){ foreach($Config in $Configs){ if ($Config.key -like "*.*") { - + $appConfigKey = ($config.key).split(".")[-1] - + } elseif ($Config.key -like "*_*"){ - + $_appConfigKey = ($config.key).replace("_"," ") $appConfigKey = (Get-Culture).TextInfo.ToTitleCase($_appConfigKey.tolower()) @@ -1456,8 +1467,8 @@ if($AppConfigurations){ } - } - + } + } } @@ -1475,7 +1486,7 @@ if($AppConfigurations){ else { - Write-Host "No $OS App Configuration Policies: Managed Devices Exist..." + Write-Host "No $OS App Configuration Policies: Managed Devices Exist..." Write-Host } @@ -1488,3 +1499,4 @@ Write-Host "Evaluation complete..." -ForegroundColor Green Write-Host write-host "-------------------------------------------------------------------" Write-Host + diff --git a/UserPolicyReport/User_Policy_Report_Get.ps1 b/UserPolicyReport/User_Policy_Report_Get.ps1 index 911c2aa..ea86c80 100644 --- a/UserPolicyReport/User_Policy_Report_Get.ps1 +++ b/UserPolicyReport/User_Policy_Report_Get.ps1 @@ -7,152 +7,203 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "User.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { - - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } +#################################################### -} #################################################### -Function Get-AADUser(){ +function Get-AADUser { <# .SYNOPSIS @@ -180,36 +231,36 @@ param # Defining Variables $graphApiVersion = "v1.0" $User_resource = "users" - + try { - - if($userPrincipalName -eq "" -or $userPrincipalName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + if("" -eq $userPrincipalName -or $null -eq $userPrincipalName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - - if($Property -eq "" -or $Property -eq $null){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName" + if("" -eq $Property -or $null -eq $Property){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName" Write-Verbose $uri - Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get + Invoke-IntuneRestMethod -Uri $uri -Method GET } else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" + $uri = "$global:GraphEndpoint/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } catch { @@ -231,7 +282,7 @@ $User_resource = "users" #################################################### -Function Get-AADUserDevices(){ +function Get-AADUserDevices { <# .SYNOPSIS @@ -256,12 +307,12 @@ param # Defining Variables $graphApiVersion = "beta" $Resource = "users/$UserID/managedDevices" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" Write-Verbose $uri - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } @@ -284,7 +335,7 @@ $Resource = "users/$UserID/managedDevices" #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -310,37 +361,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ $GID = $Group.id @@ -348,13 +399,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } } @@ -378,7 +429,7 @@ $Group_resource = "groups" #################################################### -Function Get-DeviceCompliancePolicy(){ +function Get-DeviceCompliancePolicy { <# .SYNOPSIS @@ -410,9 +461,9 @@ param $graphApiVersion = "Beta" $DCP_resource = "deviceManagement/deviceCompliancePolicies" - + try { - + # windows81CompliancePolicy # windowsPhone81CompliancePolicy @@ -423,41 +474,41 @@ $DCP_resource = "deviceManagement/deviceCompliancePolicies" if($Win10.IsPresent){ $Count_Params++ } if($Count_Params -gt 1){ - + write-host "Multiple parameters set, specify a single parameter -Android -iOS or -Win10 against the function" -f Red - + } - + elseif($Android){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("android") } - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("android") } + } - + elseif($iOS){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("ios") } - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("ios") } + } elseif($Win10){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") } - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value | Where-Object { ($_.'@odata.type').contains("windows10CompliancePolicy") } + } - + else { - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + catch { $ex = $_.Exception @@ -477,7 +528,7 @@ $DCP_resource = "deviceManagement/deviceCompliancePolicies" #################################################### -Function Get-DeviceCompliancePolicyAssignment(){ +function Get-DeviceCompliancePolicyAssignment { <# .SYNOPSIS @@ -490,27 +541,27 @@ Returns any device compliance policy assignment configured in Intune .NOTES NAME: Get-DeviceCompliancePolicyAssignment #> - + [cmdletbinding()] - + param ( [Parameter(Mandatory=$true,HelpMessage="Enter id (guid) for the Device Compliance Policy you want to check assignment")] $id ) - + $graphApiVersion = "Beta" $DCP_resource = "deviceManagement/deviceCompliancePolicies" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)/$id/assignments" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($DCP_resource)/$id/assignments" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + catch { - + $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) @@ -521,14 +572,14 @@ $DCP_resource = "deviceManagement/deviceCompliancePolicies" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" write-host break - + } - + } #################################################### -Function Get-UserDeviceStatus(){ +function Get-UserDeviceStatus { [cmdletbinding()] @@ -578,17 +629,17 @@ $UserDevices = Get-AADUserDevices -UserID $UserID else { write-host "AAD Registered:" $UserDevice.aadRegistered } - + write-host "Enrollment Type:" $UserDevice.enrollmentType write-host "Management State:" $UserDevice.managementState if($UserDevice.complianceState -eq "noncompliant"){ - + write-host "Compliance State:" $UserDevice.complianceState -f Red - $uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$UserDeviceId/deviceCompliancePolicyStates" - - $deviceCompliancePolicyStates = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "beta/deviceManagement/managedDevices/$UserDeviceId/deviceCompliancePolicyStates" + + $deviceCompliancePolicyStates = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value foreach($DCPS in $deviceCompliancePolicyStates){ @@ -600,9 +651,9 @@ $UserDevices = Get-AADUserDevices -UserID $UserID $SettingStatesId = $DCPS.id - $uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$UserDeviceId/deviceCompliancePolicyStates/$SettingStatesId/settingStates?`$filter=(userId eq '$UserID')" + $uri = "beta/deviceManagement/managedDevices/$UserDeviceId/deviceCompliancePolicyStates/$SettingStatesId/settingStates?`$filter=(userId eq '$UserID')" - $SettingStates = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $SettingStates = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value foreach($SS in $SettingStates){ @@ -621,8 +672,8 @@ $UserDevices = Get-AADUserDevices -UserID $UserID } # Getting AAD Device using azureActiveDirectoryDeviceId property - $uri = "https://graph.microsoft.com/v1.0/devices?`$filter=deviceId eq '$UserDeviceAADDeviceId'" - $AADDevice = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "v1.0/devices?`$filter=deviceId eq '$UserDeviceAADDeviceId'" + $AADDevice = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value $AAD_Compliant = $AADDevice.isCompliant @@ -632,16 +683,16 @@ $UserDevices = Get-AADUserDevices -UserID $UserID Write-Host "Compliance State - AAD and ManagedDevices" -ForegroundColor Yellow Write-Host "AAD Compliance State:" $AAD_Compliant Write-Host "Intune Managed Device State:" $UserDeviceComplianceState - + } - + else { write-host "Compliance State:" $UserDevice.complianceState -f Green # Getting AAD Device using azureActiveDirectoryDeviceId property - $uri = "https://graph.microsoft.com/v1.0/devices?`$filter=deviceId eq '$UserDeviceAADDeviceId'" - $AADDevice = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "v1.0/devices?`$filter=deviceId eq '$UserDeviceAADDeviceId'" + $AADDevice = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value $AAD_Compliant = $AADDevice.isCompliant @@ -651,7 +702,7 @@ $UserDevices = Get-AADUserDevices -UserID $UserID Write-Host "Compliance State - AAD and ManagedDevices" -ForegroundColor Yellow Write-Host "AAD Compliance State:" $AAD_Compliant Write-Host "Intune Managed Device State:" $UserDeviceComplianceState - + } write-host @@ -676,50 +727,10 @@ $UserDevices = Get-AADUserDevices -UserID $UserID #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -748,9 +759,9 @@ $AADGroups = $MemberOf | ? { $_.'@odata.type' -eq "#microsoft.graph.group" } if($AADGroups){ write-host "User AAD Group Membership:" -f Yellow - + foreach($AADGroup in $AADGroups){ - + (Get-AADGroup -id $AADGroup.id).displayName } @@ -785,7 +796,7 @@ if($CPs){ if($DCPA){ foreach($Com_Group in $DCPA){ - + if($AADGroups.id -contains $Com_Group.target.GroupId){ $CP_Names += $CP.displayName + " - " + $CP.'@odata.type' @@ -798,16 +809,16 @@ if($CPs){ } - if($CP_Names -ne $null){ - + if($null -ne $CP_Names){ + $CP_Names - + } - + else { - + write-host "No Device Compliance Policies Assigned" - + } } @@ -826,3 +837,4 @@ write-host Get-UserDeviceStatus #################################################### + diff --git a/iOSAppProvisioningProfiles/iOSAppProvisioningProfile_Analysis.ps1 b/iOSAppProvisioningProfiles/iOSAppProvisioningProfile_Analysis.ps1 index 138dffc..f391cfb 100644 --- a/iOSAppProvisioningProfiles/iOSAppProvisioningProfile_Analysis.ps1 +++ b/iOSAppProvisioningProfiles/iOSAppProvisioningProfile_Analysis.ps1 @@ -6,152 +6,204 @@ See LICENSE in the project root for license information. #> -#################################################### - -function Get-AuthToken { +function Connect-GraphAPI { <# .SYNOPSIS -This function is used to authenticate with the Graph API REST interface +Connects to Microsoft Graph API with appropriate scopes for Intune operations .DESCRIPTION -The function authenticate with the Graph API Interface with the tenant name +This function connects to Microsoft Graph using the Microsoft.Graph.Authentication module +.PARAMETER Scopes +Array of permission scopes required for the operations +.PARAMETER Environment +The Microsoft Graph environment to connect to (Global, USGov, USGovDod, China, Germany) +.EXAMPLE +Connect-GraphAPI +Connects to Microsoft Graph with default scopes .EXAMPLE -Get-AuthToken -Authenticates you with the Graph API interface +Connect-GraphAPI -Environment "USGov" +Connects to Microsoft Graph US Government environment .NOTES -NAME: Get-AuthToken +Requires Microsoft.Graph.Authentication module #> + [CmdletBinding()] + param( + [string[]]$Scopes = @( + "DeviceManagementConfiguration.Read.All", + "Group.Read.All" + ), + [ValidateSet("Global", "USGov", "USGovDod", "China", "Germany")] + [string]$Environment = "Global" + ) -[cmdletbinding()] - -param -( - [Parameter(Mandatory=$true)] - $User -) - -$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User - -$tenant = $userUpn.Host - -Write-Host "Checking for AzureAD module..." - - $AadModule = Get-Module -Name "AzureAD" -ListAvailable - - if ($AadModule -eq $null) { - - Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" - $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable - - } - - if ($AadModule -eq $null) { - write-host - write-host "AzureAD Powershell module not installed..." -f Red - write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow - write-host "Script can't continue..." -f Red - write-host - exit - } - -# Getting path to ActiveDirectory Assemblies -# If the module count is greater than 1 find the latest version - - if($AadModule.count -gt 1){ - - $Latest_Version = ($AadModule | select version | Sort-Object)[-1] - - $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version } - - # Checking if there are multiple versions of the same module found - - if($AadModule.count -gt 1){ - - $aadModule = $AadModule | select -Unique + try { + # Set global Graph endpoint based on environment + switch ($Environment) { + "Global" { $global:GraphEndpoint = "https://graph.microsoft.com" } + "USGov" { $global:GraphEndpoint = "https://graph.microsoft.us" } + "USGovDod" { $global:GraphEndpoint = "https://dod-graph.microsoft.us" } + "China" { $global:GraphEndpoint = "https://microsoftgraph.chinacloudapi.cn" } + "Germany" { $global:GraphEndpoint = "https://graph.microsoft.de" } + default { $global:GraphEndpoint = "https://graph.microsoft.com" } + } - } + Write-Host "Graph Endpoint: $global:GraphEndpoint" -ForegroundColor Magenta + # Check if Microsoft.Graph.Authentication module is available + if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) { + Write-Error "Microsoft.Graph.Authentication module not found. Please install it using: Install-Module Microsoft.Graph.Authentication" + return $false + } - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" + # Import the module if not already loaded + if (-not (Get-Module -Name Microsoft.Graph.Authentication)) { + Import-Module Microsoft.Graph.Authentication -Force + } + # Connect to Microsoft Graph + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $Scopes -Environment $Environment -NoWelcome + + # Verify connection + $context = Get-MgContext + if ($context) { + Write-Host "Successfully connected to Microsoft Graph!" -ForegroundColor Green + Write-Host "Tenant ID: $($context.TenantId)" -ForegroundColor Yellow + Write-Host "Account: $($context.Account)" -ForegroundColor Yellow + Write-Host "Environment: $($context.Environment)" -ForegroundColor Yellow + Write-Host "Scopes: $($context.Scopes -join ', ')" -ForegroundColor Yellow + return $true + } + else { + Write-Error "Failed to establish connection to Microsoft Graph" + return $false + } } - - else { - - $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" - $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" - + catch { + Write-Error "Error connecting to Microsoft Graph: $($_.Exception.Message)" + return $false } +} -[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null - -[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null - -# Using this authentication method requires a clientID. Register a new app in the Entra ID admin center to obtain a clientID. More information -# on app registration and clientID is available here: https://learn.microsoft.com/entra/identity-platform/quickstart-register-app - -$clientId = "" +function Invoke-IntuneRestMethod { +<# +.SYNOPSIS +Invokes Microsoft Graph REST API calls with automatic paging support +.DESCRIPTION +This function makes REST API calls to Microsoft Graph with built-in error handling and automatic paging for large result sets +.PARAMETER Uri +The Microsoft Graph URI to call (can be relative path or full URL) +.PARAMETER Method +The HTTP method to use (GET, POST, PUT, DELETE, PATCH) +.PARAMETER Body +The request body for POST/PUT/PATCH operations +.PARAMETER ContentType +The content type for the request (default: application/json) +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.EXAMPLE +Invoke-IntuneRestMethod -Uri "v1.0/deviceManagement/deviceConfigurations" -Method GET +.NOTES +Requires an active Microsoft Graph connection via Connect-MgGraph +Uses the global $GraphEndpoint variable for environment-specific endpoints +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Uri, -$redirectUri = "urn:ietf:wg:oauth:2.0:oob" + [Parameter(Mandatory = $false)] + [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')] + [string]$Method = 'GET', -$resourceAppIdURI = "https://graph.microsoft.com" + [Parameter(Mandatory = $false)] + [object]$Body = $null, -$authority = "https://login.microsoftonline.com/$Tenant" + [Parameter(Mandatory = $false)] + [string]$ContentType = 'application/json' + ) try { + # Ensure we have a Graph endpoint set + if (-not $global:GraphEndpoint) { + $global:GraphEndpoint = "https://graph.microsoft.com" + Write-Warning "No Graph endpoint set, defaulting to: $global:GraphEndpoint" + } - $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority - - # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx - # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession - - $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" + # Handle both relative and absolute URIs + if (-not $Uri.StartsWith("http")) { + $Uri = "$global:GraphEndpoint/$Uri" + } - $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") + $results = @() + $nextLink = $Uri - $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result + do { + Write-Verbose "Making request to: $nextLink" - # If the accesstoken is valid then create the authentication header + $requestParams = @{ + Uri = $nextLink + Method = $Method + ContentType = $ContentType + } - if($authResult.AccessToken){ + if ($Body) { + if ($Body -is [string]) { + # Check if the string is valid JSON by trying to parse it + try { + $null = $Body | ConvertFrom-Json -ErrorAction Stop + # If we get here, it's valid JSON - use as-is + $requestParams.Body = $Body + Write-Verbose "Body detected as JSON string" + } + catch { + # String is not valid JSON, treat as plain string and wrap in quotes + $requestParams.Body = "`"$($Body)`"" + Write-Verbose "Body detected as plain string, wrapping in quotes" + } + } else { + # Body is an object (hashtable, PSCustomObject, etc.), convert to JSON + $requestParams.Body = $Body | ConvertTo-Json -Depth 10 + Write-Verbose "Body detected as object, converting to JSON" + } + } - # Creating header for Authorization token + $response = Invoke-MgGraphRequest @requestParams - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $authResult.AccessToken - 'ExpiresOn'=$authResult.ExpiresOn + # Handle paging + if ($response.value) { + $results += $response.value + $nextLink = $response.'@odata.nextLink' + } + else { + $results += $response + $nextLink = $null } - return $authHeader + } while ($nextLink) + return $results + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Error "Graph API request failed with status $statusCode : $errorMessage" } - else { - - Write-Host - Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red - Write-Host - break - + Write-Error "Graph API request failed: $errorMessage" } - + throw } +} - catch { +#################################################### - write-host $_.Exception.Message -f Red - write-host $_.Exception.ItemName -f Red - write-host - break +#################################################### - } - -} #################################################### -Function Get-AADGroup(){ +function Get-AADGroup { <# .SYNOPSIS @@ -177,37 +229,37 @@ param # Defining Variables $graphApiVersion = "v1.0" $Group_resource = "groups" - + try { if($id){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } - - elseif($GroupName -eq "" -or $GroupName -eq $null){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + elseif("" -eq $GroupName -or $null -eq $GroupName){ + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } else { - + if(!$Members){ - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + } - + elseif($Members){ - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" - $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - + + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" + $Group = (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value + if($Group){ $GID = $Group.id @@ -215,13 +267,13 @@ $Group_resource = "groups" $Group.displayName write-host - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" - (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Group_resource)/$GID/Members" + (Invoke-IntuneRestMethod -Uri $uri -Method GET).Value } } - + } } @@ -263,15 +315,15 @@ NAME: Get-iOSProvisioningProfile $graphApiVersion = "Beta" $Resource = "deviceAppManagement/iosLobAppProvisioningConfigurations?`$expand=assignments" - + try { - - $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" - (Invoke-RestMethod -Uri $uri –Headers $authToken –Method Get).value - + $uri = "$global:GraphEndpoint/$graphApiVersion/$($Resource)" + (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value + + } - + catch { $ex = $_.Exception @@ -287,56 +339,16 @@ $Resource = "deviceAppManagement/iosLobAppProvisioningConfigurations?`$expand=as } -} +} #################################################### #region Authentication -write-host - -# Checking if authToken exists before running authentication -if($global:authToken){ - - # Setting DateTime to Universal time to work in all timezones - $DateTime = (Get-Date).ToUniversalTime() - - # If the authToken exists checking when it expires - $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes - - if($TokenExpires -le 0){ - - write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow - write-host - - # Defining User Principal Name if not present - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - - $global:authToken = Get-AuthToken -User $User - - } -} - -# Authentication doesn't exist, calling Get-AuthToken function - -else { - - if($User -eq $null -or $User -eq ""){ - - $User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication" - Write-Host - - } - -# Getting the authorization token -$global:authToken = Get-AuthToken -User $User - +# Connect to Microsoft Graph +if (-not (Connect-GraphAPI)) { + Write-Error "Failed to connect to Microsoft Graph. Exiting script." + exit 1 } #endregion @@ -357,7 +369,7 @@ $CSV += "iOSAppProvisioningProfileName,GroupAssignedName,ExpiryDate" $GroupsOutput = @() foreach ($Profile in $Profiles) { - + $Payload = $Profile.payload $payloadFileName = $Profile.payloadFileName $PayloadRaw = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload)) @@ -370,12 +382,12 @@ $GroupsOutput = @() $TotalDays = ($TimeDifference.Days) write-host "iOS App Provisioning Profile Name: $($displayName)" - - + + if ($GroupID) { - + foreach ($id in $GroupID) { - + $GroupName = (Get-AADGroup -id $id).DisplayName write-host "Group assigned: $($GroupName)" $CSV += "$($displayName),$($GroupName),$($ProfileExpirationDate)" @@ -385,52 +397,52 @@ $GroupsOutput = @() } else { - - write-host "Group assigned: " -NoNewline + + write-host "Group assigned: " -NoNewline Write-Host "Unassigned" $CSV += "$($displayName),,$($ProfileExpirationDate)" - + } - + if ($TotalDays -gt "0") { - + Write-Host "iOS App Provisioning Profile Expiration Date: " -NoNewline write-host "$($ProfileExpirationDate)" -ForegroundColor Red } elseif ($TotalDays -gt "-30") { - + Write-Host "iOS App Provisioning Profile Expiration Date: " -NoNewline - write-host "$($ProfileExpirationDate)" -ForegroundColor Yellow + write-host "$($ProfileExpirationDate)" -ForegroundColor Yellow } else { - + Write-Host "iOS App Provisioning Profile: $($ProfileExpirationDate)" } - + Write-Host write-host "-------------------------------------------------------------------" write-host - - + + } if (!($Profiles.count -eq 0)) { Write-Host "Export results? [Y]es, [N]o" $conf = Read-Host - + if ($conf -eq "Y"){ $parent = [System.IO.Path]::GetTempPath() [string] $name = [System.Guid]::NewGuid() New-Item -ItemType Directory -Path (Join-Path $parent $name) | Out-Null - $TempDirPath = "$parent$name" + $TempDirPath = "$parent$name" $TempExportFilePath = "$($TempDirPath)\iOSAppProvisioningProfileExport.txt" $CSV | Add-Content $TempExportFilePath -Force Write-Host @@ -442,8 +454,9 @@ $GroupsOutput = @() } else { - + write-host "No iOS App Provisioning Profiles found." write-host } +