diff --git a/Intune-Set-PrimaryUser.ps1 b/Intune-Set-PrimaryUser.ps1 index ed04822..fc3b460 100644 --- a/Intune-Set-PrimaryUser.ps1 +++ b/Intune-Set-PrimaryUser.ps1 @@ -53,18 +53,17 @@ #region ---------------------------------------------------[Modifiable Parameters and defaults]------------------------------------ # Customizations -[System.Object]$Enrollmentaccounts = @("install@tbone.se", "wds@tbone.se") # @() = No Enrollment accounts. @("wds@tbone.se","wds2@tbone.se") = will filter them out and not assign them as primary users. -[int]$SigninsTimeSpan = 30 # Number of days back in time to look back for Sign-In logs (Default 30 days) -[int]$DeviceTimeSpan = 30 # Number of days back in time to look back for active devices (Default 30 days) -[Bool]$TestMode = $False # $True = No changes will be made on Primary owner, $False = Primary Owner will be changed -[Bool]$Verboselogging = $False # $Ture = Enable verbose logging for t-shoot. $False = Disable Verbose Logging -[Bool]$ReturnReport = $True # $True = Will return a report with all devices and primary users. $False = No report will be returned +[System.Object]$Enrollmentaccounts = @("install@tbone.se","wds@tbone.se") # @() = No Enrollment accounts. @("wds@tbone.se","wds2@tbone.se") = will filter them out and not assign them as primary users. +[int]$SigninsTimeSpan = 30 # Number of days back in time to look back for Sign-In logs (Default 30 days) +[int]$DeviceTimeSpan = 30 # Number of days back in time to look back for active devices (Default 30 days) +[Bool]$TestMode = $False # $True = No changes will be made on Primary owner, $False = Primary Owner will be changed +[Bool]$Verboselogging = $False # $Ture = Enable verbose logging for t-shoot. $False = Disable Verbose Logging +[Bool]$ReturnReport = $True # $True = Will return a report with all devices and primary users. $False = No report will be returned #Batch Runtime settings -[Bool]$RunBatchMode = $true #Run the script in batch mode, faster but uses more memory, recommended for large environments -[int]$Batchsize = 20 #How many objects to process in each batch -[int]$waittime = 0 #How many seconds to wait between Batches to avoid throttling -[int]$MaxRetry = 50 #How many retries of trottled requests before error -[string[]]$DeviceGroups = @("Group1", "Group2") # Add your desired groups here +[Bool]$RunBatchMode = $true #Run the script in batch mode, faster but uses more memory, recommended for large environments +[int]$Batchsize = 20 #How many objects to process in each batch +[int]$waittime = 0 #How many seconds to wait between Batches to avoid throttling +[int]$MaxRetry = 50 #How many retries of trottled requests before error #endregion #region ---------------------------------------------------[Set global script settings]-------------------------------------------- @@ -78,17 +77,17 @@ import-Module Microsoft.Graph.Reports #endregion #region ---------------------------------------------------[Static Variables]------------------------------------------------------ -[Int64]$MemoryUsage = 0 -[System.Object]$report = @() -[System.Object]$IntuneDevices = @() -[System.Object]$SignInLogs = @() -[System.Object]$AllPrimaryUsersHash = @() -[System.Object]$RequiredScopes = ("DeviceManagementManagedDevices.ReadWrite.All", "AuditLog.Read.All", "User.Read.All") -[datetime]$scriptStartTime = Get-Date -[datetime]$SignInsStartTime = (Get-Date).AddDays(-$SigninsTimeSpan ) -[datetime]$DeviceStartTime = (Get-Date).AddDays(-$DeviceTimeSpan ) -if ($Verboselogging) { $VerbosePreference = "Continue" } -else { $VerbosePreference = "SilentlyContinue" } +[Int64]$MemoryUsage = 0 +[System.Object]$report = @() +[System.Object]$IntuneDevices = @() +[System.Object]$SignInLogs = @() +[System.Object]$AllPrimaryUsersHash = @() +[System.Object]$RequiredScopes = ("DeviceManagementManagedDevices.ReadWrite.All", "AuditLog.Read.All", "User.Read.All") +[datetime]$scriptStartTime = Get-Date +[datetime]$SignInsStartTime = (Get-Date).AddDays(-$SigninsTimeSpan ) +[datetime]$DeviceStartTime = (Get-Date).AddDays(-$DeviceTimeSpan ) +if ($Verboselogging){$VerbosePreference = "Continue"} +else{$VerbosePreference = "SilentlyContinue"} #endregion #region ---------------------------------------------------[Functions]------------------------------------------------------------ @@ -100,50 +99,40 @@ function ConnectTo-MgGraph { $ErrorActionPreference = 'stop' [String]$resourceURL = "https://graph.microsoft.com/" $GraphAccessToken = $null - if ($env:AUTOMATION_ASSET_ACCOUNTID) { [Bool]$ManagedIdentity = $true } # Check if running in Azure Automation - else { [Bool]$ManagedIdentity = $false } # Otherwise running in Local PowerShell - } + if ($env:AUTOMATION_ASSET_ACCOUNTID) { [Bool]$ManagedIdentity = $true} # Check if running in Azure Automation + else { [Bool]$ManagedIdentity = $false} # Otherwise running in Local PowerShell + } Process { - if ($ManagedIdentity) { - #Connect to the Microsoft Graph using the ManagedIdentity and get the AccessToken - Try { - $response = [System.Text.Encoding]::Default.GetString((Invoke-WebRequest -UseBasicParsing -Uri "$($env:IDENTITY_ENDPOINT)?resource=$resourceURL" -Method 'GET' -Headers @{'X-IDENTITY-HEADER' = "$env:IDENTITY_HEADER"; 'Metadata' = 'True' }).RawContentStream.ToArray()) | ConvertFrom-Json + if ($ManagedIdentity){ #Connect to the Microsoft Graph using the ManagedIdentity and get the AccessToken + Try{$response = [System.Text.Encoding]::Default.GetString((Invoke-WebRequest -UseBasicParsing -Uri "$($env:IDENTITY_ENDPOINT)?resource=$resourceURL" -Method 'GET' -Headers @{'X-IDENTITY-HEADER' = "$env:IDENTITY_HEADER"; 'Metadata' = 'True'}).RawContentStream.ToArray()) | ConvertFrom-Json $GraphAccessToken = $response.access_token Write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get an Access Token to Graph for managed identity" - } - Catch { Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get an Access Token to Graph for managed identity, with error: $_" } + } + Catch{Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get an Access Token to Graph for managed identity, with error: $_"} $GraphVersion = ($GraphVersion = (Get-Module -Name 'Microsoft.Graph.Authentication' -ErrorAction SilentlyContinue).Version | Sort-Object -Desc | Select-Object -First 1) if ('2.0.0' -le $GraphVersion) { - Try { - Connect-MgGraph -ManagedIdentity -Nowelcome + Try{Connect-MgGraph -ManagedIdentity -Nowelcome $GraphAccessToken = convertto-securestring($response.access_token) -AsPlainText -Force - Write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to connect to Graph with module 2.x and Managedidentity" + Write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to connect to Graph with module 2.x and Managedidentity"} + Catch{Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to connect to Graph with module 2.x and Managedidentity, with error: $_"} } - Catch { Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to connect to Graph with module 2.x and Managedidentity, with error: $_" } - } - else { - #Connect to the Microsoft Graph using the AccessToken - Try { - Connect-mgGraph -AccessToken $GraphAccessToken - Write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to connect to Graph with module 1.x and Managedidentity" + else {#Connect to the Microsoft Graph using the AccessToken + Try{Connect-mgGraph -AccessToken $GraphAccessToken + Write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to connect to Graph with module 1.x and Managedidentity"} + Catch{Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to connect to Graph with module 1.x and Managedidentity, with error: $_"} } - Catch { Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to connect to Graph with module 1.x and Managedidentity, with error: $_" } } - } - else { - Try { - Connect-MgGraph -Scope $RequiredScopes -NoWelcome - Write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to connect to Graph manually" + else{ + Try{Connect-MgGraph -Scope $RequiredScopes -NoWelcome + Write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to connect to Graph manually"} + Catch{Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to connect to Graph manually, with error: $_"} } - Catch { Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to connect to Graph manually, with error: $_" } - } #Check and cleanup memory after connecting to Graph return $GraphAccessToken - } - End { - $MemoryUsage = [System.GC]::GetTotalMemory($true) + } + End {$MemoryUsage = [System.GC]::GetTotalMemory($true) Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to cleanup Memory usage after connect to Graph to: $(($MemoryUsage/1024/1024).ToString('N2')) MB" - } + } } function get-mggraphrequestbatch { Param( @@ -155,78 +144,70 @@ function get-mggraphrequestbatch { [int]$BatchSize, [int]$WaitTime, [int]$MaxRetry - ) + ) Begin { $Retrycount = 0 $CollectedObjects = [System.Collections.ArrayList]@() $LookupHash = @{} - if ($env:AUTOMATION_ASSET_ACCOUNTID) { $ManagedIdentity = $true } # Check if running in Azure Automation - else { $ManagedIdentity = $false } # Otherwise running in Local PowerShell - } + if ($env:AUTOMATION_ASSET_ACCOUNTID) {$ManagedIdentity = $true} # Check if running in Azure Automation + else {$ManagedIdentity = $false} # Otherwise running in Local PowerShell + } Process { do { $TotalObjects = $objects.count [int]$i = 0 $currentObject = 0 $RetryObjects = [System.Collections.ArrayList]@() - #Start looping all objects and run batches - for ($i = 0; $i -lt $TotalObjects; $i += $BatchSize) { + #Start looping all objects and run batches + for($i=0;$i -lt $TotalObjects;$i+=$BatchSize){ # Create Requests of id, method and url [System.Object]$req = @() - if ($i + ($BatchSize - 1) -lt $TotalObjects) { - $req += ($objects[$i..($i + ($BatchSize - 1))] | Select-Object @{n = 'id'; e = { $_.id } }, @{n = 'method'; e = { 'GET' } }, @{n = 'url'; e = { "/$($Object)/$($_.id)$($uri)" } }) - } - elseif ($TotalObjects -eq 1) { - $req += ($objects[$i] | Select-Object @{n = 'id'; e = { $_.id } }, @{n = 'method'; e = { 'GET' } }, @{n = 'url'; e = { "/$($Object)/$($_.id)$($uri)" } }) - } - else { - $req += ($objects[$i..($TotalObjects - 1)] | Select-Object @{n = 'id'; e = { $_.id } }, @{n = 'method'; e = { 'GET' } }, @{n = 'url'; e = { "/$($Object)/$($_.id)$($uri)" } }) + if($i + ($BatchSize-1) -lt $TotalObjects){ + $req += ($objects[$i..($i+($BatchSize-1))] | Select-Object @{n='id';e={$_.id}},@{n='method';e={'GET'}},@{n='url';e={"/$($Object)/$($_.id)$($uri)"}}) + } elseif ($TotalObjects -eq 1) { + $req += ($objects[$i] | Select-Object @{n='id';e={$_.id}},@{n='method';e={'GET'}},@{n='url';e={"/$($Object)/$($_.id)$($uri)"}}) + } else { + $req += ($objects[$i..($TotalObjects-1)] | Select-Object @{n='id';e={$_.id}},@{n='method';e={'GET'}},@{n='url';e={"/$($Object)/$($_.id)$($uri)"}}) } #Send the requests in a batch $responses = invoke-mggraphrequest -Method POST ` -URI "https://graph.microsoft.com/$($RunProfile)/`$batch" ` - -body (@{'requests' = $req } | convertto-json) - #Process the responses and verify status + -body (@{'requests' = $req} | convertto-json) + #Process the responses and verify status foreach ($respons in $responses.responses) { $CurrentObject++ switch ($respons.status) { - 200 { - [void] $CollectedObjects.Add($respons) - Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get object $($respons.id) from Graph batches" - } - 403 { write-error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Error Access denied during Graph batches - Status: $($respons.status)" } - 404 { write-error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Error Result not found during Graph batches- Status: $($respons.status)" } - 429 { - [void] $RetryObjects.Add($respons) - write-warning "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Warning, Throttling occured during Graph batches- Status: $($respons.status)" - } - default { - [void] $RetryObjects.Add($respons) - write-error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Error Other error occured during Graph batches - Status: $($respons.status)" - } + 200 {[void] $CollectedObjects.Add($respons) + Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get object $($respons.id) from Graph batches" } + 403 {write-error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Error Access denied during Graph batches - Status: $($respons.status)"} + 404 {write-error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Error Result not found during Graph batches- Status: $($respons.status)"} + 429 {[void] $RetryObjects.Add($respons) + write-warning "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Warning, Throttling occured during Graph batches- Status: $($respons.status)"} + default {[void] $RetryObjects.Add($respons) + write-error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Error Other error occured during Graph batches - Status: $($respons.status)"} } } #progressbar $Elapsedtime = (get-date) - $starttime $timeLeft = [TimeSpan]::FromMilliseconds((($ElapsedTime.TotalMilliseconds / $CurrentObject) * ($TotalObjects - $CurrentObject))) - if (!$ManagedIdentity) { + if (!$ManagedIdentity){ Write-Progress -Activity "Get $($uri) $($CurrentObject) of $($TotalObjects)" ` -Status "Est Time Left: $($timeLeft.Hours) Hour, $($timeLeft.Minutes) Min, $($timeLeft.Seconds) Sek - Throttled $($retryObjects.count) - Retry $($Retrycount) of $($MaxRetry)" ` -PercentComplete $([math]::ceiling($($CurrentObject / $TotalObjects) * 100)) } - $throttledResponses = $responses.responses | Select-Object -last 20 | Where-Object { $_.status -eq "429" } - $throttledResponse = $throttledResponses | Select-Object -last 1 - # | Select-Object -Property *,@{Name='HasDelay';Expression={$null -ne $_.headers."retry-after"}} | Where-Object HasDelay -eq $true + $throttledResponses = $responses.responses | Select-Object -last 20 | Where-Object {$_.status -eq "429"} + $throttledResponse = $throttledResponses |select -last 1 + # | Select-Object -Property *,@{Name='HasDelay';Expression={$null -ne $_.headers."retry-after"}} | Where-Object HasDelay -eq $true if ($throttledResponse) { [int]$recommendedWait = ($throttledResponses.headers.'retry-after' | Measure-object -Maximum).maximum write-warning "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Warning Throttling occured during Graph batches, Will wait the recommended $($recommendedWait+1) seconds" Start-Sleep -Seconds ($recommendedWait + 1) } - else { Start-Sleep -Milliseconds $WaitTime } #to avoid throttling + else{Start-Sleep -Milliseconds $WaitTime} #to avoid throttling } - if ($RetryObjects.Count -gt 0 -and $MaxRetry -gt 0) { + if ($RetryObjects.Count -gt 0 -and $MaxRetry -gt 0){ $Retrycount++ $MaxRetry-- write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to start rerun batches with $($RetryObjects.Count) collected a total of $($CollectedObjects.count))" @@ -234,14 +215,13 @@ function get-mggraphrequestbatch { $objects = $RetryObjects } }While ($RetryObjects.Count -gt 0 -and $MaxRetry -gt 0) - write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success returning $($CollectedObjects.count) objects from Graph batching" - foreach ($CollectedObject in $CollectedObjects) { $LookupHash[$CollectedObject.id] = $CollectedObject } - return $LookupHash + write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success returning $($CollectedObjects.count) objects from Graph batching" + foreach ($CollectedObject in $CollectedObjects) {$LookupHash[$CollectedObject.id] = $CollectedObject} + return $LookupHash } - End { - $MemoryUsage = [System.GC]::GetTotalMemory($true) + End {$MemoryUsage = [System.GC]::GetTotalMemory($true) Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to cleanup Memory usage after Graph batching to: $(($MemoryUsage/1024/1024).ToString('N2')) MB" - } + } } function Set-IntunePrimaryUsers { param ( @@ -254,93 +234,78 @@ function Set-IntunePrimaryUsers { ) Begin { $ErrorActionPreference = 'stop' - [int]$i = 0 - [String]$EnrollmentaccountsFilter = ($Enrollmentaccounts | ForEach-Object { [regex]::escape($_) }) -join '|' - } + [int]$i=0 + [String]$EnrollmentaccountsFilter = ($Enrollmentaccounts|ForEach-Object{[regex]::escape($_)}) -join '|' + } Process { - [System.Object]$report = @() - Foreach ($IntuneDevice in $IntuneDevices) { - [System.Object]$SignInLogsOnDevice = $null - [System.Object]$MostFrequentUser = $null - [hashtable]$primaryuserHash = @{} - [String]$MostFrequentUserPrincipalname = $null - [String]$MostFrequentUserID = $null - [String]$primaryUser = $null + [System.Object]$report = @() + Foreach ($IntuneDevice in $IntuneDevices){ + [System.Object]$SignInLogsOnDevice = $null + [System.Object]$MostFrequentUser = $null + [hashtable]$primaryuserHash = @{} + [String]$MostFrequentUserPrincipalname = $null + [String]$MostFrequentUserID = $null + [String]$primaryUser = $null $i++ #Get current Primary User - if ($AllPrimaryUsersHash.count -gt 0) { - $PrimaryuserHash = $AllPrimaryUsersHash[$IntuneDevice.id] - $primaryUserJson = ($primaryuserHash.body.value | ConvertTo-Json -Depth 9 | ConvertFrom-Json -Depth 9) + if ($AllPrimaryUsersHash.count -gt 0){$PrimaryuserHash = $AllPrimaryUsersHash[$IntuneDevice.id] + $primaryUserJson = ($primaryuserHash.body.value | ConvertTo-Json -Depth 9 | ConvertFrom-Json) if ($primaryUserJson -and $primaryUserJson.PSObject.Properties.Name -contains 'userprincipalname') { - $primaryuser = $primaryUserJson.userprincipalname - } - write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get Primary User $($Primaryuser) for $($IntuneDevice.DeviceName) from batch lookup" - } + $primaryuser = $primaryUserJson.userprincipalname} + write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get Primary User $($Primaryuser) for $($IntuneDevice.DeviceName) from batch lookup"} else { - try { - $primaryUser = (Get-MgDeviceManagementManagedDeviceUser -ManagedDeviceId $IntuneDevice.ID -property "UserPrincipalName").UserPrincipalName + try {$primaryUser = (Get-MgDeviceManagementManagedDeviceUser -ManagedDeviceId $IntuneDevice.ID -property "UserPrincipalName").UserPrincipalName write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get Primary User $($Primaryuser) for device $($IntuneDevice.DeviceName) from Graph" + } + catch{write-Warning "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get Primary User $($Primaryuser) for device $($IntuneDevice.DeviceName) from Graph with error: $_" + } } - catch { - write-Warning "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get Primary User $($Primaryuser) for device $($IntuneDevice.DeviceName) from Graph with error: $_" - } - } - if (!$primaryUser) { $primaryUser = ""; write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success getting Primary user for device $($IntuneDevice.DeviceName) but device has no Primary User" } + if (!$primaryUser){$primaryUser = "";write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success getting Primary user for device $($IntuneDevice.DeviceName) but device has no Primary User"} # Get sign in logs for the device - if ($enrollmentaccounts.count -ge 1) { $SignInLogsOnDevice = $SignInLogs | Where-Object { $_.deviceid -eq $IntuneDevice.AzureAdDeviceId -and $_.userprincipalname -notmatch $EnrollmentaccountsFilter } } - else { $SignInLogsOnDevice = $SignInLogs | Where-Object { $_.deviceid -eq $IntuneDevice.AzureAdDeviceId } } - if ($SignInLogsOnDevice) { $SignInUsers = $SignInLogsOnDevice | Select-Object userprincipalname, UserId | Group-Object userprincipalname } - else { + if ($enrollmentaccounts.count -ge 1){$SignInLogsOnDevice = $SignInLogs | Where-Object {$_.deviceid -eq $IntuneDevice.AzureAdDeviceId -and $_.userprincipalname -notmatch $EnrollmentaccountsFilter}} + else {$SignInLogsOnDevice = $SignInLogs | Where-Object {$_.deviceid -eq $IntuneDevice.AzureAdDeviceId}} + if ($SignInLogsOnDevice){$SignInUsers = $SignInLogsOnDevice | Select-Object userprincipalname, UserId | Group-Object userprincipalname} + else{ write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Warning Device $($IntuneDevice.DeviceName) is skipped due to failing to find Sign-In logs" - if ($ReturnReport) { $report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Warning Device $($IntuneDevice.DeviceName) is skipped due to failing to find Sign-In logs" } - continue - } + if ($ReturnReport){$report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Warning Device $($IntuneDevice.DeviceName) is skipped due to failing to find Sign-In logs"} + continue} $MostFrequentUser = $SignInUsers | Sort-Object count | Select-Object -Last 1 $MostFrequentUserPrincipalname = $MostFrequentUser.group[0].UserPrincipalName $MostFrequentUserID = $MostFrequentUser.group[0].UserID $IntuneDeviceID = $IntuneDevice.id #Set primary User if needed - if (($MostFrequentUserPrincipalname) -and ($MostFrequentUserid) -and ($MostFrequentUserPrincipalname -ne $PrimaryUser)) { + if (($MostFrequentUserPrincipalname) -and ($MostFrequentUserid) -and ($MostFrequentUserPrincipalname -ne $PrimaryUser)) + { write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to determine change needed on Device $($IntuneDevice.DeviceName) primaryuser from $($PrimaryUser) to $($MostFrequentUserPrincipalname)" $uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices('$IntuneDeviceID')/users/`$ref" $Body = @{ "@odata.id" = "https://graph.microsoft.com/beta/users/$MostFrequentUserid" } | ConvertTo-Json $Method = "POST" - if (!$TestMode) { - try { - Invoke-MgGraphRequest -Method $Method -uri $uri -body $Body + if (!$TestMode){ + try{Invoke-MgGraphRequest -Method $Method -uri $uri -body $Body write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName)" - if ($ReturnReport) { $report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName)" } - } - catch { - write-Warning "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName) with error: $_" - if ($ReturnReport) { $report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName) with error: $_" } + if ($ReturnReport){$report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName)"}} + catch{write-Warning "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName) with error: $_" + if ($ReturnReport){$report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName) with error: $_"}} } + else{write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Testmode - Will not set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName)" + if ($ReturnReport){$report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Testmode - Will not set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName)"}} } - else { - write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Testmode - Will not set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName)" - if ($ReturnReport) { $report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Testmode - Will not set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName)" } - } - } - else { - if (!$MostFrequentUserPrincipalname) { + else{ + if (!$MostFrequentUserPrincipalname){ write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to determine that Device $($IntuneDevice.DeviceName) has no logins in collected logs" - if ($ReturnReport) { $report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to determine that Device $($IntuneDevice.DeviceName) has no logins in collected logs" } - } - else { - write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to determine that Device $($IntuneDevice.DeviceName) have correct Primary User $($PrimaryUser)" - if ($ReturnReport) { $report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to determine that Device $($IntuneDevice.DeviceName) have correct Primary User $($PrimaryUser)" } + if ($ReturnReport){$report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to determine that Device $($IntuneDevice.DeviceName) has no logins in collected logs"}} + else {write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to determine that Device $($IntuneDevice.DeviceName) have correct Primary User $($PrimaryUser)" + if ($ReturnReport){$report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to determine that Device $($IntuneDevice.DeviceName) have correct Primary User $($PrimaryUser)"}} } } - } return $report - } - End { - $MemoryUsage = [System.GC]::GetTotalMemory($true) + } + End {$MemoryUsage = [System.GC]::GetTotalMemory($true) Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to cleanup Memory usage after set Primary Users to: $(($MemoryUsage/1024/1024).ToString('N2')) MB" + } } -} #endregion @@ -348,35 +313,14 @@ function Set-IntunePrimaryUsers { $StartTime = Get-Date $MgGraphAccessToken = ConnectTo-MgGraph -RequiredScopes $RequiredScopes -# Get Intune Devices -try { - $IntuneDevices = Get-MgDeviceManagementManagedDevice -Filter "operatingSystem eq 'Windows' and LastSyncDateTime gt $($DeviceStartTime.ToString('yyyy-MM-ddTHH:mm:ssZ'))" -All -Property "AzureAdDeviceId,DeviceName,Id" - - if ($DeviceGroups.Count -gt 0) { - $GroupIds = @() - foreach ($group in $DeviceGroups) { - $groupObj = Get-MgGroup -Filter "displayName eq '$group'" -Property id - if ($groupObj) { - $GroupIds += $groupObj.id - } - else { - Write-Warning "Group $group not found." - } - } - - $DeviceIdsInGroups = @() - foreach ($groupId in $GroupIds) { - $devicesInGroup = Get-MgGroupMember -GroupId $groupId -All -Property id | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.device' } - $DeviceIdsInGroups += $devicesInGroup.id - } - - $IntuneDevices = $IntuneDevices | Where-Object { $DeviceIdsInGroups -contains $_.Id } - } - - Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get $($IntuneDevices.Count) Devices with selected properties for devices synced last $($DeviceTimeSpan) days" +#Get Intune Devices +try{ + $IntuneDevices = @(Get-MgDeviceManagementManagedDevice -filter "operatingSystem eq 'Windows'and LastSyncDateTime gt $($DeviceStartTime.ToString("yyyy-MM-ddTHH:mm:ssZ"))" -all -Property "AzureAdDeviceId,DeviceName,Id") + $IntuneDeviceCount = $IntuneDevices | Measure-Object | Select-Object -ExpandProperty Count + write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get $($IntuneDeviceCount) Devices with selected properties for devices synced last $($DeviceTimeSpan) days" } -catch { - Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get Devices with error: $_" +catch{ + write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get Devices with error: $_" } #Memory Garbage collection @@ -384,38 +328,45 @@ $MemoryUsage = [System.GC]::GetTotalMemory($true) Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to cleanup Memory usage after get devices to: $(($MemoryUsage/1024/1024).ToString('N2')) MB" #Get Sign-In logs -try { - $SignInLogs = Get-MgAuditLogSignIn -Filter "appDisplayName eq 'Windows Sign In' and CreatedDateTime gt $($SignInsStartTime.ToString("yyyy-MM-ddTHH:mm:ssZ"))" -All | Select-Object devicedetail.deviceid, userprincipalname, UserId -ExpandProperty devicedetail - write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get $($SignInLogs.count) Sign-In logs with selected properties for last $($SigninsTimeSpan) days" +try{ + $SignInLogs = Get-MgAuditLogSignIn -Filter "appDisplayName eq 'Windows Sign In' and CreatedDateTime gt $($SignInsStartTime.ToString("yyyy-MM-ddTHH:mm:ssZ"))" -All | select devicedetail.deviceid,userprincipalname, UserId -ExpandProperty devicedetail + $SignInLogCount = $SignInLogs | Measure-Object | Select-Object -ExpandProperty Count + write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get $($SignInLogCount) Sign-In logs with selected properties for last $($SigninsTimeSpan) days" +} +catch{ + write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get Sign-In logs with error: $_" } -catch { write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get Sign-In logs with error: $_" } #Memory Garbage collection $MemoryUsage = [System.GC]::GetTotalMemory($true) Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to cleanup Memory usage after get Sign-In logs to: $(($MemoryUsage/1024/1024).ToString('N2')) MB" -if (($IntuneDevices) -and ($SignInLogs)) { - If ($RunBatchMode) { - #Getting Primary Users in batch mode - try { +if (($IntuneDevices) -and ($SignInLogs)){ + If ($RunBatchMode){ #Getting Primary Users in batch mode + try{ $AllPrimaryUsersHash = get-mggraphrequestbatch -RunProfile "beta" -method GET -Object "deviceManagement/managedDevices" -objects $IntuneDevices -uri "/users" -BatchSize $Batchsize -WaitTime $waittime -MaxRetry $MaxRetry - Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get Primary Users for $($AllPrimaryUsersHash.count) Devices" + Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get Primary Users for $($AllPrimaryUsersHash.count) Devices" + } + catch{ + write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get Primary Users for Devices with error: $_" } - catch { Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get Primary Users for Devices with error: $_" } $report = Set-IntunePrimaryUsers -IntuneDevices $IntuneDevices -SignInLogs $SignInLogs -AllPrimaryUsersHash $AllPrimaryUsersHash -Enrollmentaccounts $Enrollmentaccounts -TestMode $TestMode -ReturnReport $ReturnReport } - else { - #Getting Primary Users in foreach mode + else{ #Getting Primary Users in foreach mode $report = Set-IntunePrimaryUsers -IntuneDevices $IntuneDevices -SignInLogs $SignInLogs -AllPrimaryUsersHash $AllPrimaryUsersHash -Enrollmentaccounts $Enrollmentaccounts -TestMode $TestMode -ReturnReport $ReturnReport } } -else { write-Warning "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),No Devices or Sign-In logs found, exiting script" } +else{ + write-Warning "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),No Devices or Sign-In logs found, exiting script" +} -if ($ReturnReport) { write-Output -InputObject $report } +if ($ReturnReport){ + write-Output -InputObject $report +} disconnect-mggraph | out-null -[datetime]$scriptEndTime = Get-Date -$MemoryUsage = [System.GC]::GetTotalMemory($false) +[datetime]$scriptEndTime = Get-Date +$MemoryUsage = [System.GC]::GetTotalMemory($false) write-Output "Script execution time: $(($scriptEndTime-$scriptStartTime).ToString('hh\:mm\:ss'))" Write-Output "Memory Usage at the end of script execution: $(($MemoryUsage/1024/1024).ToString('N2')) MB" $VerbosePreference = "SilentlyContinue" diff --git a/Intune-Set-PrimaryUserGroups.ps1 b/Intune-Set-PrimaryUserGroups.ps1 new file mode 100644 index 0000000..da0f351 --- /dev/null +++ b/Intune-Set-PrimaryUserGroups.ps1 @@ -0,0 +1,420 @@ +<#PSScriptInfo +.SYNOPSIS + Script for Intune to set Primary User on Device + +.DESCRIPTION + This script will get the Entra Sign in logs for Windows Sign ins + The script then determine who has logged on to the device the most times in the last 30 days and set the Primary user to that user + The script uses Ms Graph with MGGraph modules + +.EXAMPLE + .\Intune-Set-PrimaryUser.ps1 + Will set the primary user for devices in Intune + +.NOTES + Written by Mr-Tbone (Tbone Granheden) Coligo AB + torbjorn.granheden@coligo.se + +.VERSION + 2.0 + +.RELEASENOTES + 1.0 2023-02-14 Initial Build + 2.0 2021-03-01 Large update to use Graph batching and reduce runtime + +.AUTHOR + Tbone Granheden + @MrTbone_se + +.COMPANYNAME + Coligo AB + +.GUID + 00000000-0000-0000-0000-000000000000 + +.COPYRIGHT + Feel free to use this, But would be grateful if My name is mentioned in Notes + +.CHANGELOG + 1.0.2202.1 - Initial Version + 2.0.2312.1 - Large update to use Graph batching and reduce runtime +#> + +#region ---------------------------------------------------[Set script requirements]----------------------------------------------- +# +#Requires -Modules Microsoft.Graph.Authentication +#Requires -Modules Microsoft.Graph.DeviceManagement +#Requires -Modules Microsoft.Graph.Reports +# +#endregion + +#region ---------------------------------------------------[Script Parameters]----------------------------------------------- +#endregion + +#region ---------------------------------------------------[Modifiable Parameters and defaults]------------------------------------ +# Customizations +[System.Object]$Enrollmentaccounts = @("install@tbone.se", "wds@tbone.se") # @() = No Enrollment accounts. @("wds@tbone.se","wds2@tbone.se") = will filter them out and not assign them as primary users. +[int]$SigninsTimeSpan = 30 # Number of days back in time to look back for Sign-In logs (Default 30 days) +[int]$DeviceTimeSpan = 30 # Number of days back in time to look back for active devices (Default 30 days) +[Bool]$TestMode = $True # $True = No changes will be made on Primary owner, $False = Primary Owner will be changed +[Bool]$Verboselogging = $True # $Ture = Enable verbose logging for t-shoot. $False = Disable Verbose Logging +[Bool]$ReturnReport = $True # $True = Will return a report with all devices and primary users. $False = No report will be returned +#Batch Runtime settings +[Bool]$RunBatchMode = $true #Run the script in batch mode, faster but uses more memory, recommended for large environments +[int]$Batchsize = 20 #How many objects to process in each batch +[int]$waittime = 0 #How many seconds to wait between Batches to avoid throttling +[int]$MaxRetry = 50 #How many retries of trottled requests before error +[string[]]$DeviceGroups = @("Group1", "Group2") # Add your desired groups here +#endregion + +#region ---------------------------------------------------[Set global script settings]-------------------------------------------- +Set-StrictMode -Version Latest +#endregion + +#region ---------------------------------------------------[Import Modules and Extensions]----------------------------------------- +import-Module Microsoft.Graph.Authentication +import-Module Microsoft.Graph.DeviceManagement +import-Module Microsoft.Graph.Reports +#endregion + +#region ---------------------------------------------------[Static Variables]------------------------------------------------------ +[Int64]$MemoryUsage = 0 +[System.Object]$report = @() +[System.Object]$IntuneDevices = @() +[System.Object]$SignInLogs = @() +[System.Object]$AllPrimaryUsersHash = @() +[System.Object]$RequiredScopes = ("DeviceManagementManagedDevices.ReadWrite.All", "AuditLog.Read.All", "User.Read.All") +[datetime]$scriptStartTime = Get-Date +[datetime]$SignInsStartTime = (Get-Date).AddDays(-$SigninsTimeSpan ) +[datetime]$DeviceStartTime = (Get-Date).AddDays(-$DeviceTimeSpan ) +if ($Verboselogging) { $VerbosePreference = "Continue" } +else { $VerbosePreference = "SilentlyContinue" } +#endregion + +#region ---------------------------------------------------[Functions]------------------------------------------------------------ +function ConnectTo-MgGraph { + param ( + [System.Object]$RequiredScopes + ) + Begin { + $ErrorActionPreference = 'stop' + [String]$resourceURL = "https://graph.microsoft.com/" + $GraphAccessToken = $null + if ($env:AUTOMATION_ASSET_ACCOUNTID) { [Bool]$ManagedIdentity = $true } # Check if running in Azure Automation + else { [Bool]$ManagedIdentity = $false } # Otherwise running in Local PowerShell + } + Process { + if ($ManagedIdentity) { + #Connect to the Microsoft Graph using the ManagedIdentity and get the AccessToken + Try { + $response = [System.Text.Encoding]::Default.GetString((Invoke-WebRequest -UseBasicParsing -Uri "$($env:IDENTITY_ENDPOINT)?resource=$resourceURL" -Method 'GET' -Headers @{'X-IDENTITY-HEADER' = "$env:IDENTITY_HEADER"; 'Metadata' = 'True' }).RawContentStream.ToArray()) | ConvertFrom-Json + $GraphAccessToken = $response.access_token + Write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get an Access Token to Graph for managed identity" + } + Catch { Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get an Access Token to Graph for managed identity, with error: $_" } + $GraphVersion = ($GraphVersion = (Get-Module -Name 'Microsoft.Graph.Authentication' -ErrorAction SilentlyContinue).Version | Sort-Object -Desc | Select-Object -First 1) + if ('2.0.0' -le $GraphVersion) { + Try { + Connect-MgGraph -ManagedIdentity -Nowelcome + $GraphAccessToken = convertto-securestring($response.access_token) -AsPlainText -Force + Write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to connect to Graph with module 2.x and Managedidentity" + } + Catch { Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to connect to Graph with module 2.x and Managedidentity, with error: $_" } + } + else { + #Connect to the Microsoft Graph using the AccessToken + Try { + Connect-mgGraph -AccessToken $GraphAccessToken + Write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to connect to Graph with module 1.x and Managedidentity" + } + Catch { Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to connect to Graph with module 1.x and Managedidentity, with error: $_" } + } + } + else { + Try { + Connect-MgGraph -Scope $RequiredScopes -NoWelcome + Write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to connect to Graph manually" + } + Catch { Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to connect to Graph manually, with error: $_" } + } + #Check and cleanup memory after connecting to Graph + return $GraphAccessToken + } + End { + $MemoryUsage = [System.GC]::GetTotalMemory($true) + Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to cleanup Memory usage after connect to Graph to: $(($MemoryUsage/1024/1024).ToString('N2')) MB" + } +} +function get-mggraphrequestbatch { + Param( + [string]$RunProfile, + [string]$Object, + [String]$Method, + [system.object]$Objects, + [string]$Uri, + [int]$BatchSize, + [int]$WaitTime, + [int]$MaxRetry + ) + Begin { + $Retrycount = 0 + $CollectedObjects = [System.Collections.ArrayList]@() + $LookupHash = @{} + if ($env:AUTOMATION_ASSET_ACCOUNTID) { $ManagedIdentity = $true } # Check if running in Azure Automation + else { $ManagedIdentity = $false } # Otherwise running in Local PowerShell + } + Process { + do { + $TotalObjects = $objects.count + [int]$i = 0 + $currentObject = 0 + $RetryObjects = [System.Collections.ArrayList]@() + #Start looping all objects and run batches + for ($i = 0; $i -lt $TotalObjects; $i += $BatchSize) { + # Create Requests of id, method and url + [System.Object]$req = @() + if ($i + ($BatchSize - 1) -lt $TotalObjects) { + $req += ($objects[$i..($i + ($BatchSize - 1))] | Select-Object @{n = 'id'; e = { $_.id } }, @{n = 'method'; e = { 'GET' } }, @{n = 'url'; e = { "/$($Object)/$($_.id)$($uri)" } }) + } + elseif ($TotalObjects -eq 1) { + $req += ($objects[$i] | Select-Object @{n = 'id'; e = { $_.id } }, @{n = 'method'; e = { 'GET' } }, @{n = 'url'; e = { "/$($Object)/$($_.id)$($uri)" } }) + } + else { + $req += ($objects[$i..($TotalObjects - 1)] | Select-Object @{n = 'id'; e = { $_.id } }, @{n = 'method'; e = { 'GET' } }, @{n = 'url'; e = { "/$($Object)/$($_.id)$($uri)" } }) + } + + #Send the requests in a batch + $responses = invoke-mggraphrequest -Method POST ` + -URI "https://graph.microsoft.com/$($RunProfile)/`$batch" ` + -body (@{'requests' = $req } | convertto-json) + #Process the responses and verify status + foreach ($respons in $responses.responses) { + $CurrentObject++ + switch ($respons.status) { + 200 { + [void] $CollectedObjects.Add($respons) + Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get object $($respons.id) from Graph batches" + } + 403 { write-error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Error Access denied during Graph batches - Status: $($respons.status)" } + 404 { write-error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Error Result not found during Graph batches- Status: $($respons.status)" } + 429 { + [void] $RetryObjects.Add($respons) + write-warning "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Warning, Throttling occured during Graph batches- Status: $($respons.status)" + } + default { + [void] $RetryObjects.Add($respons) + write-error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Error Other error occured during Graph batches - Status: $($respons.status)" + } + } + } + + #progressbar + $Elapsedtime = (get-date) - $starttime + $timeLeft = [TimeSpan]::FromMilliseconds((($ElapsedTime.TotalMilliseconds / $CurrentObject) * ($TotalObjects - $CurrentObject))) + if (!$ManagedIdentity) { + Write-Progress -Activity "Get $($uri) $($CurrentObject) of $($TotalObjects)" ` + -Status "Est Time Left: $($timeLeft.Hours) Hour, $($timeLeft.Minutes) Min, $($timeLeft.Seconds) Sek - Throttled $($retryObjects.count) - Retry $($Retrycount) of $($MaxRetry)" ` + -PercentComplete $([math]::ceiling($($CurrentObject / $TotalObjects) * 100)) + } + $throttledResponses = $responses.responses | Select-Object -last 20 | Where-Object { $_.status -eq "429" } + $throttledResponse = $throttledResponses | Select-Object -last 1 + # | Select-Object -Property *,@{Name='HasDelay';Expression={$null -ne $_.headers."retry-after"}} | Where-Object HasDelay -eq $true + if ($throttledResponse) { + [int]$recommendedWait = ($throttledResponses.headers.'retry-after' | Measure-object -Maximum).maximum + write-warning "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Warning Throttling occured during Graph batches, Will wait the recommended $($recommendedWait+1) seconds" + Start-Sleep -Seconds ($recommendedWait + 1) + } + else { Start-Sleep -Milliseconds $WaitTime } #to avoid throttling + } + if ($RetryObjects.Count -gt 0 -and $MaxRetry -gt 0) { + $Retrycount++ + $MaxRetry-- + write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to start rerun batches with $($RetryObjects.Count) collected a total of $($CollectedObjects.count))" + $objects = @() + $objects = $RetryObjects + } + }While ($RetryObjects.Count -gt 0 -and $MaxRetry -gt 0) + write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success returning $($CollectedObjects.count) objects from Graph batching" + foreach ($CollectedObject in $CollectedObjects) { $LookupHash[$CollectedObject.id] = $CollectedObject } + return $LookupHash + } + End { + $MemoryUsage = [System.GC]::GetTotalMemory($true) + Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to cleanup Memory usage after Graph batching to: $(($MemoryUsage/1024/1024).ToString('N2')) MB" + } +} +function Set-IntunePrimaryUsers { + param ( + [System.Object]$IntuneDevices, + [System.Object]$SignInLogs, + [System.Object]$AllPrimaryUsersHash, + [System.Object]$Enrollmentaccounts, + [Bool]$Testmode, + [Bool]$ReturnReport + ) + Begin { + $ErrorActionPreference = 'stop' + [int]$i = 0 + [String]$EnrollmentaccountsFilter = ($Enrollmentaccounts | ForEach-Object { [regex]::escape($_) }) -join '|' + } + Process { + [System.Object]$report = @() + Foreach ($IntuneDevice in $IntuneDevices) { + [System.Object]$SignInLogsOnDevice = $null + [System.Object]$MostFrequentUser = $null + [hashtable]$primaryuserHash = @{} + [String]$MostFrequentUserPrincipalname = $null + [String]$MostFrequentUserID = $null + [String]$primaryUser = $null + $i++ + #Get current Primary User + if ($AllPrimaryUsersHash.count -gt 0) { + $PrimaryuserHash = $AllPrimaryUsersHash[$IntuneDevice.id] + $primaryUserJson = ($primaryuserHash.body.value | ConvertTo-Json -Depth 9 | ConvertFrom-Json -Depth 9) + if ($primaryUserJson -and $primaryUserJson.PSObject.Properties.Name -contains 'userprincipalname') { + $primaryuser = $primaryUserJson.userprincipalname + } + write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get Primary User $($Primaryuser) for $($IntuneDevice.DeviceName) from batch lookup" + } + else { + try { + $primaryUser = (Get-MgDeviceManagementManagedDeviceUser -ManagedDeviceId $IntuneDevice.ID -property "UserPrincipalName").UserPrincipalName + write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get Primary User $($Primaryuser) for device $($IntuneDevice.DeviceName) from Graph" + } + catch { + write-Warning "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get Primary User $($Primaryuser) for device $($IntuneDevice.DeviceName) from Graph with error: $_" + } + } + if (!$primaryUser) { $primaryUser = ""; write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success getting Primary user for device $($IntuneDevice.DeviceName) but device has no Primary User" } + + # Get sign in logs for the device + if ($enrollmentaccounts.count -ge 1) { $SignInLogsOnDevice = $SignInLogs | Where-Object { $_.deviceid -eq $IntuneDevice.AzureAdDeviceId -and $_.userprincipalname -notmatch $EnrollmentaccountsFilter } } + else { $SignInLogsOnDevice = $SignInLogs | Where-Object { $_.deviceid -eq $IntuneDevice.AzureAdDeviceId } } + if ($SignInLogsOnDevice) { $SignInUsers = $SignInLogsOnDevice | Select-Object userprincipalname, UserId | Group-Object userprincipalname } + else { + write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Warning Device $($IntuneDevice.DeviceName) is skipped due to failing to find Sign-In logs" + if ($ReturnReport) { $report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Warning Device $($IntuneDevice.DeviceName) is skipped due to failing to find Sign-In logs" } + continue + } + $MostFrequentUser = $SignInUsers | Sort-Object count | Select-Object -Last 1 + $MostFrequentUserPrincipalname = $MostFrequentUser.group[0].UserPrincipalName + $MostFrequentUserID = $MostFrequentUser.group[0].UserID + $IntuneDeviceID = $IntuneDevice.id + + #Set primary User if needed + if (($MostFrequentUserPrincipalname) -and ($MostFrequentUserid) -and ($MostFrequentUserPrincipalname -ne $PrimaryUser)) { + write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to determine change needed on Device $($IntuneDevice.DeviceName) primaryuser from $($PrimaryUser) to $($MostFrequentUserPrincipalname)" + $uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices('$IntuneDeviceID')/users/`$ref" + $Body = @{ "@odata.id" = "https://graph.microsoft.com/beta/users/$MostFrequentUserid" } | ConvertTo-Json + $Method = "POST" + if (!$TestMode) { + try { + Invoke-MgGraphRequest -Method $Method -uri $uri -body $Body + write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName)" + if ($ReturnReport) { $report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName)" } + } + catch { + write-Warning "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName) with error: $_" + if ($ReturnReport) { $report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName) with error: $_" } + } + } + else { + write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Testmode - Will not set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName)" + if ($ReturnReport) { $report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Testmode - Will not set Primary User $($MostFrequentUserPrincipalname) for device $($IntuneDevice.DeviceName)" } + } + } + else { + if (!$MostFrequentUserPrincipalname) { + write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to determine that Device $($IntuneDevice.DeviceName) has no logins in collected logs" + if ($ReturnReport) { $report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to determine that Device $($IntuneDevice.DeviceName) has no logins in collected logs" } + } + else { + write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to determine that Device $($IntuneDevice.DeviceName) have correct Primary User $($PrimaryUser)" + if ($ReturnReport) { $report += "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to determine that Device $($IntuneDevice.DeviceName) have correct Primary User $($PrimaryUser)" } + } + } + } + return $report + } + End { + $MemoryUsage = [System.GC]::GetTotalMemory($true) + Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to cleanup Memory usage after set Primary Users to: $(($MemoryUsage/1024/1024).ToString('N2')) MB" + } +} + +#endregion + +#region ---------------------------------------------------[[Script Execution]------------------------------------------------------ +$StartTime = Get-Date +$MgGraphAccessToken = ConnectTo-MgGraph -RequiredScopes $RequiredScopes + +# Get Intune Devices +try { + $IntuneDevices = Get-MgDeviceManagementManagedDevice -Filter "operatingSystem eq 'Windows' and LastSyncDateTime gt $($DeviceStartTime.ToString('yyyy-MM-ddTHH:mm:ssZ'))" -All -Property "AzureAdDeviceId,DeviceName,Id" + + if ($DeviceGroups.Count -gt 0) { + $GroupIds = @() + foreach ($group in $DeviceGroups) { + $groupObj = Get-MgGroup -Filter "displayName eq '$group'" -Property id + if ($groupObj) { + $GroupIds += $groupObj.id + } + else { + Write-Warning "Group $group not found." + } + } + + $DeviceIdsInGroups = @() + foreach ($groupId in $GroupIds) { + $devicesInGroup = Get-MgGroupMember -GroupId $groupId -All -Property id | Where-Object { $_.id -ne $null } + $DeviceIdsInGroups += $devicesInGroup.id + } + + $IntuneDevices = $IntuneDevices | Where-Object { $DeviceIdsInGroups -contains $_.Id } + } + + $IntuneDeviceCount = $IntuneDevices | Measure-Object | Select-Object -ExpandProperty Count + Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get $($IntuneDeviceCount) Devices with selected properties for devices synced last $($DeviceTimeSpan) days" +} +catch { + Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get Devices with error: $_" +} + +# Get Sign-In logs +try { + $SignInLogs = Get-MgAuditLogSignIn -Filter "appDisplayName eq 'Windows Sign In' and CreatedDateTime gt $($SignInsStartTime.ToString("yyyy-MM-ddTHH:mm:ssZ"))" -All | Select-Object devicedetail.deviceid, userprincipalname, UserId -ExpandProperty devicedetail + $SignInLogCount = $SignInLogs | Measure-Object | Select-Object -ExpandProperty Count + write-verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get $($SignInLogCount) Sign-In logs with selected properties for last $($SigninsTimeSpan) days" +} +catch { write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get Sign-In logs with error: $_" } + +# Memory Garbage collection +$MemoryUsage = [System.GC]::GetTotalMemory($true) +Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to cleanup Memory usage after get Sign-In logs to: $(($MemoryUsage/1024/1024).ToString('N2')) MB" + +if (($IntuneDevices) -and ($SignInLogs)) { + If ($RunBatchMode) { + # Getting Primary Users in batch mode + try { + $AllPrimaryUsersHash = get-mggraphrequestbatch -RunProfile "beta" -method GET -Object "deviceManagement/managedDevices" -objects $IntuneDevices -uri "/users" -BatchSize $Batchsize -WaitTime $waittime -MaxRetry $MaxRetry + $AllPrimaryUsersHashCount = $AllPrimaryUsersHash.Keys.Count + Write-Verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Success to get Primary Users for $($AllPrimaryUsersHashCount) Devices" + } + catch { Write-Error "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),Failed to get Primary Users for Devices with error: $_" } + $report = Set-IntunePrimaryUsers -IntuneDevices $IntuneDevices -SignInLogs $SignInLogs -AllPrimaryUsersHash $AllPrimaryUsersHash -Enrollmentaccounts $Enrollmentaccounts -TestMode $TestMode -ReturnReport $ReturnReport + } + else { + # Getting Primary Users in foreach mode + $report = Set-IntunePrimaryUsers -IntuneDevices $IntuneDevices -SignInLogs $SignInLogs -AllPrimaryUsersHash $AllPrimaryUsersHash -Enrollmentaccounts $Enrollmentaccounts -TestMode $TestMode -ReturnReport $ReturnReport + } +} +else { write-Warning "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),No Devices or Sign-In logs found, exiting script" } + +if ($ReturnReport) { write-Output -InputObject $report } + +disconnect-mggraph | out-null +[datetime]$scriptEndTime = Get-Date +$MemoryUsage = [System.GC]::GetTotalMemory($false) +write-Output "Script execution time: $(($scriptEndTime-$scriptStartTime).ToString('hh\:mm\:ss'))" +Write-Output "Memory Usage at the end of script execution: $(($MemoryUsage/1024/1024).ToString('N2')) MB" +$VerbosePreference = "SilentlyContinue"