UCLobbyTeams.psm1
#Region '.\Private\Convert-UcTeamsDeviceType.ps1' -1 function Convert-UcTeamsDeviceType { param ( [string]$DeviceType ) switch ($DeviceType) { "ipPhone" { return "Phone" } "lowCostPhone" { return "Phone" } "teamsRoom" { return "MTR Windows" } "collaborationBar" { return "MTR Android" } "surfaceHub" { return "Surface Hub" } "teamsDisplay" { return "Display" } "touchConsole" { return "Touch Console (MTRA)" } "teamsPanel" { return "Panel" } "sip" { return "SIP Phone" } Default { return $DeviceType} } } #EndRegion '.\Private\Convert-UcTeamsDeviceType.ps1' 18 #Region '.\Private\ConvertTo-IPv4MaskString.ps1' -1 function ConvertTo-IPv4MaskString { param( [parameter(Mandatory = $true)] [ValidateRange(0, 32)] [Int] $MaskBits ) <# .SYNOPSIS Converts a number of bits (0-32) to an IPv4 network mask string (e.g., "255.255.255.0"). .DESCRIPTION Converts a number of bits (0-32) to an IPv4 network mask string (e.g., "255.255.255.0"). .PARAMETER MaskBits Specifies the number of bits in the mask. Credits to: Bill Stewart - https://www.itprotoday.com/powershell/working-ipv4-addresses-powershell #> $mask = ([Math]::Pow(2, $MaskBits) - 1) * [Math]::Pow(2, (32 - $MaskBits)) $bytes = [BitConverter]::GetBytes([UInt32] $mask) (($bytes.Count - 1)..0 | ForEach-Object { [String] $bytes[$_] }) -join "." } #EndRegion '.\Private\ConvertTo-IPv4MaskString.ps1' 24 #Region '.\Private\Get-UcUPNFromString.ps1' -1 function Get-UcUPNFromString { param ( [string]$InputStr ) $regexUPN = "([^|]+@[a-zA-Z0-9_\-\.]+\.[a-zA-Z]*)" try { $RegexTemp = [regex]::Match($InputStr, $regexUPN).captures.groups if ($RegexTemp.Count -ge 2) { $outUPN = $RegexTemp[1].value } return $outUPN } catch { return "" } } #EndRegion '.\Private\Get-UcUPNFromString.ps1' 17 #Region '.\Private\Invoke-UcMgGraphBatch.ps1' -1 function Invoke-UcMgGraphBatch { param( [object]$Requests, [ValidateSet("beta", "v1.0")] [string]$MgProfile, [string]$Activity, [switch]$IncludeBody ) $GraphURI_BetaAPIBatch = "https://graph.microsoft.com/beta/`$batch" $GraphURI_ProdAPIBatch = "https://graph.microsoft.com/v1.0/`$batch" $outBatchResponses = [System.Collections.ArrayList]::new() $tmpGraphRequests = [System.Collections.ArrayList]::new() if ($MgProfile.Equals("beta")) { $GraphURI_Batch = $GraphURI_BetaAPIBatch } else { $GraphURI_Batch = $GraphURI_ProdAPIBatch } #If activity is null then we can use this to get the function that call this function. if (!($Activity)) { $Activity = [string]$(Get-PSCallStack)[1].FunctionName } $g = 1 $batchCount = [int][Math]::Ceiling(($Requests.count / 20)) foreach ($GraphRequest in $Requests) { Write-Progress -Activity $Activity -Status "Running batch $g of $batchCount" [void]$tmpGraphRequests.Add($GraphRequest) if ($tmpGraphRequests.Count -ge 20) { $g++ $grapRequestBody = ' { "requests": ' + ($tmpGraphRequests | ConvertTo-Json) + ' }' $GraphResponses += (Invoke-MgGraphRequest -Method Post -Uri $GraphURI_Batch -Body $grapRequestBody).responses $tmpGraphRequests = [System.Collections.ArrayList]::new() } } if ($tmpGraphRequests.Count -gt 0) { Write-Progress -Activity $Activity -Status "Running batch $g of $batchCount" #TO DO: Look for alternatives instead of doing this. if ($tmpGraphRequests.Count -gt 1) { $grapRequestBody = ' { "requests": ' + ($tmpGraphRequests | ConvertTo-Json) + ' }' } else { $grapRequestBody = ' { "requests": [' + ($tmpGraphRequests | ConvertTo-Json) + '] }' } try{ $GraphResponses += (Invoke-MgGraphRequest -Method Post -Uri $GraphURI_Batch -Body $grapRequestBody).responses } catch { Write-Warning "Error while getting the Graph Request." } } #In some cases we will need the complete graph response, in that case the calling function will have to process pending pages. $attempts = 1 for ($j = 0; $j -lt $GraphResponses.length; $j++) { $ResponseCount = 0 if($IncludeBody){ $outBatchResponses += $GraphResponses[$j] } else { $outBatchResponses += $GraphResponses[$j].body if ($GraphResponses[$j].status -eq "200"){ #Checking if there are more pages available $GraphURI_NextPage = $GraphResponses[$j].body.'@odata.nextLink' $GraphTotalCount = $GraphResponses[$j].body.'@odata.count' $ResponseCount += $GraphResponses[$j].body.value.count while (![string]::IsNullOrEmpty($GraphURI_NextPage)) { try{ $graphNextPageResponse = Invoke-MgGraphRequest -Method Get -Uri $GraphURI_NextPage $outBatchResponses += $graphNextPageResponse $GraphURI_NextPage = $graphNextPageResponse.'@odata.nextLink' $ResponseCount += $graphNextPageResponse.value.count Write-Progress -Activity $Activity -Status "$ResponseCount of $GraphTotalCount" } catch { Write-Warning "Failed to get the next batch page, retrying..." $attempts-- } if($attempts -eq 0){ Write-Warning "Could not get next batch page, skiping it." break } } } else { Write-Warning ("Failed to get Graph Response" + [Environment]::NewLine + ` "Error Code: " + $GraphResponses[$j].status + " " + $GraphResponses[$j].body.error.code + [Environment]::NewLine + ` "Error Message: " + $GraphResponses[$j].body.error.message + [Environment]::NewLine + ` "Request Date: " + $GraphResponses[$j].body.error.innerError.date + [Environment]::NewLine + ` "Request ID: " + $GraphResponses[$j].body.error.innerError.'request-id' + [Environment]::NewLine + ` "Client Request Id: " + $GraphResponses[$j].body.error.innerError.'client-request-id') } } } return $outBatchResponses } #EndRegion '.\Private\Invoke-UcMgGraphBatch.ps1' 97 #Region '.\Private\Test-UcIPAddressInSubnet.ps1' -1 function Test-UcIPaddressInSubnet { <# .SYNOPSIS Check if an IP address is part of an Subnet. .DESCRIPTION Returns true if the given IP address is part of the subnet, false for not or invalid ip address. Contributors: David Paulino .PARAMETER IPAddress IP Address that we want to confirm that belongs to a range. .PARAMETER Subnet Subnet in the IPaddress/SubnetMaskBits. .EXAMPLE PS> Test-UcIPaddressInSubnet -IPAddress 192.168.0.1 -Subnet 192.168.0.0/24 #> param( [Parameter(mandatory = $true)] [string]$IPAddress, [Parameter(mandatory = $true)] [string]$Subnet ) $regExIPAddressSubnet = "^((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9]))\/(3[0-2]|[1-2]{1}[0-9]{1}|[1-9])$" try { [void]($Subnet -match $regExIPAddressSubnet) $IPSubnet = [ipaddress]$Matches[1] $tmpIPAddress = [ipaddress]$IPAddress $subnetMask = ConvertTo-IPv4MaskString $Matches[6] $tmpSubnet = [ipaddress] ($subnetMask) $netidSubnet = [ipaddress]($IPSubnet.address -band $tmpSubnet.address) $netidIPAddress = [ipaddress]($tmpIPAddress.address -band $tmpSubnet.address) return ($netidSubnet.ipaddresstostring -eq $netidIPAddress.ipaddresstostring) } catch { return $false } } #EndRegion '.\Private\Test-UcIPAddressInSubnet.ps1' 42 #Region '.\Private\Test-UcMgGraphConnection.ps1' -1 function Test-UcMgGraphConnection { param( [Parameter(mandatory = $false)] [string[]]$Scopes, [string[]]$AltScopes, [ValidateSet("AppOnly", "Delegated")] [string]$AuthType ) <# .SYNOPSIS Test connection to Microsoft Graph .DESCRIPTION This function will validate if the current connection to Microsoft Graph has the required Scopes. Contributors: Daniel Jelinek, David Paulino Requirements: Microsoft Graph PowerShell Module (Install-Module Microsoft.Graph) .PARAMETER Scopes When present it will get detailed information from Teams Devices #> #Checking if Microsoft.Graph is installed if (!(Get-Module Microsoft.Graph.Authentication -ListAvailable)) { Write-Warning ("Missing Microsoft.Graph.Authentication PowerShell module. Please install it with:" + [Environment]::NewLine + "Install-Module Microsoft.Graph.Authentication") return $false } $MgGraphContext = Get-MgContext if ($AuthType -and $MgGraphContext.AuthType -ne $AuthType) { Write-Warning ("Wrong Permission Type: " + $MgGraphContext.AuthType + ", this PowerShell cmdlet requires: $AuthType") return $false } #Checking if we have the required scopes. $currentScopes = $MgGraphContext.Scopes $strScope = "" $strAltScope = "" $missingScopes = "" $missingAltScopes = "" $missingScope = $false $missingAltScope = $false foreach ($scope in $Scopes) { $strScope += "`"" + $scope + "`"," if ($scope -notin $currentScopes) { $missingScope = $true $missingScopes += $scope + "," } } #20231013 - Added option to specify alternative scopes if ($missingScope -and $AltScopes) { foreach ($altScope in $AltScopes) { $strAltScope += "`"" + $altScope + "`"," if ($altScope -notin $currentScopes) { $missingAltScope = $true $missingAltScopes += $altScope + "," } } #20231117 - Issue when AltScopes wasnt submitted } else { $missingAltScope = $true } if (!($currentScopes)) { Write-Warning ("Not Connected to Microsoft Graph" + [Environment]::NewLine + "Please connect to Microsoft Graph before running this cmdlet." + [Environment]::NewLine + "Commercial Tenant: Connect-MgGraph -Scopes " + $strScope.Substring(0, $strScope.Length - 1) + [Environment]::NewLine + "US Gov (GCC-H) Tenant: Connect-MgGraph -Scopes " + $strScope.Substring(0, $strScope.Length - 1) + " -Environment USGov") return $false } #If scopes are missing we need to connect using the required scopes if ($missingScope -and $missingAltScope) { if ($Scopes -and $AltScopes) { Write-Warning ("Missing scope(s): " + $missingScopes.Substring(0, $missingScopes.Length - 1) + " and missing alternative Scope(s): " + $missingAltScopes.Substring(0, $missingAltScopes.Length - 1) + ` [Environment]::NewLine + "Please reconnect to Microsoft Graph before running this cmdlet." + ` [Environment]::NewLine + "Commercial Tenant: Connect-MgGraph -Scopes " + $strScope.Substring(0, $strScope.Length - 1) + " or Connect-MgGraph -Scopes " + $strAltScope.Substring(0, $strAltScope.Length - 1) + ` [Environment]::NewLine + "US Gov (GCC-H) Tenant: Connect-MgGraph -Environment USGov -Scopes " + $strScope.Substring(0, $strScope.Length - 1) + " or Connect-MgGraph -Environment USGov -Scopes " + $strAltScope.Substring(0, $strAltScope.Length - 1) ) } else { Write-Warning ("Missing scope(s): " + $missingScopes.Substring(0, $missingScopes.Length - 1) + ` [Environment]::NewLine + "Please reconnect to Microsoft Graph before running this cmdlet." + ` [Environment]::NewLine + "Commercial Tenant: Connect-MgGraph -Scopes " + $strScope.Substring(0, $strScope.Length - 1) + ` [Environment]::NewLine + "US Gov (GCC-H) Tenant: Connect-MgGraph -Environment USGov -Scopes " + $strScope.Substring(0, $strScope.Length - 1)) } return $false } else { return $true } } #EndRegion '.\Private\Test-UcMgGraphConnection.ps1' 93 #Region '.\Private\Test-UcMicrosoftTeamsConnection.ps1' -1 function Test-UcMicrosoftTeamsConnection { <# .SYNOPSIS Test connection to Microsoft Teams PowerShell .DESCRIPTION This function will validate if the current session is connected to Microsoft Teams PowerShell. #> #Checking if MicrosoftTeams module is installed if (!(Get-Module MicrosoftTeams -ListAvailable)) { Write-Warning ("Missing MicrosoftTemas PowerShell module. Please install it with:" + [Environment]::NewLine + "Install-Module MicrosoftTeams") return $false } #We need to use a cmdlet to know if we are connected to MicrosoftTeams PowerShell try { Get-CsTenant -ErrorAction SilentlyContinue | Out-Null return $true } catch [System.UnauthorizedAccessException] { Write-Warning ("Please connect to Microsoft Teams PowerShell with Connect-MicrosoftTeams before running this cmdlet") return $false } } #EndRegion '.\Private\Test-UcMicrosoftTeamsConnection.ps1' 24 #Region '.\Public\Export-UcM365LicenseAssignment.ps1' -1 function Export-UcM365LicenseAssignment { param( [string]$SKU, [switch]$UseFriendlyNames, [switch]$SkipServicePlan, [string]$OutputPath, [switch]$DuplicateServicePlansOnly ) <# .SYNOPSIS Generate a report of the User assigned licenses either direct or assigned by group (Inherited) .DESCRIPTION This script will get a report of all Service Plans assigned to users and how the license is assigned to the user (Direct, Inherited) Contributors: David Paulino, Freydem Fernandez Lopez, Gal Naor Requirements: Microsoft Graph PowerShell Module (Install-Module Microsoft.Graph) Microsoft Graph Scopes: "Directory.Read.All" .PARAMETER UseFriendlyNames When present will download a csv file containing the License/ServicePlans friendly names Product names and service plan identifiers for licensing https://learn.microsoft.com/en-us/azure/active-directory/enterprise-users/licensing-service-plan-reference .PARAMETER SkipServicePlan When present will just check the licenses and not the service plans assigned to the user. .PARAMETER OutputPath Allows to specify the path where we want to save the results. By default, it will save on current user Download. .PARAMETER DuplicateServicePlansOnly When present the report will be the users that have the same service plan from different assigned licenses. .EXAMPLE PS> Get-UcM365LicenseAssignment .EXAMPLE PS> Get-UcM365LicenseAssignment -UseFriendlyNames #> $startTime = Get-Date if ((Test-UcMgGraphConnection -Scopes "Directory.Read.All" -AltScopes ("User.Read.All", "Organization.Read.All"))) { Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null $outFile = "M365LicenseAssigment_" #region 20240905 - Users with Duplicate Service Plans if($DuplicateServicePlansOnly){ $outFile += "DuplicateServicePlansOnly_" } #endregion $outFile += (Get-Date).ToString('yyyyMMdd-HHmmss') + ".csv" #Verify if the Output Path exists if ($OutputPath) { if (!(Test-Path $OutputPath -PathType Container)) { Write-Host ("Error: Invalid folder " + $OutputPath) -ForegroundColor Red return } $OutputFilePath = [System.IO.Path]::Combine($OutputPath, $outFile) } else { $OutputFilePath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads", $outFile) } if ($UseFriendlyNames) { #20231019 - Change: OutputPath will be for both report and Product names and service plan identifiers for licensing.csv $SKUnSPFilePath = [System.IO.Path]::Combine($OutputPath, "Product names and service plan identifiers for licensing.csv") if (!(Test-Path -Path $SKUnSPFilePath)) { try { Write-Warning "M365 Product Names and Service Plans file not found, attempting to download it." Invoke-WebRequest -Uri "https://download.microsoft.com/download/e/3/e/e3e9faf2-f28b-490a-9ada-c6089a1fc5b0/Product%20names%20and%20service%20plan%20identifiers%20for%20licensing.csv" -OutFile $SKUnSPFilePath } catch { Write-Warning "Could not download M365 Product Names and Service Plans." } } try { $SKUnSP = import-CSV -Path $SKUnSPFilePath } catch { Write-Warning "Could not import Service Plan ID file." $UseFriendlyNames = $false } } #region 20240905 - Combined Graph calls for SKUs, Licensed Groups #Tenant SKUs - All Licenses that exist in the tenant $graphRequests = [System.Collections.ArrayList]::new() $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "TenantSKUs" method = "GET" url = "/subscribedSkus?`$select=skuID,skuPartNumber,servicePlans,appliesTo,consumedUnits" } [void]$graphRequests.Add($gRequestTmp) #Groups with Licenses Assignment. $GraphRequestHeader = New-Object 'System.Collections.Generic.Dictionary[string, string]' $GraphRequestHeader.Add("ConsistencyLevel", "eventual") $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "GroupsWithLicenses" method = "GET" headers = $GraphRequestHeader url = "/groups?`$filter=assignedLicenses/`$count ne 0&`$count=true&`$select=id,displayName,assignedLicenses&`$top=999" } [void]$graphRequests.Add($gRequestTmp) $BatchResponse = Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -IncludeBody -Activity "Get-UcM365LicenseAssignment, Step 1: Getting Tenant License details" $tmpGraphResponse = $BatchResponse | Where-Object { $_.id -eq ("TenantSKUs") } if ($tmpGraphResponse.status -eq 200) { $TenantSKUs = $tmpGraphResponse.body.value } $tmpGraphResponse = $BatchResponse | Where-Object { $_.id -eq ("GroupsWithLicenses") } if ($tmpGraphResponse.status -eq 200) { $GroupsWithLicenses = $tmpGraphResponse.body.value } #endregion #region 20232019 - Adding filter to SKU if ($SKU) { if ($UseFriendlyNames) { #20241022 - Change to allow to search using SKU parameter instead of exact match. $SKUGUID = ($SKUnSP | Where-Object { $_.String_Id -match $SKU -or $_.Product_Display_Name -match $SKU } | Sort-Object GUID -Unique ).GUID $TenantSKUs = $TenantSKUs | Where-Object { $_.skuId -in $SKUGUID -or $_.skuPartNumber -match $SKU} if ($TenantSKUs.count -eq 0) { Write-Warning "Could not find `"$SKU`" (SKU Name/Part Number) subscription associated with the tenant." return } } else { #20241022 - Change to allow to search using SKU parameter instead of exact match. $TenantSKUs = $TenantSKUs | Where-Object { $_.skuPartNumber -match $SKU } if ($TenantSKUs.count -eq 0) { Write-Warning "Could not find `"$SKU`" (SKU Part Number) subscription associated with the tenant." return } } } else { $TenantSKUs = $TenantSKUs | Where-Object -Property consumedUnits -GT -Value 0 | Sort-Object skuPartNumber } #endregion #region 20131019 - Getting all Service Plans for new matrix style report $allServicePlans = [System.Collections.ArrayList]::new() foreach ($TenantSKU in $TenantSKUs) { $tmpUserServicePlans = $TenantSKU.ServicePlans | Where-Object -Property appliesTo -EQ -Value "User" foreach ($ServicePlan in $tmpUserServicePlans) { if (!($ServicePlan.ServicePlanId -in $allServicePlans.ServicePlanId)) { if ($UseFriendlyNames) { $servicePlanName = ($SKUnSP | Where-Object { $_.Service_Plan_Id -eq $ServicePlan.ServicePlanId -and $_.GUID -eq $TenantSKU.skuID } | Sort-Object Service_Plans_Included_Friendly_Names -Unique).Service_Plans_Included_Friendly_Names if ([string]::IsNullOrEmpty($servicePlanName)) { $servicePlanName = $ServicePlan.servicePlanName } } else { $servicePlanName = $ServicePlan.ServicePlanName } $tmpSP = New-Object -TypeName PSObject -Property @{ servicePlanId = $ServicePlan.ServicePlanId servicePlanName = $servicePlanName } [void]$allServicePlans.Add($tmpSP) } } } #Sorting service plans by name and creating the file header $allServicePlans = $allServicePlans | Sort-Object ServicePlanName $row = "UserPrincipalName,LicenseAssigned,LicenseAssignment,LicenseAssignmentGroup" if (!($SkipServicePlan)) { foreach ($ServicePlan in $allServicePlans) { $row += "," + $ServicePlan.servicePlanName } } $row += [Environment]::NewLine #endregion Write-Progress -Id 2 -Activity "Get-UcM365LicenseAssignment, Step 2: Reading users assigned licenses/service plans" if ($DuplicateServicePlansOnly) { #region 20240905 - Users with Duplicate Service Plans #We need to check license per user, this is slower than check per SKU like in Licensing Assignment but required in order to detect duplicates. $TotalUsers = 0 $usersProcessed = 0 $GraphNextPage = "https://graph.microsoft.com/v1.0/users?`$filter=assignedLicenses/`$count ne 0&`$count=true&`$select=userPrincipalName,licenseAssignmentStates&`$top=999" do { $GraphResponse = Invoke-MgGraphRequest -Method Get -Uri $GraphNextPage -Headers $GraphRequestHeader $GraphNextPage = $GraphResponse.'@odata.nextLink' $UsersWithLicenses = $GraphResponse.value if (![string]::IsNullOrEmpty($GraphResponse.'@odata.count')) { $TotalUsers = $GraphResponse.'@odata.count' } foreach ($LicensedUser in $UsersWithLicenses) { $usersProcessed++ #Update the status every 100 users if (($usersProcessed % 100 -eq 0) -or ($usersProcessed -eq $TotalUsers) -or ($usersProcessed -eq 1) ) { Write-Progress -Id 2 -Activity "Get-UcM365LicenseAssignment, Step 2: Reading users assigned licenses/service plans" -Status "$usersProcessed of $TotalUsers" } #We only need to process users that have 2 or more licenses assigned. if ($LicensedUser.licenseAssignmentStates.count -gt 1) { $tmpUserServicePlans = [System.Collections.ArrayList]::new() foreach ($licenseState in $LicensedUser.licenseAssignmentStates) { #If not a in the Tenant SKUs we can skip it if ($licenseState.skuId -in $TenantSKUs.skuId) { $tmpLicenseInfo = ($TenantSKUs | Where-Object { $_.skuId -eq $licenseState.skuId }) $LicenseDisplayName = $tmpLicenseInfo.skuPartNumber if ($UseFriendlyNames) { $LicenseDisplayName = ($SKUnSP | Where-Object { $_.GUID -eq $licenseState.skuId } | Sort-Object Product_Display_Name -Unique).Product_Display_Name } if ([string]::IsNullOrEmpty($LicenseDisplayName)) { $LicenseDisplayName = $licenseState.skuId } $licenseAssignment = "Direct" $licenseAssignmentGroup = "NA" if (!([string]::IsNullOrEmpty($licenseState.assignedByGroup))) { $licenseAssignment = "Inherited" $licenseAssignmentGroup = ($GroupsWithLicenses | Where-Object -Property "id" -EQ -Value $licenseState.assignedByGroup).displayName if ([string]::IsNullOrEmpty($licenseAssignmentGroup)) { $licenseAssignmentGroup = $licenseState.assignedByGroup } } $SKUUserServicePlans = $tmpLicenseInfo.servicePlans | Where-Object -Property appliesTo -EQ -Value "User" | Sort-Object servicePlanName foreach ($SKUUserServicePlan in $SKUUserServicePlans) { $SPStatus = "Off" if ($SKUUserServicePlan.servicePlanId -notin $licenseState.disabledPlans) { $SPStatus = "On" } $ObjUserServicePlans = [PSCustomObject]@{ LicenseSkuId = $licenseState.skuId LicenseDisplayName = $LicenseDisplayName LicenseAssignment = $licenseAssignment LicenseAssignmentGroup = $licenseAssignmentGroup ServicePlanId = $SKUUserServicePlan.servicePlanId ServicePlanName = $SKUUserServicePlan.servicePlanName Status = $SPStatus } [void]$tmpUserServicePlans.Add($ObjUserServicePlans) } } } # Checking if we have more then one Service Plan # In the future we can add filters, like only if both are ON or Ignore Direct/Inherited $skuWithDupServicePlans = $tmpUserServicePlans | Group-Object -Property ServicePlanId | Where-Object { $_.Count -gt 1 } | Select-Object -ExpandProperty Group | Select-Object LicenseSkuId, LicenseDisplayName, LicenseAssignment, LicenseAssignmentGroup | Sort-Object -Property LicenseDisplayName, LicenseAssignment, LicenseAssignmentGroup -Unique if (($skuWithDupServicePlans.Count -gt 0)) { foreach ($UserLicenseState in $skuWithDupServicePlans) { foreach ($ServicePlan in $allServicePlans) { $tmpSPStatus = $tmpUserServicePlans | Where-Object { $UserLicenseState.LicenseSkuId -eq $_.LicenseSkuId -and $UserLicenseState.LicenseAssignment -eq $_.LicenseAssignment -and $UserLicenseState.LicenseAssignmentGroup -eq $_.LicenseAssignmentGroup -and $_.servicePlanId -eq $servicePlan.servicePlanId } if ($tmpSPStatus.Status -in ("On", "Off")) { $userServicePlans += "," + $tmpSPStatus.Status } else { $userServicePlans += "," } } $row += $LicensedUser.userPrincipalName + "," + $UserLicenseState.LicenseDisplayName + "," + $UserLicenseState.LicenseAssignment + "," + $UserLicenseState.LicenseAssignmentGroup + $userServicePlans Out-File -FilePath $OutputFilePath -InputObject $row -Encoding UTF8 -append $row = "" $userServicePlans = "" } } } } } while (!([string]::IsNullOrEmpty($GraphNextPage))) #endregion } else { #region License Assignment foreach ($TenantSKU in $TenantSKUs) { $LicenseDisplayName = $TenantSKU.skuPartNumber if ($UseFriendlyNames) { $tmpFriendlyName = ($SKUnSP | Where-Object { $_.GUID -eq $TenantSKU.skuID } | Sort-Object Product_Display_Name -Unique).Product_Display_Name #20241022 - To prevent empty name when a license exists in the tenant but the data is not available in "Products names and Services Identifiers" file. if ($tmpFriendlyName) { $LicenseDisplayName = $tmpFriendlyName } } $SKUUserServicePlans = $TenantSKU.servicePlans | Where-Object -Property appliesTo -EQ -Value "User" | Sort-Object servicePlanName $usersProcessed = 0 $GraphRequestURI = "https://graph.microsoft.com/v1.0/users?`$filter=assignedLicenses/any(u:u/skuId eq " + $TenantSKU.skuId + " )&`$select=userPrincipalName,licenseAssignmentStates&`$orderby=userPrincipalName&`$count=true&`$top=999" do { try { $UsersWithLicenses = Invoke-MgGraphRequest -Method Get -Uri $GraphRequestURI -Headers $GraphRequestHeader if (![string]::IsNullOrEmpty($UsersWithLicenses.'@odata.count')) { $TotalUsers = $UsersWithLicenses.'@odata.count' } $GraphRequestURI = $UsersWithLicenses.'@odata.nextLink' foreach ($UserWithLicense in $UsersWithLicenses.value) { if (($usersProcessed % 1000 -eq 0) -or ($usersProcessed -eq $TotalUsers)) { Write-Progress -ParentId 2 -Activity "Checking license assignments for $LicenseDisplayName" -Status "$usersProcessed of $TotalUsers" } $tmpLicenseAssignmentStates = $UserWithLicense.licenseAssignmentStates | Where-Object -Property skuId -EQ -Value $TenantSKU.skuId | Sort-Object assignedByGroup foreach ($licenseState in $tmpLicenseAssignmentStates) { $licenseAssignment = "Direct" $licenseAssignmentGroup = "" if (!([string]::IsNullOrEmpty($licenseState.assignedByGroup))) { $licenseAssignment = "Inherited" $licenseAssignmentGroup = ($GroupsWithLicenses | Where-Object -Property "id" -EQ -Value $licenseState.assignedByGroup).displayName if ([string]::IsNullOrEmpty($licenseAssignmentGroup)) { $licenseAssignmentGroup = $licenseState.assignedByGroup } } $userServicePlans = "" if (!($SkipServicePlan)) { foreach ($ServicePlan in $allServicePlans) { if ($servicePlan.servicePlanId -in $SKUUserServicePlans.servicePlanId) { if ($servicePlan.servicePlanId -notin $licenseState.disabledPlans) { $userServicePlans += ",On" } else { $userServicePlans += ",Off" } } else { $userServicePlans += "," } } } $row += $UserWithLicense.userPrincipalName + "," + $LicenseDisplayName + "," + $LicenseAssignment + "," + $LicenseAssignmentGroup + $userServicePlans Out-File -FilePath $OutputFilePath -InputObject $row -Encoding UTF8 -append $row = "" } $usersProcessed++ } } catch { Write-Warning ("Failed to get Users with assigned SKU Id: " + $TenantSKU.skuID) $GraphRequestURI = "" } } while (![string]::IsNullOrEmpty($GraphRequestURI)) } #endregion } if ($usersProcessed -gt 0) { Write-Host ("Results available in " + $OutputFilePath) -ForegroundColor Cyan #region 20231019 - Change: Added execution time to the output. $endTime = Get-Date $totalSeconds = [math]::round(($endTime - $startTime).TotalSeconds, 2) $totalTime = New-TimeSpan -Seconds $totalSeconds Write-Host "Execution time:" $totalTime.Hours "Hours" $totalTime.Minutes "Minutes" $totalTime.Seconds "Seconds" -ForegroundColor Green #endregion } } } #EndRegion '.\Public\Export-UcM365LicenseAssignment.ps1' 347 #Region '.\Public\Export-UcOneDriveWithMultiplePermissions.ps1' -1 function Export-UcOneDriveWithMultiplePermissions { param( [string]$OutputPath, [switch]$MultiGeo ) <# .SYNOPSIS Generate a report with OneDrive's that have more than a user with access permissions. .DESCRIPTION This script will check all OneDrives and return the OneDrive that have additional users with permissions. Author: David Paulino Requirements: Microsoft Graph Authentication PowerShell Module (Microsoft.Graph.Authentication) Microsoft Graph Scopes: "Sites.Read.All" Note: Currently the SharePoint Sites requires to authenticate to Graph API with AppOnly https://learn.microsoft.com/graph/auth/auth-concepts .PARAMETER OutputPath Allows to specify the path where we want to save the results. By default, it will save on current user Download. .PARAMETER MultiGeo Required if Tenant is MultiGeo .EXAMPLE PS> Get-UcOneDriveWithMultiplePermissions .EXAMPLE PS> Get-UcOneDriveWithMultiplePermissions -MultiGeo #> $startTime = Get-Date if ((Test-UcMgGraphConnection -Scopes "Sites.Read.All" -AltScopes ("Sites.ReadWrite.All") -AuthType "AppOnly" )) { Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null #Verify if the Output Path exists if ($OutputPath) { if (!(Test-Path $OutputPath -PathType Container)) { Write-Host ("Error: Invalid folder " + $OutputPath) -ForegroundColor Red return } $OutputFilePath = [System.IO.Path]::Combine($OutputPath, $outFile) } else { $OutputFilePath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads", $outFile) } #Graph API request is different when the tenant has multigeo if ($MultiGeo) { $outFile = "OneDrivePermissions_MultiGeo_" + (Get-Date).ToString('yyyyMMdd-HHmmss') + ".csv" $GraphRequestSites = "https://graph.microsoft.com/v1.0/sites/getAllSites?`$select=id,displayName,isPersonalSite,WebUrl&`$top=999" } else { $outFile = "OneDrivePermissions_" + (Get-Date).ToString('yyyyMMdd-HHmmss') + ".csv" $GraphRequestSites = "https://graph.microsoft.com/v1.0/sites?`$select=id,displayName,isPersonalSite,WebUrl&`$top=999" } $OneDriveProcessed = 0 $OneDriveFound = 0 $BatchNumber = 1 $row = "OneDriveDisplayName,OneDriveUrl,Role,UserWithAccessDisplayName,UserWithAccessUPN,UserWithAccessSharePointLogin,OneDriveID,PermissionID" + [Environment]::NewLine do { try { $ResponseSites = Invoke-MgGraphRequest -Method Get -Uri $GraphRequestSites $GraphRequestSites = $ResponseSites.'@odata.nextLink' #Currently the SharePoint API doenst support filter for isPersonalSite, so we need to filter it $tempOneDrives = $ResponseSites.value | Where-Object { $_.isPersonalSite -eq $true } #Adding a progress messsage to show status foreach ($OneDrive in $tempOneDrives) { if ($OneDriveProcessed % 10 -eq 0) { Write-Progress -Activity "Looking for addtional users in OneDrive permissions" -Status "Batch #$BatchNumber - Number of OneDrives Processed $OneDriveProcessed" } $GROneDrivePermission = "https://graph.microsoft.com/v1.0/sites/" + $OneDrive.id + "/drive/root/permissions" try { $OneDrivePermissions = (Invoke-MgGraphRequest -Method Get -Uri $GROneDrivePermission).value if ($OneDrivePermissions.count -gt 1) { foreach ($OneDrivePermission in $OneDrivePermissions) { if ($OneDrivePermission.grantedToV2.siteuser.displayName -ne $OneDrive.displayName) { $tempUPN = Get-UcUPNFromString $OneDrivePermission.grantedToV2.siteuser.loginName $row += $OneDrive.displayName + "," + $OneDrive.WebUrl + "," + $OneDrivePermission.roles + ",`"" + $OneDrivePermission.grantedToV2.siteuser.displayName + "`"," + $tempUPN + "," + $OneDrivePermission.grantedToV2.siteuser.loginName + ",`"" + $OneDrive.id + "`"," + $OneDrivePermission.id Out-File -FilePath $OutputFilePath -InputObject $row -Encoding UTF8 -append $row = "" $OneDriveFound++ } } } $OneDriveProcessed++ } catch { } } $BatchNumber++ } catch { break } } while (![string]::IsNullOrEmpty($GraphRequestSites)) $endTime = Get-Date $totalSeconds = [math]::round(($endTime - $startTime).TotalSeconds, 2) $totalTime = New-TimeSpan -Seconds $totalSeconds Write-Host "Total of OneDrives processed: $OneDriveProcessed, total OneDrives with additional users with permissions: $OneDriveFound" -ForegroundColor Cyan if ($OneDriveFound -gt 0) { Write-Host ("Results available in " + $OutputFilePath) -ForegroundColor Cyan } Write-Host "Execution time:" $totalTime.Hours "Hours" $totalTime.Minutes "Minutes" $totalTime.Seconds "Seconds" -ForegroundColor Green } } #EndRegion '.\Public\Export-UcOneDriveWithMultiplePermissions.ps1' 104 #Region '.\Public\Get-UcArch.ps1' -1 function Get-UcArch { param( [string]$FilePath ) <# .SYNOPSIS Funcion to get the Architecture from .exe file .DESCRIPTION Based on PowerShell script Get-ExecutableType.ps1 by David Wyatt, please check the complete script in: Identify 16-bit, 32-bit and 64-bit executables with PowerShell https://gallery.technet.microsoft.com/scriptcenter/Identify-16-bit-32-bit-and-522eae75 .PARAMETER FilePath Specifies the executable full file path. .EXAMPLE PS> Get-UcArch -FilePath C:\temp\example.exe #> try { $stream = New-Object System.IO.FileStream( $FilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::Read ) $exeType = 'Unknown' $bytes = New-Object byte[](4) if ($stream.Seek(0x3C, [System.IO.SeekOrigin]::Begin) -eq 0x3C -and $stream.Read($bytes, 0, 4) -eq 4) { if (-not [System.BitConverter]::IsLittleEndian) { [Array]::Reverse($bytes, 0, 4) } $peHeaderOffset = [System.BitConverter]::ToUInt32($bytes, 0) if ($stream.Length -ge $peHeaderOffset + 6 -and $stream.Seek($peHeaderOffset, [System.IO.SeekOrigin]::Begin) -eq $peHeaderOffset -and $stream.Read($bytes, 0, 4) -eq 4 -and $bytes[0] -eq 0x50 -and $bytes[1] -eq 0x45 -and $bytes[2] -eq 0 -and $bytes[3] -eq 0) { $exeType = 'Unknown' if ($stream.Read($bytes, 0, 2) -eq 2) { if (-not [System.BitConverter]::IsLittleEndian) { [Array]::Reverse($bytes, 0, 2) } $machineType = [System.BitConverter]::ToUInt16($bytes, 0) switch ($machineType) { 0x014C { $exeType = 'x86' } 0x8664 { $exeType = 'x64' } } } } } return $exeType } catch { return "Unknown" } finally { if ($null -ne $stream) { $stream.Dispose() } } } #EndRegion '.\Public\Get-UcArch.ps1' 57 #Region '.\Public\Get-UcM365Domains.ps1' -1 function Get-UcM365Domains { param( [Parameter(Mandatory = $true)] [string]$Domain ) <# .SYNOPSIS Get Microsoft 365 Domains from a Tenant .DESCRIPTION This function returns a list of domains that are associated with a Microsoft 365 Tenant. .PARAMETER Domain Specifies a domain registered with Microsoft 365 .EXAMPLE PS> Get-UcM365Domains -Domain uclobby.com #> $regex = "^(.*@)(.*[.].*)$" $outDomains = [System.Collections.ArrayList]::new() try { Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null $AllowedAudiences = Invoke-WebRequest -Uri ("https://accounts.accesscontrol.windows.net/" + $Domain + "/metadata/json/1") -UseBasicParsing | ConvertFrom-Json | Select-Object -ExpandProperty allowedAudiences } catch [System.Net.WebException] { if ($PSItem.Exception.Message -eq "The remote server returned an error: (400) Bad Request.") { Write-Warning "The domain $Domain is not part of a Microsoft 365 Tenant." } else { Write-Warning $PSItem.Exception.Message } } catch { #20240318 - Support for GCC High tenants. try { $AllowedAudiences = Invoke-WebRequest -Uri ("https://login.microsoftonline.us/" + $Domain + "/metadata/json/1") -UseBasicParsing | ConvertFrom-Json | Select-Object -ExpandProperty allowedAudiences } catch { Write-Warning "Unknown error while checking domain: $Domain" } } try { foreach ($AllowedAudience in $AllowedAudiences) { $temp = [regex]::Match($AllowedAudience , $regex).captures.groups if ($temp.count -ge 2) { $tempObj = New-Object -TypeName PSObject -Property @{ Name = $temp[2].value } $outDomains.Add($tempObj) | Out-Null } } } catch { Write-Warning "Unknown error while checking domain: $Domain" } return $outDomains } #EndRegion '.\Public\Get-UcM365Domains.ps1' 58 #Region '.\Public\Get-UcM365LicenseAssignment.ps1' -1 function Get-UcM365LicenseAssignment { param( [string]$SKU, [switch]$UseFriendlyNames, [switch]$SkipServicePlan, [string]$OutputPath, [switch]$DuplicateServicePlansOnly ) Write-Warning "Get-UcM365LicenseAssignment will be deprecated in a future release, please use the Export-UcM365LicenseAssignment instead." Export-UcM365LicenseAssignment @params } #EndRegion '.\Public\Get-UcM365LicenseAssignment.ps1' 12 #Region '.\Public\Get-UcM365TenantId.ps1' -1 function Get-UcM365TenantId { param( [Parameter(Mandatory = $true)] [string]$Domain ) <# .SYNOPSIS Get Microsoft 365 Tenant Id .DESCRIPTION This function returns the Tenant ID associated with a domain that is part of a Microsoft 365 Tenant. .PARAMETER Domain Specifies a domain registered with Microsoft 365 .EXAMPLE PS> Get-UcM365TenantId -Domain uclobby.com #> $regexTenantID = "^(.*@)(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})$" $regexOnMicrosoftDomain = "^(.*@)(?!.*mail)(.*.onmicrosoft.com)$" try { Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null $AllowedAudiences = Invoke-WebRequest -Uri ("https://accounts.accesscontrol.windows.net/" + $Domain + "/metadata/json/1") -UseBasicParsing | ConvertFrom-Json | Select-Object -ExpandProperty allowedAudiences } catch [System.Net.Http.HttpRequestException] { if ($PSItem.Exception.Response.StatusCode -eq "BadRequest") { Write-Error "The domain $Domain is not part of a Microsoft 365 Tenant." } else { Write-Error $PSItem.Exception.Message } } catch { Write-Error "Unknown error while checking domain: $Domain" } $output = [System.Collections.ArrayList]::new() $OnMicrosoftDomains = [System.Collections.ArrayList]::new() $TenantID = "" foreach ($AllowedAudience in $AllowedAudiences) { $tempTID = [regex]::Match($AllowedAudience , $regexTenantID).captures.groups $tempID = [regex]::Match($AllowedAudience , $regexOnMicrosoftDomain).captures.groups if ($tempTID.count -ge 2) { $TenantID = $tempTID[2].value } if ($tempID.count -ge 2) { [void]$OnMicrosoftDomains.Add($tempID[2].value) } } #Multi Geo will have multiple OnMicrosoft Domains foreach ($OnMicrosoftDomain in $OnMicrosoftDomains) { if ($TenantID -and $OnMicrosoftDomain) { $M365TidPSObj = [PSCustomObject]@{ TenantID = $TenantID OnMicrosoftDomain = $OnMicrosoftDomain } $M365TidPSObj.PSObject.TypeNames.Insert(0, 'M365TenantId') [void]$output.Add($M365TidPSObj) } } return $output } #EndRegion '.\Public\Get-UcM365TenantId.ps1' 63 #Region '.\Public\Get-UcOneDriveWithMultiplePermissions.ps1' -1 function Get-UcOneDriveWithMultiplePermissions { param( [string]$OutputPath, [switch]$MultiGeo ) Write-Warning "Get-UcOneDriveWithMultiplePermissions will be deprecated in a future release, please use the Export-UcOneDriveWithMultiplePermissions instead." Export-UcOneDriveWithMultiplePermissions @params } #EndRegion '.\Public\Get-UcOneDriveWithMultiplePermissions.ps1' 9 #Region '.\Public\Get-UcTeamsDevice.ps1' -1 function Get-UcTeamsDevice { param( [ValidateSet("Phone", "MTR", "MTRA", "MTRW", "SurfaceHub", "Display", "Panel", "SIPPhone")] [string]$Filter, [switch]$Detailed, [switch]$ExportCSV, [string]$OutputPath ) <# .SYNOPSIS Get Microsoft Teams Devices information .DESCRIPTION This function fetch Teams Devices provisioned in a M365 Tenant using MS Graph. Contributors: David Paulino, Silvio Schanz, Gonçalo Sepulveda, Bryan Kendrick and Daniel Jelinek Requirements: Microsoft Graph PowerShell Module (Install-Module Microsoft.Graph) Microsoft Graph Scopes: "TeamworkDevice.Read.All" "User.Read.All" .PARAMETER Filter Specifies a filter, valid options: Phone - Teams Native Phones MTR - Microsoft Teams Rooms running Windows or Android MTRW - Microsoft Teams Room Running Windows MTRA - Microsoft Teams Room Running Android SurfaceHub - Surface Hub Display - Microsoft Teams Displays Panel - Microsoft Teams Panels .PARAMETER Detailed When present it will get detailed information from Teams Devices .PARAMETER ExportCSV When present will export the detailed results to a CSV file. By defautl will save the file under the current user downloads, unless we specify the OutputPath. .PARAMETER OutputPath Allows to specify the path where we want to save the results. .EXAMPLE PS> Get-UcTeamsDevice .EXAMPLE PS> Get-UcTeamsDevice -Filter MTR .EXAMPLE PS> Get-UcTeamsDevice -Detailed #> $outTeamsDevices = [System.Collections.ArrayList]::new() if ($ExportCSV) { $Detailed = $true } #Verify if the Output Path exists if ($OutputPath) { if (!(Test-Path $OutputPath -PathType Container)) { Write-Host ("Error: Invalid folder " + $OutputPath) -ForegroundColor Red return } } else { $OutputPath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads") } if (Test-UcMgGraphConnection -Scopes "TeamworkDevice.Read.All,User.Read.All" -AltScopes ("TeamworkDevice.Read.All","Directory.Read.All")) { Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null $graphRequests = [System.Collections.ArrayList]::new() $tmpFileName = "MSTeamsDevices_" + $Filter + "_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv" switch ($filter) { "Phone" { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "ipPhone" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'ipPhone'" } $graphRequests.Add($gRequestTmp) | Out-Null $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "lowCostPhone" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'lowCostPhone'" } $graphRequests.Add($gRequestTmp) | Out-Null } "MTR" { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "teamsRoom" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'teamsRoom'" } $graphRequests.Add($gRequestTmp) | Out-Null $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "collaborationBar" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'collaborationBar'" } $graphRequests.Add($gRequestTmp) | Out-Null $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "touchConsole" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'touchConsole'" } $graphRequests.Add($gRequestTmp) | Out-Null } "MTRW" { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "teamsRoom" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'teamsRoom'" } $graphRequests.Add($gRequestTmp) | Out-Null } "MTRA" { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "collaborationBar" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'collaborationBar'" } $graphRequests.Add($gRequestTmp) | Out-Null $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "touchConsole" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'touchConsole'" } $graphRequests.Add($gRequestTmp) | Out-Null } "SurfaceHub" { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "surfaceHub" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'surfaceHub'" } $graphRequests.Add($gRequestTmp) | Out-Null } "Display" { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "teamsDisplay" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'teamsDisplay'" } $graphRequests.Add($gRequestTmp) | Out-Null } "Panel" { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "teamsPanel" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'teamsPanel'" } $graphRequests.Add($gRequestTmp) | Out-Null } "SIPPhone" { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "sip" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'sip'" } $graphRequests.Add($gRequestTmp) | Out-Null } Default { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = 1 method = "GET" url = "/teamwork/devices" } $graphRequests.Add($gRequestTmp) | Out-Null $tmpFileName = "MSTeamsDevices_All_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv" } } $TeamsDeviceList = (Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Get-UcTeamsDevice, getting Teams device info").value #To improve performance we will use batch requests $graphRequests = [System.Collections.ArrayList]::new() foreach ($TeamsDevice in $TeamsDeviceList) { if (($graphRequests.id -notcontains $TeamsDevice.currentuser.id) -and !([string]::IsNullOrEmpty($TeamsDevice.currentuser.id))) { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = $TeamsDevice.currentuser.id method = "GET" url = "/users/" + $TeamsDevice.currentuser.id } $graphRequests.Add($gRequestTmp) | Out-Null } if ($Detailed) { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = $TeamsDevice.id + "-activity" method = "GET" url = "/teamwork/devices/" + $TeamsDevice.id + "/activity" } $graphRequests.Add($gRequestTmp) | Out-Null $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = $TeamsDevice.id + "-configuration" method = "GET" url = "/teamwork/devices/" + $TeamsDevice.id + "/configuration" } $graphRequests.Add($gRequestTmp) | Out-Null $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = $TeamsDevice.id + "-health" method = "GET" url = "/teamwork/devices/" + $TeamsDevice.id + "/health" } $graphRequests.Add($gRequestTmp) | Out-Null $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = $TeamsDevice.id + "-operations" method = "GET" url = "/teamwork/devices/" + $TeamsDevice.id + "/operations" } $graphRequests.Add($gRequestTmp) | Out-Null } } if ($graphRequests.Count -gt 0) { if ($Detailed) { $ActivityInfo = "Get-UcTeamsDevice, getting Teams device addtional information (User UPN/Health/Operations/Configurarion)." } else { $ActivityInfo = "Get-UcTeamsDevice, getting Teams device user information." } $graphResponseExtra = (Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity $ActivityInfo -IncludeBody) } $devicesProcessed = 0 foreach ($TeamsDevice in $TeamsDeviceList) { $devicesProcessed++ $userUPN = ($graphResponseExtra | Where-Object { $_.id -eq $TeamsDevice.currentuser.id }).body.userPrincipalName if ($Detailed) { $TeamsDeviceActivity = ($graphResponseExtra | Where-Object { $_.id -eq ($TeamsDevice.id + "-activity") }).body $TeamsDeviceConfiguration = ($graphResponseExtra | Where-Object { $_.id -eq ($TeamsDevice.id + "-configuration") }).body $TeamsDeviceHealth = ($graphResponseExtra | Where-Object { $_.id -eq ($TeamsDevice.id + "-health") }).body $TeamsDeviceOperations = ($graphResponseExtra | Where-Object { $_.id -eq ($TeamsDevice.id + "-operations") }).body.value if ($TeamsDeviceOperations.count -gt 0) { $LastHistoryAction = $TeamsDeviceOperations[0].operationType $LastHistoryStatus = $TeamsDeviceOperations[0].status $LastHistoryInitiatedBy = $TeamsDeviceOperations[0].createdBy.user.displayName $LastHistoryModifiedDate = ($TeamsDeviceOperations[0].lastActionDateTime).ToLocalTime() $LastHistoryErrorCode = $TeamsDeviceOperations[0].error.code $LastHistoryErrorMessage = $TeamsDeviceOperations[0].error.message } else { $LastHistoryAction = "" $LastHistoryStatus = "" $LastHistoryInitiatedBy = "" $LastHistoryModifiedDate = "" $LastHistoryErrorCode = "" $LastHistoryErrorMessage = "" } $outMacAddress = "" foreach ($macAddress in $TeamsDevice.hardwaredetail.macAddresses) { $outMacAddress += $macAddress + ";" } $TDObj = New-Object -TypeName PSObject -Property @{ UserDisplayName = $TeamsDevice.currentuser.displayName UserUPN = $userUPN TACDeviceID = $TeamsDevice.id DeviceType = Convert-UcTeamsDeviceType $TeamsDevice.deviceType Notes = $TeamsDevice.notes CompanyAssetTag = $TeamsDevice.companyAssetTag Manufacturer = $TeamsDevice.hardwaredetail.manufacturer Model = $TeamsDevice.hardwaredetail.model SerialNumber = $TeamsDevice.hardwaredetail.serialNumber MacAddresses = $outMacAddress DeviceHealth = $TeamsDevice.healthStatus WhenCreated = ($TeamsDevice.createdDateTime).ToLocalTime() WhenChanged = ($TeamsDevice.lastModifiedDateTime).ToLocalTime() ChangedByUser = $TeamsDevice.lastModifiedBy.user.displayName #Activity ActivePeripherals = $TeamsDeviceActivity.activePeripherals #Configuration ConfigurationCreateDate = ($TeamsDeviceConfiguration.createdDateTime).ToLocalTime() ConfigurationCreatedBy = $TeamsDeviceConfiguration.createdBy ConfigurationLastModifiedDate = ($TeamsDeviceConfiguration.lastModifiedDateTime).ToLocalTime() ConfigurationLastModifiedBy = $TeamsDeviceConfiguration.lastModifiedBy DisplayConfiguration = $TeamsDeviceConfiguration.displayConfiguration CameraConfiguration = $TeamsDeviceConfiguration.cameraConfiguration.contentCameraConfiguration SpeakerConfiguration = $TeamsDeviceConfiguration.speakerConfiguration MicrophoneConfiguration = $TeamsDeviceConfiguration.microphoneConfiguration TeamsClientConfiguration = $TeamsDeviceConfiguration.teamsClientConfiguration SupportedMeetingMode = $TeamsDeviceConfiguration.teamsClientConfiguration.accountConfiguration.supportedClient HardwareProcessor = $TeamsDeviceConfiguration.hardwareConfiguration.processorModel SystemConfiguration = $TeamsDeviceConfiguration.systemConfiguration #Health #20240417 - Added connection fields ConnectionStatus = $TeamsDeviceHealth.connection.connectionStatus ConnectionLastActivity = ($TeamsDeviceHealth.connection.lastModifiedDateTime).ToLocalTime() ComputeStatus = $TeamsDeviceHealth.hardwareHealth.computeHealth.connection.connectionStatus HdmiIngestStatus = $TeamsDeviceHealth.hardwareHealth.hdmiIngestHealth.connection.connectionStatus RoomCameraStatus = $TeamsDeviceHealth.peripheralsHealth.roomCameraHealth.connection.connectionStatus ContentCameraStatus = $TeamsDeviceHealth.peripheralsHealth.contentCameraHealth.connection.connectionStatus SpeakerStatus = $TeamsDeviceHealth.peripheralsHealth.speakerHealth.connection.connectionStatus CommunicationSpeakerStatus = $TeamsDeviceHealth.peripheralsHealth.communicationSpeakerHealth.connection.connectionStatus #DisplayCollection = $TeamsDeviceHealth.peripheralsHealth.displayHealthCollection.connectionStatus MicrophoneStatus = $TeamsDeviceHealth.peripheralsHealth.microphoneHealth.connection.connectionStatus TeamsAdminAgentVersion = $TeamsDeviceHealth.softwareUpdateHealth.adminAgentSoftwareUpdateStatus.currentVersion FirmwareVersion = $TeamsDeviceHealth.softwareUpdateHealth.firmwareSoftwareUpdateStatus.currentVersion CompanyPortalVersion = $TeamsDeviceHealth.softwareUpdateHealth.companyPortalSoftwareUpdateStatus.currentVersion OEMAgentAppVersion = $TeamsDeviceHealth.softwareUpdateHealth.partnerAgentSoftwareUpdateStatus.currentVersion TeamsAppVersion = $TeamsDeviceHealth.softwareUpdateHealth.teamsClientSoftwareUpdateStatus.currentVersion #LastOperation LastHistoryAction = $LastHistoryAction LastHistoryStatus = $LastHistoryStatus LastHistoryInitiatedBy = $LastHistoryInitiatedBy LastHistoryModifiedDate = $LastHistoryModifiedDate LastHistoryErrorCode = $LastHistoryErrorCode LastHistoryErrorMessage = $LastHistoryErrorMessage } $TDObj.PSObject.TypeNames.Insert(0, 'TeamsDevice') } else { $TDObj = New-Object -TypeName PSObject -Property @{ UserDisplayName = $TeamsDevice.currentuser.displayName UserUPN = $userUPN TACDeviceID = $TeamsDevice.id DeviceType = Convert-UcTeamsDeviceType $TeamsDevice.deviceType Manufacturer = $TeamsDevice.hardwaredetail.manufacturer Model = $TeamsDevice.hardwaredetail.model SerialNumber = $TeamsDevice.hardwaredetail.serialNumber MacAddresses = $TeamsDevice.hardwaredetail.macAddresses DeviceHealth = $TeamsDevice.healthStatus #20240419 - Adding additional fields that are available on graph api WhenCreated = ($TeamsDevice.createdDateTime).ToLocalTime() WhenChanged = ($TeamsDevice.lastModifiedDateTime).ToLocalTime() ChangedByUser = $TeamsDevice.lastModifiedBy.user.displayName } $TDObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceList') } $outTeamsDevices.Add($TDObj) | Out-Null } #20231020 - We only need to output if we have results. if ($devicesProcessed -gt 0) { #region: Modified by Daniel Jelinek if ($ExportCSV) { $OutputFullPath = [System.IO.Path]::Combine($OutputPath, $tmpFileName) $outTeamsDevices | Sort-Object DeviceType, Manufacturer, Model | Select-Object TACDeviceID, DeviceType, Manufacturer, Model, UserDisplayName, UserUPN, Notes, CompanyAssetTag, SerialNumber, MacAddresses, WhenCreated, WhenChanged, ChangedByUser, HdmiIngestStatus, ComputeStatus, RoomCameraStatus, SpeakerStatus, CommunicationSpeakerStatus, MicrophoneStatus, SupportedMeetingMode, HardwareProcessor, SystemConfiguratio, TeamsAdminAgentVersion, FirmwareVersion, CompanyPortalVersion, OEMAgentAppVersion, TeamsAppVersion, LastUpdate, LastHistoryAction, LastHistoryStatus, LastHistoryInitiatedBy, LastHistoryModifiedDate, LastHistoryErrorCode, LastHistoryErrorMessage | Export-Csv -path $OutputFullPath -NoTypeInformation Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan } else { $outTeamsDevices | Sort-Object DeviceType, Manufacturer, Model } #endregion } } } #EndRegion '.\Public\Get-UcTeamsDevice.ps1' 362 #Region '.\Public\Get-UcTeamsVersion.ps1' -1 function Get-UcTeamsVersion { param( [string]$Path, [string]$Computer, [System.Management.Automation.PSCredential]$Credential, [switch]$SkipModuleCheck ) <# .SYNOPSIS Get Microsoft Teams Desktop Version .DESCRIPTION This function returns the installed Microsoft Teams desktop version for each user profile. .PARAMETER Path Specify the path with Teams Log Files .PARAMETER Computer Specify the remote computer .PARAMETER Credential Specify the credential to be used to connect to the remote computer .EXAMPLE PS> Get-UcTeamsVersion .EXAMPLE PS> Get-UcTeamsVersion -Path C:\Temp\ .EXAMPLE PS> Get-UcTeamsVersion -Computer workstation124 .EXAMPLE PS> $cred = Get-Credential PS> Get-UcTeamsVersion -Computer workstation124 -Credential $cred #> $regexVersion = '("version":")([0-9.]*)' $regexRing = '("ring":")(\w*)' $regexEnv = '("environment":")(\w*)' $regexCloudEnv = '("cloudEnvironment":")(\w*)' $regexWindowsUser = '("upnWindowUserUpn":")([a-zA-Z0-9@._-]*)' $regexTeamsUserName = '("userName":")([a-zA-Z0-9@._-]*)' #20240309 - REGEX to get New Teams version from log file DesktopApp: Version: 23202.1500.2257.3700 $regexNewVersion = '(DesktopApp: Version: )(\d{5}.\d{4}.\d{4}.\d{4})' $outTeamsVersion = [System.Collections.ArrayList]::new() if (!$SkipModuleCheck) { Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null } if ($Path) { if (Test-Path $Path -ErrorAction SilentlyContinue) { #region Teams Classic Path $TeamsSettingsFiles = Get-ChildItem -Path $Path -Include "settings.json" -Recurse foreach ($TeamsSettingsFile in $TeamsSettingsFiles) { $TeamsSettings = Get-Content -Path $TeamsSettingsFile.FullName $Version = "" $Ring = "" $Env = "" $CloudEnv = "" try { $VersionTemp = [regex]::Match($TeamsSettings, $regexVersion).captures.groups if ($VersionTemp.Count -ge 2) { $Version = $VersionTemp[2].value } $RingTemp = [regex]::Match($TeamsSettings, $regexRing).captures.groups if ($RingTemp.Count -ge 2) { $Ring = $RingTemp[2].value } $EnvTemp = [regex]::Match($TeamsSettings, $regexEnv).captures.groups if ($EnvTemp.Count -ge 2) { $Env = $EnvTemp[2].value } $CloudEnvTemp = [regex]::Match($TeamsSettings, $regexCloudEnv).captures.groups if ($CloudEnvTemp.Count -ge 2) { $CloudEnv = $CloudEnvTemp[2].value } } catch { } $TeamsDesktopSettingsFile = $TeamsSettingsFile.Directory.FullName + "\desktop-config.json" if (Test-Path $TeamsDesktopSettingsFile -ErrorAction SilentlyContinue) { $TeamsDesktopSettings = Get-Content -Path $TeamsDesktopSettingsFile $WindowsUser = "" $TeamsUserName = "" $RegexTemp = [regex]::Match($TeamsDesktopSettings, $regexWindowsUser).captures.groups if ($RegexTemp.Count -ge 2) { $WindowsUser = $RegexTemp[2].value } $RegexTemp = [regex]::Match($TeamsDesktopSettings, $regexTeamsUserName).captures.groups if ($RegexTemp.Count -ge 2) { $TeamsUserName = $RegexTemp[2].value } } $TeamsVersion = New-Object -TypeName PSObject -Property @{ WindowsUser = $WindowsUser TeamsUser = $TeamsUserName Type = "Teams Classic" Version = $Version Ring = $Ring Environment = $Env CloudEnvironment = $CloudEnv Path = $TeamsSettingsFile.Directory.FullName } $TeamsVersion.PSObject.TypeNames.Insert(0, 'TeamsVersionFromPath') $outTeamsVersion.Add($TeamsVersion) | Out-Null } #endregion #region New Teams Path $TeamsSettingsFiles = Get-ChildItem -Path $Path -Include "tma_settings.json" -Recurse foreach ($TeamsSettingsFile in $TeamsSettingsFiles) { if (Test-Path $TeamsSettingsFile -ErrorAction SilentlyContinue) { $NewTeamsSettings = Get-Content -Path $TeamsSettingsFile | ConvertFrom-Json $tmpAccountID = $NewTeamsSettings.primary_user.accounts.account_id try { $Version = "" $MostRecentTeamsLogFile = Get-ChildItem -Path $TeamsSettingsFile.Directory.FullName -Include "MSTeams_*.log" -Recurse | Sort-Object -Property CreationTime -Descending | Select-Object -First 1 $TeamLogContents = Get-Content $MostRecentTeamsLogFile $RegexTemp = [regex]::Match($TeamLogContents, $regexNewVersion).captures.groups if ($RegexTemp.Count -ge 2) { $Version = $RegexTemp[2].value } } catch {} $TeamsVersion = New-Object -TypeName PSObject -Property @{ WindowsUser = "NA" TeamsUser = $NewTeamsSettings.primary_user.accounts.account_upn Type = "New Teams" Version = $Version Ring = $NewTeamsSettings.tma_ecs_settings.$tmpAccountID.ring Environment = $NewTeamsSettings.tma_ecs_settings.$tmpAccountID.environment CloudEnvironment = $NewTeamsSettings.primary_user.accounts.cloud Path = $TeamsSettingsFile.Directory.FullName } $TeamsVersion.PSObject.TypeNames.Insert(0, 'TeamsVersionFromPath') [void]$outTeamsVersion.Add($TeamsVersion) } } #endregion } else { Write-Error -Message ("Invalid Path, please check if path: " + $path + " is correct and exists.") } } else { $currentDateFormat = [cultureinfo]::CurrentCulture.DateTimeFormat.ShortDatePattern if ($Computer) { $RemotePath = "\\" + $Computer + "\C$\Users" $ComputerName = $Computer if ($Credential) { if ($Computer.IndexOf('.') -gt 0) { $PSDriveName = $Computer.Substring(0, $Computer.IndexOf('.')) + "_TmpTeamsVersion" } else { $PSDriveName = $Computer + "_TmpTeamsVersion" } New-PSDrive -Root $RemotePath -Name $PSDriveName -PSProvider FileSystem -Credential $Credential | Out-Null } if (Test-Path -Path $RemotePath) { $Profiles = Get-ChildItem -Path $RemotePath -ErrorAction SilentlyContinue } else { Write-Error -Message ("Error: Cannot get users on " + $computer + ", please check if name is correct and if the current user has permissions.") } } else { $ComputerName = $Env:COMPUTERNAME $Profiles = Get-childItem 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' | ForEach-Object { Get-ItemProperty $_.pspath } | Where-Object { $_.fullprofile -eq 1 } } foreach ($UserProfile in $Profiles) { if ($Computer) { $ProfilePath = $UserProfile.FullName $ProfileName = $UserProfile.Name } else { $ProfilePath = $UserProfile.ProfileImagePath #20231013 Added exception handeling, only known case is when a windows profile was created when the machine was joined to a previous domain. try { $ProfileName = (New-Object System.Security.Principal.SecurityIdentifier($UserProfile.PSChildName)).Translate( [System.Security.Principal.NTAccount]).Value } catch { $ProfileName = "Unknown Windows User" } } #region classic teams $TeamsSettingPath = $ProfilePath + "\AppData\Roaming\Microsoft\Teams\settings.json" if (Test-Path $TeamsSettingPath -ErrorAction SilentlyContinue) { $TeamsSettings = Get-Content -Path $TeamsSettingPath $Version = "" $Ring = "" $Env = "" $CloudEnv = "" try { $VersionTemp = [regex]::Match($TeamsSettings, $regexVersion).captures.groups if ($VersionTemp.Count -ge 2) { $Version = $VersionTemp[2].value } $RingTemp = [regex]::Match($TeamsSettings, $regexRing).captures.groups if ($RingTemp.Count -ge 2) { $Ring = $RingTemp[2].value } $EnvTemp = [regex]::Match($TeamsSettings, $regexEnv).captures.groups if ($EnvTemp.Count -ge 2) { $Env = $EnvTemp[2].value } $CloudEnvTemp = [regex]::Match($TeamsSettings, $regexCloudEnv).captures.groups if ($CloudEnvTemp.Count -ge 2) { $CloudEnv = $CloudEnvTemp[2].value } } catch { } $TeamsApp = $ProfilePath + "\AppData\Local\Microsoft\Teams\current\Teams.exe" $TeamsInstallTimePath = $ProfilePath + "\AppData\Roaming\Microsoft\Teams\installTime.txt" #20240228 - In some cases the install file can be missing. $tmpInstallDate = "" if (Test-Path $TeamsInstallTimePath -ErrorAction SilentlyContinue) { $InstallDateStr = Get-Content ($ProfilePath + "\AppData\Roaming\Microsoft\Teams\installTime.txt") $tmpInstallDate = [Datetime]::ParseExact($InstallDateStr, 'M/d/yyyy', $null) | Get-Date -Format $currentDateFormat } $TeamsVersion = New-Object -TypeName PSObject -Property @{ Computer = $ComputerName Profile = $ProfileName ProfilePath = $ProfilePath Type = "Teams Classic" Version = $Version Ring = $Ring Environment = $Env CloudEnvironment = $CloudEnv Arch = Get-UcArch $TeamsApp InstallDate = $tmpInstallDate } $TeamsVersion.PSObject.TypeNames.Insert(0, 'TeamsVersion') [void]$outTeamsVersion.Add($TeamsVersion) } #endregion #region New Teams $NewTeamsSettingPath = $ProfilePath + "\AppData\Local\Publishers\8wekyb3d8bbwe\TeamsSharedConfig\tma_settings.json" if (Test-Path $NewTeamsSettingPath -ErrorAction SilentlyContinue) { $NewTeamsSettings = Get-Content -Path $NewTeamsSettingPath | ConvertFrom-Json $tmpAccountID = $NewTeamsSettings.primary_user.accounts.account_id if ($Computer) { $newTeamsLocation = Get-ChildItem -Path ( $RemotePath + "\..\Program Files\Windowsapps" ) -Filter "ms-teams.exe" -Recurse -Depth 1 | Sort-Object -Property CreationTime -Descending | Select-Object -First 1 } else { #20240103 - Using Get-AppPackage drops the requirement to run with Administrative Rights $newTeamsInstallPath = (Get-AppPackage MSTeams).InstallLocation + ".\ms-teams.exe" $newTeamsLocation = Get-ItemProperty -Path ($newTeamsInstallPath) } if (Test-Path -Path $newTeamsLocation.FullName -ErrorAction SilentlyContinue) { $TeamsVersion = New-Object -TypeName PSObject -Property @{ Computer = $ComputerName Profile = $ProfileName ProfilePath = $ProfilePath Type = "New Teams" Version = $newTeamsLocation.VersionInfo.ProductVersion Ring = $NewTeamsSettings.tma_ecs_settings.$tmpAccountID.ring Environment = $NewTeamsSettings.tma_ecs_settings.$tmpAccountID.environment CloudEnvironment = $NewTeamsSettings.primary_user.accounts.cloud Arch = Get-UcArch $newTeamsLocation.FullName InstallDate = $newTeamsLocation.CreationTime | Get-Date -Format $currentDateFormat } $TeamsVersion.PSObject.TypeNames.Insert(0, 'TeamsVersion') [void]$outTeamsVersion.Add($TeamsVersion) } } #endregion } if ($Credential -and $PSDriveName) { try { Remove-PSDrive -Name $PSDriveName -ErrorAction SilentlyContinue } catch {} } } return $outTeamsVersion } #EndRegion '.\Public\Get-UcTeamsVersion.ps1' 284 #Region '.\Public\Get-UcTeamsVersionBatch.ps1' -1 function Get-UcTeamsVersionBatch { param( [Parameter(Mandatory = $true)] [string]$InputCSV, [string]$OutputPath, [switch]$ExportCSV, [System.Management.Automation.PSCredential]$Credential ) <# .SYNOPSIS Get Microsoft Teams Desktop Version from all computers in a csv file. .DESCRIPTION This function returns the installed Microsoft Teams desktop version for each user profile. .PARAMETER InputCSV CSV with the list of computers that we want to get the Teams Version .PARAMETER OutputPath Specify the output path .PARAMETER ExportCSV Export the output to a CSV file .PARAMETER Credential Specify the credential to be used to connect to the remote computers .EXAMPLE PS> Get-UcTeamsVersionBatch .EXAMPLE PS> Get-UcTeamsVersionBatch -InputCSV C:\Temp\ComputerList.csv -Credential $cred .EXAMPLE PS> Get-UcTeamsVersionBatch -InputCSV C:\Temp\ComputerList.csv -Credential $cred -ExportCSV #> Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null if (Test-Path $InputCSV) { try { $Computers = Import-Csv -Path $InputCSV } catch { Write-Host ("Invalid CSV input file: " + $InputCSV) -ForegroundColor Red return } $outTeamsVersion = [System.Collections.ArrayList]::new() #Verify if the Output Path exists if ($OutputPath) { if (!(Test-Path $OutputPath -PathType Container)) { Write-Host ("Error: Invalid folder: " + $OutputPath) -ForegroundColor Red return } } else { $OutputPath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads") } $c = 0 $compCount = $Computers.count foreach ($computer in $Computers) { $c++ Write-Progress -Activity ("Getting Teams Version from: " + $computer.Computer) -Status "Computer $c of $compCount " $tmpTV = Get-UcTeamsVersion -Computer $computer.Computer -Credential $cred -SkipModuleCheck $outTeamsVersion.Add($tmpTV) | Out-Null } if ($ExportCSV) { $tmpFileName = "MSTeamsVersion_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv" $OutputFullPath = [System.IO.Path]::Combine($OutputPath, $tmpFileName) $outTeamsVersion | Sort-Object Computer, Profile | Select-Object Computer, Profile, ProfilePath, Arch, Version, Environment, Ring, InstallDate | Export-Csv -path $OutputFullPath -NoTypeInformation Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan } else { return $outTeamsVersion } } else { Write-Host ("Error: File not found " + $InputCSV) -ForegroundColor Red } } #EndRegion '.\Public\Get-UcTeamsVersionBatch.ps1' 82 #Region '.\Public\Get-UcTeamsWithSingleOwner.ps1' -1 function Get-UcTeamsWithSingleOwner { <# .SYNOPSIS Get Teams that have a single owner .DESCRIPTION This function returns a list of Teams that only have a single owner. .EXAMPLE PS> Get-UcTeamsWithSingleOwner #> Get-UcTeamUsersEmail -Role Owner -Confirm:$false | Group-Object -Property TeamDisplayName | Where-Object { $_.Count -lt 2 } | Select-Object -ExpandProperty Group } #EndRegion '.\Public\Get-UcTeamsWithSingleOwner.ps1' 14 #Region '.\Public\Get-UcTeamUsersEmail.ps1' -1 function Get-UcTeamUsersEmail { [cmdletbinding(SupportsShouldProcess)] param( [string]$TeamName, [ValidateSet("Owner", "User", "Guest")] [string]$Role ) <# .SYNOPSIS Get Users Email Address that are in a Team .DESCRIPTION This function returns a list of users email address that are part of a Team. .PARAMETER TeamName Specifies Team Name .PARAMETER Role Specifies which roles to filter (Owner, User, Guest) .EXAMPLE PS> Get-UcTeamUsersEmail .EXAMPLE PS> Get-UcTeamUsersEmail -TeamName "Marketing" .EXAMPLE PS> Get-UcTeamUsersEmail -Role "Guest" .EXAMPLE PS> Get-UcTeamUsersEmail -TeamName "Marketing" -Role "Guest" #> $output = [System.Collections.ArrayList]::new() Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null if ($TeamName) { $Teams = Get-Team -DisplayName $TeamName } else { if ($ConfirmPreference) { $title = 'Confirm' $question = 'Are you sure that you want to list all Teams?' $choices = '&Yes', '&No' $decision = $Host.UI.PromptForChoice($title, $question, $choices, 1) } else { $decision = 0 } if ($decision -eq 0) { $Teams = Get-Team } else { return } } foreach ($Team in $Teams) { if ($Role) { $TeamMembers = Get-TeamUser -GroupId $Team.GroupID -Role $Role } else { $TeamMembers = Get-TeamUser -GroupId $Team.GroupID } foreach ($TeamMember in $TeamMembers) { $Email = ( Get-csOnlineUser $TeamMember.User | Select-Object @{Name = 'PrimarySMTPAddress'; Expression = { $_.ProxyAddresses -cmatch '^SMTP:' -creplace 'SMTP:' } }).PrimarySMTPAddress $Member = New-Object -TypeName PSObject -Property @{ TeamGroupID = $Team.GroupID TeamDisplayName = $Team.DisplayName TeamVisibility = $Team.Visibility UPN = $TeamMember.User Role = $TeamMember.Role Email = $Email } $Member.PSObject.TypeNames.Insert(0, 'TeamUsersEmail') $output.Add($Member) | Out-Null } } return $output } #EndRegion '.\Public\Get-UcTeamUsersEmail.ps1' 79 #Region '.\Public\Test-UcPowerShellModule.ps1' -1 function Test-UcPowerShellModule { param( [Parameter(Mandatory = $true)] [string]$ModuleName ) <# .SYNOPSIS Test if PowerShell module is installed and updated .DESCRIPTION This function returns FALSE if PowerShell module is not installed. .PARAMETER ModuleName Specifies PowerShell module name .EXAMPLE PS> Test-UcPowerShellModule -ModuleName UCLobbyTeams #> try { #Get all installed versions $installedVersions = (Get-Module $ModuleName -ListAvailable | Sort-Object Version -Descending).Version #Get the lastest version available $availableVersion = (Find-Module -Name $ModuleName -Repository PSGallery -ErrorAction SilentlyContinue).Version if (!($installedVersions)) { if ($availableVersion ) { #Module not installed and there is an available version to install. Write-Warning ("The PowerShell Module $ModuleName is not installed, please install the latest available version ($availableVersion) with:" + [Environment]::NewLine + "Install-Module $ModuleName") } else { #Wrong name or not found in the registered PS Repository. Write-Warning ("The PowerShell Module $ModuleName not found in the registered PS Repository, please check the module name and try again.") } return $false } #Get the current loaded version $tmpCurrentVersion = (Get-Module $ModuleName | Sort-Object Version -Descending) if ($tmpCurrentVersion) { $currentVersion = $tmpCurrentVersion[0].Version.ToString() } if (!($currentVersion)) { #Module is installed but not imported, in this case we check if there is a newer version available. if ($availableVersion -in $installedVersions) { Write-Warning ("The lastest available version of $ModuleName module is installed, however the module is not imported." + [Environment]::NewLine + "Please make sure you import it with:" + [Environment]::NewLine + "Import-Module $ModuleName -RequiredVersion $availableVersion") } else { Write-Warning ("There is a new version available $availableVersion, the lastest installed version is " + $installedVersions[0] + "." + [Environment]::NewLine + "Please update the module with:" + [Environment]::NewLine + "Update-Module $ModuleName") } } if ($currentVersion -ne $availableVersion ) { if ($availableVersion -in $installedVersions) { Write-Warning ("The lastest available version of $ModuleName module is installed, however version $currentVersion is imported." + [Environment]::NewLine + "Please make sure you import it with:" + [Environment]::NewLine + "Import-Module $ModuleName -RequiredVersion $availableVersion") } else { Write-Warning ("There is a new version available $availableVersion, current version $currentVersion." + [Environment]::NewLine + "Please update the module with:" + [Environment]::NewLine + "Update-Module $ModuleName") } } return $true } catch { } return $false } #EndRegion '.\Public\Test-UcPowerShellModule.ps1' 68 #Region '.\Public\Test-UcTeamsDevicesCompliancePolicy.ps1' -1 function Test-UcTeamsDevicesCompliancePolicy { param( [switch]$Detailed, [switch]$All, [switch]$IncludeSupported, [string]$PolicyID, [string]$PolicyName, [string]$UserUPN, [string]$DeviceID, [switch]$ExportCSV, [string]$OutputPath ) <# .SYNOPSIS Validate which Intune Compliance policies are supported by Microsoft Teams Android Devices .DESCRIPTION This function will validate each setting in the Intune Compliance Policy to make sure they are in line with the supported settings: https://docs.microsoft.com/en-us/microsoftteams/rooms/supported-ca-and-compliance-policies?tabs=phones#supported-device-compliance-policies Contributors: Traci Herr, David Paulino Requirements: Microsoft Graph PowerShell Module (Install-Module Microsoft.Graph) .PARAMETER Detailed Displays test results for unsupported settings in each Intune Compliance Policy .PARAMETER All Will check all Intune Compliance policies independently if they are assigned to a Group(s) .PARAMETER IncludeSupported Displays results for all settings in each Intune Compliance Policy .PARAMETER PolicyID Specifies a Policy ID that will be checked if is supported by Microsoft Teams Android Devices .PARAMETER PolicyName Specifies a Policy Name that will be checked if is supported by Microsoft Teams Android Devices .PARAMETER UserUPN Specifies a UserUPN that we want to check for applied compliance policies .PARAMETER DeviceID Specifies DeviceID that we want to check for applied compliance policies .PARAMETER ExportCSV When present will export the detailed results to a CSV file. By defautl will save the file under the current user downloads, unless we specify the OutputPath. .PARAMETER OutputPath Allows to specify the path where we want to save the results. .EXAMPLE PS> Test-UcTeamsDevicesCompliancePolicy .EXAMPLE PS> Test-UcTeamsDevicesCompliancePolicy -Detailed #> $connectedMSGraph = $false $CompliancePolicies = $null $totalCompliancePolicies = 0 $skippedCompliancePolicies = 0 $GraphURI_CompliancePolicies = "https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies/" $GraphURI_Users = "https://graph.microsoft.com/v1.0/users" $GraphURI_Groups = "https://graph.microsoft.com/v1.0/groups" $GraphURI_Devices = "https://graph.microsoft.com/v1.0/devices" $SupportedAndroidCompliancePolicies = "#microsoft.graph.androidCompliancePolicy", "#microsoft.graph.androidDeviceOwnerCompliancePolicy", "#microsoft.graph.aospDeviceOwnerCompliancePolicy" $SupportedWindowsCompliancePolicies = "#microsoft.graph.windows10CompliancePolicy" $URLSupportedCompliancePoliciesAndroid = "https://aka.ms/TeamsDevicePolicies?tabs=phones#supported-device-compliance-policies" $URLSupportedCompliancePoliciesWindows = "https://aka.ms/TeamsDevicePolicies?tabs=mtr-w#supported-device-compliance-policies" if (Test-UcMgGraphConnection -Scopes "DeviceManagementConfiguration.Read.All", "Directory.Read.All") { Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null $outFileName = "TeamsDevices_CompliancePolicy_Report_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv" if ($OutputPath) { if (!(Test-Path $OutputPath -PathType Container)) { Write-Host ("Error: Invalid folder " + $OutputPath) -ForegroundColor Red return } $OutputFullPath = [System.IO.Path]::Combine($OutputPath, $outFileName) } else { $OutputFullPath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads", $outFileName) } try { Write-Progress -Activity "Test-UcTeamsDeviceCompliancePolicy" -Status "Getting Compliance Policies" $CompliancePolicies = (Invoke-MgGraphRequest -Uri $GraphURI_CompliancePolicies -Method GET).value $connectedMSGraph = $true } catch [System.Net.Http.HttpRequestException] { if ($PSItem.Exception.Response.StatusCode -eq "Unauthorized") { Write-Error "Access Denied, please make sure the user connecing to MS Graph is part of one of the following Global Reader/Intune Service Administrator/Global Administrator roles" } else { Write-Error $PSItem.Exception.Message } } catch { Write-Error 'Please connect to MS Graph with Connect-MgGraph -Scopes "DeviceManagementConfiguration.Read.All","Directory.Read.All" before running this script' } if ($connectedMSGraph) { $output = [System.Collections.ArrayList]::new() $outputSum = [System.Collections.ArrayList]::new() if ($UserUPN) { try { $UserGroups = (Invoke-MgGraphRequest -Uri ($GraphURI_Users + "/" + $userUPN + "/transitiveMemberOf?`$select=id") -Method GET).value.id } catch [System.Net.Http.HttpRequestException] { if ($PSItem.Exception.Response.StatusCode -eq "NotFound") { Write-warning -Message ("User Not Found: " + $UserUPN) } return } #We also need to take in consideration devices that are registered to this user $DeviceGroups = [System.Collections.ArrayList]::new() $userDevices = (Invoke-MgGraphRequest -Uri ($GraphURI_Users + "/" + $userUPN + "/registeredDevices?`$select=deviceId,displayName") -Method GET).value foreach ($userDevice in $userDevices) { $tmpGroups = (Invoke-MgGraphRequest -Uri ($GraphURI_Devices + "(deviceId='{" + $userDevice.deviceID + "}')/transitiveMemberOf?`$select=id") -Method GET).value.id foreach ($tmpGroup in $tmpGroups) { $tmpDG = New-Object -TypeName PSObject -Property @{ GroupId = $tmpGroup DeviceId = $userDevice.deviceID DeviceDisplayName = $userDevice.displayName } $DeviceGroups.Add($tmpDG) | Out-Null } } } if ($DeviceID) { try { $DeviceGroups = (Invoke-MgGraphRequest -Uri ($GraphURI_Devices + "(deviceId='{" + $DeviceID + "}')/transitiveMemberOf?`$select=id") -Method GET).value.id } catch [System.Net.Http.HttpRequestException] { if ($PSItem.Exception.Response.StatusCode -eq "BadRequest") { Write-warning -Message ("Device ID Not Found: " + $DeviceID) } return } } #TO DO: We should only get display names from assigned groups $Groups = New-Object 'System.Collections.Generic.Dictionary[string, string]' $p = 0 $policyCount = $CompliancePolicies.Count foreach ($CompliancePolicy in $CompliancePolicies) { $p++ Write-Progress -Activity "Test-UcTeamsDeviceCompliancePolicy" -Status ("Checking policy " + $CompliancePolicy.displayName + " - $p of $policyCount") if ((($PolicyID -eq $CompliancePolicy.id) -or ($PolicyName -eq $CompliancePolicy.displayName) -or (!$PolicyID -and !$PolicyName)) -and (($CompliancePolicy."@odata.type" -in $SupportedAndroidCompliancePolicies) -or ($CompliancePolicy."@odata.type" -in $SupportedWindowsCompliancePolicies))) { #We need to check if the policy has assignments (Groups/All Users/All Devices) $CompliancePolicyAssignments = (Invoke-MgGraphRequest -Uri ($GraphURI_CompliancePolicies + $CompliancePolicy.id + "/assignments" ) -Method GET).value $AssignedToGroup = [System.Collections.ArrayList]::new() $ExcludedFromGroup = [System.Collections.ArrayList]::new() $outAssignedToGroup = "" $outExcludedFromGroup = "" #We wont need to check the settings if the policy is not assigned to a user if ($UserUPN -or $DeviceID) { $userOrDeviceIncluded = $false } else { $userOrDeviceIncluded = $true } #Define the Compliance Policy type switch ($CompliancePolicy."@odata.type") { "#microsoft.graph.androidCompliancePolicy" { $CPType = "Android Device" } "#microsoft.graph.androidDeviceOwnerCompliancePolicy" { $CPType = "Android Enterprise" } "#microsoft.graph.aospDeviceOwnerCompliancePolicy" { $CPType = "Android (AOSP)" } "#microsoft.graph.windows10CompliancePolicy" { $CPType = "Windows 10 or later" } Default { $CPType = $CompliancePolicy."@odata.type".split('.')[2] } } #Checking Compliance Policy assigments since we can skip non assigned policies. foreach ($CompliancePolicyAssignment in $CompliancePolicyAssignments) { $GroupDisplayName = $CompliancePolicyAssignment.target.Groupid if ($Groups.ContainsKey($CompliancePolicyAssignment.target.Groupid)) { $GroupDisplayName = $Groups.Item($CompliancePolicyAssignment.target.Groupid) } else { try { $GroupInfo = Invoke-MgGraphRequest -Uri ($GraphURI_Groups + "/" + $CompliancePolicyAssignment.target.Groupid + "/?`$select=id,displayname") -Method GET $Groups.Add($GroupInfo.id, $GroupInfo.displayname) $GroupDisplayName = $GroupInfo.displayname } catch { } } $GroupEntry = New-Object -TypeName PSObject -Property @{ GroupID = $CompliancePolicyAssignment.target.Groupid GroupDisplayName = $GroupDisplayName } switch ($CompliancePolicyAssignment.target."@odata.type") { #Policy assigned to all users "#microsoft.graph.allLicensedUsersAssignmentTarget" { $GroupEntry = New-Object -TypeName PSObject -Property @{ GroupID = "allLicensedUsersAssignment" GroupDisplayName = "All Users" } $AssignedToGroup.Add($GroupEntry) | Out-Null $userOrDeviceIncluded = $true } #Policy assigned to all devices "#microsoft.graph.allDevicesAssignmentTarget" { $GroupEntry = New-Object -TypeName PSObject -Property @{ GroupID = "allDevicesAssignmentTarget" GroupDisplayName = "All Devices" } $AssignedToGroup.Add($GroupEntry) | Out-Null $userOrDeviceIncluded = $true } #Group that this policy is assigned "#microsoft.graph.groupAssignmentTarget" { $AssignedToGroup.Add($GroupEntry) | Out-Null if (($UserUPN -or $DeviceID) -and (($CompliancePolicyAssignment.target.Groupid -in $UserGroups) -or ($CompliancePolicyAssignment.target.Groupid -in $DeviceGroups))) { $userOrDeviceIncluded = $true } } #Group that this policy is excluded "#microsoft.graph.exclusionGroupAssignmentTarget" { $ExcludedFromGroup.Add($GroupEntry) | Out-Null #If user is excluded then we dont need to check the policy if ($UserUPN -and ($CompliancePolicyAssignment.target.Groupid -in $UserGroups)) { Write-Warning ("Skiping compliance policy " + $CompliancePolicy.displayName + ", since user " + $UserUPN + " is part of an Excluded Group: " + $GroupEntry.GroupDisplayName) $userOrDeviceExcluded = $true } elseif ($DeviceID -and ($CompliancePolicyAssignment.target.Groupid -in $DeviceGroups)) { Write-Warning ("Skiping compliance policy " + $CompliancePolicy.displayName + ", since device " + $DeviceID + " is part of an Excluded Group: " + $GroupEntry.GroupDisplayName) $userOrDeviceExcluded = $true } elseif ($UserUPN -and (($CompliancePolicyAssignment.target.Groupid -in $DeviceGroups.GroupId))) { #In case a device is excluded we will check the policy but output a message $tmpDev = ($DeviceGroups | Where-Object -Property GroupId -eq -Value $CompliancePolicyAssignment.target.Groupid) Write-Warning ("Compliance policy " + $CompliancePolicy.displayName + " will not be applied to device " + $tmpDev.DeviceDisplayName + " (" + $tmpDev.DeviceID + "), since this device is part of an Excluded Group: " + $GroupEntry.GroupDisplayName) } } } } if ((($AssignedToGroup.count -gt 0) -and !$userOrDeviceExcluded -and $userOrDeviceIncluded) -or $all) { $totalCompliancePolicies++ $PolicyErrors = 0 $PolicyWarnings = 0 #If only assigned/excluded from a group we will show the group display name, otherwise the number of groups assigned/excluded. if ($AssignedToGroup.count -eq 1) { $outAssignedToGroup = $AssignedToGroup.GroupDisplayName } elseif ($AssignedToGroup.count -eq 0) { $outAssignedToGroup = "None" } else { $outAssignedToGroup = "" + $AssignedToGroup.count + " groups" } if ($ExcludedFromGroup.count -eq 1) { $outExcludedFromGroup = $ExcludedFromGroup.GroupDisplayName } elseif ($ExcludedFromGroup.count -eq 0) { $outExcludedFromGroup = "None" } else { $outExcludedFromGroup = "" + $ExcludedFromGroup.count + " groups" } if ($CompliancePolicy."@odata.type" -in $SupportedAndroidCompliancePolicies) { $URLSupportedCompliancePolicies = $URLSupportedCompliancePoliciesAndroid } elseif ($CompliancePolicy."@odata.type" -in $SupportedWindowsCompliancePolicies) { $URLSupportedCompliancePolicies = $URLSupportedCompliancePoliciesWindows } #region Common settings between Android (ADA and AOSP) and Windows #region 9: Device Properties > Operation System Version $ID = 9.1 $Setting = "osMinimumVersion" $SettingDescription = "Device Properties > Operation System Version > Minimum OS version" $SettingValue = "Not Configured" $Comment = "" if (!([string]::IsNullOrEmpty($CompliancePolicy.osMinimumVersion))) { if ($CompliancePolicy."@odata.type" -in $SupportedWindowsCompliancePolicies) { $Status = "Unsupported" $Comment = "Teams Rooms automatically updates to newer versions of Windows and setting values here could prevent successful sign-in after an OS update." $PolicyErrors++ } else { $Status = "Warning" $Comment = "This setting can cause sign in issues." $PolicyWarnings++ } $SettingValue = $CompliancePolicy.osMinimumVersion } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) $ID = 9.2 $Setting = "osMaximumVersion" $SettingDescription = "Device Properties > Operation System Version > Maximum OS version" $SettingValue = "Not Configured" $Comment = "" if (!([string]::IsNullOrEmpty($CompliancePolicy.osMaximumVersion))) { if ($CompliancePolicy."@odata.type" -in $SupportedWindowsCompliancePolicies) { $Status = "Unsupported" $Comment = "Teams Rooms automatically updates to newer versions of Windows and setting values here could prevent successful sign-in after an OS update." $PolicyErrors++ } else { $Status = "Warning" $Comment = "This setting can cause sign in issues." $PolicyWarnings++ } $SettingValue = $CompliancePolicy.osMaximumVersion } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 17: System Security > All Android devices > Require a password to unlock mobile devices $ID = 17 $Setting = "passwordRequired" $SettingDescription = "System Security > All Android devices > Require a password to unlock mobile devices" $SettingValue = "Not Configured" $Comment = "" if ($CompliancePolicy.passwordRequired) { $Status = "Unsupported" $SettingValue = "Require" $Comment = $URLSupportedCompliancePolicies $PolicyErrors++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #endregion #20240508 - We need to limit the settings since not all are available in AOSP compliance policies. if ($CompliancePolicy."@odata.type" -in $SupportedAndroidCompliancePolicies -and $CompliancePolicy."@odata.type" -ne "#microsoft.graph.aospDeviceOwnerCompliancePolicy") { #region 1: Microsoft Defender for Endpoint > Require the device to be at or under the machine risk score $ID = 1 $Setting = "deviceThreatProtectionEnabled" $SettingDescription = "Microsoft Defender for Endpoint > Require the device to be at or under the machine risk score" $SettingValue = "Not Configured" $Comment = "" if ($CompliancePolicy.deviceThreatProtectionEnabled) { $Status = "Unsupported" $PolicyErrors++ $SettingValue = $CompliancePolicy.advancedThreatProtectionRequiredSecurityLevel $Comment = $URLSupportedCompliancePolicies } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 2: Device Health > Device managed with device administrator $ID = 2 $Setting = "securityBlockDeviceAdministratorManagedDevices" $SettingDescription = "Device Health > Device managed with device administrator" $SettingValue = "Not Configured" $Comment = "" if ($CompliancePolicy.securityBlockDeviceAdministratorManagedDevices) { $Status = "Unsupported" $SettingValue = "Block" $Comment = "Teams Android devices management requires device administrator to be enabled." $PolicyErrors++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 4: Device Health > Require the device to be at or under the Device Threat Level $ID = 4 $Setting = "deviceThreatProtectionRequiredSecurityLevel" $SettingDescription = "Device Health > Require the device to be at or under the Device Threat Level" $SettingValue = "Not Configured" $Comment = "" if ($CompliancePolicy.deviceThreatProtectionRequiredSecurityLevel -ne "unavailable") { $Status = "Unsupported" $SettingValue = $CompliancePolicy.deviceThreatProtectionRequiredSecurityLevel $Comment = $URLSupportedCompliancePolicies $PolicyErrors++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 5: Device Health > Google Protect > Google Play Services is Configured $ID = 5 $Setting = "securityRequireGooglePlayServices" $SettingDescription = "Device Health > Google Protect > Google Play Services is Configured" $SettingValue = "Not Configured" $Comment = "" if ($CompliancePolicy.securityRequireGooglePlayServices) { $Status = "Unsupported" $SettingValue = "Require" $Comment = "Google play isn't installed on Teams Android devices." $PolicyErrors++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 6: Device Health > Google Protect > Up-to-date security provider $ID = 6 $Setting = "securityRequireUpToDateSecurityProviders" $SettingDescription = "Device Health > Google Protect > Up-to-date security provider" $SettingValue = "Not Configured" $Comment = "" if ($CompliancePolicy.securityRequireUpToDateSecurityProviders) { $Status = "Unsupported" $SettingValue = "Require" $Comment = "Google play isn't installed on Teams Android devices." $PolicyErrors++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 7: Device Health > Google Protect > Threat scan on apps $ID = 7 $Setting = "securityRequireVerifyApps" $SettingDescription = "Device Health > Google Protect > Threat scan on apps" $SettingValue = "Not Configured" $Comment = "" if ($CompliancePolicy.securityRequireVerifyApps) { $Status = "Unsupported" $SettingValue = "Require" $Comment = "Google play isn't installed on Teams Android devices." $PolicyErrors++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 8: Device Health > Google Protect > SafetyNet device attestation $ID = 8 $Setting = "securityRequireSafetyNetAttestation" $SettingDescription = "Device Health > Google Protect > SafetyNet device attestation" $SettingValue = "Not Configured" $Comment = "" if (($CompliancePolicy.securityRequireSafetyNetAttestationBasicIntegrity) -or ($CompliancePolicy.securityRequireSafetyNetAttestationCertifiedDevice)) { $Status = "Unsupported" $Comment = "Google play isn't installed on Teams Android devices." $PolicyErrors++ if ($CompliancePolicy.securityRequireSafetyNetAttestationCertifiedDevice) { $SettingValue = "Check basic integrity and certified devices" } elseif ($CompliancePolicy.securityRequireSafetyNetAttestationBasicIntegrity) { $SettingValue = "Check basic integrity" } } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 11: System Security > Device Security > Block apps from unknown sources $ID = 11 $Setting = "securityPreventInstallAppsFromUnknownSources" $SettingDescription = "System Security > Device Security > Block apps from unknown sources" $SettingValue = "Not Configured" $Comment = "" if ($CompliancePolicy.securityPreventInstallAppsFromUnknownSources) { $Status = "Unsupported" $SettingValue = "Block" $Comment = "Only Teams admins install apps or OEM tools" $PolicyErrors++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 15: System Security > Device Security > Restricted apps $ID = 15 $Setting = "securityPreventInstallAppsFromUnknownSources" $SettingDescription = "System Security > Device Security > Restricted apps" $SettingValue = "Not Configured" $Comment = "" if (($CompliancePolicy.restrictedApps).count -gt 0 ) { $Status = "Unsupported" $SettingValue = "Found " + ($CompliancePolicy.restrictedApps).count + " restricted app(s)" $Comment = $URLSupportedCompliancePolicies $PolicyErrors++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion } if ($CompliancePolicy."@odata.type" -in $SupportedAndroidCompliancePolicies) { #region 3: Device Health > Rooted devices $ID = 3 $Setting = "securityBlockJailbrokenDevices" $SettingDescription = "Device Health > Rooted devices" $SettingValue = "Not Configured" $Comment = "" if ($CompliancePolicy.securityBlockJailbrokenDevices) { $Status = "Warning" $SettingValue = "Block" $Comment = "This setting can cause sign in issues." $PolicyWarnings++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 10: System Security > Encryption > Require encryption of data storage on device. $ID = 10 $Setting = "storageRequireEncryption" $SettingDescription = "System Security > Encryption > Require encryption of data storage on device" $SettingValue = "Not Configured" $Comment = "" if ($CompliancePolicy.storageRequireEncryption) { $Status = "Warning" $SettingValue = "Require" $Comment = "Manufacturers might configure encryption attributes on their devices in a way that Intune doesn't recognize. If this happens, Intune marks the device as noncompliant." $PolicyWarnings++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 14: System Security > Device Security > Minimum security patch level $ID = 14 $Setting = "minAndroidSecurityPatchLevel" $SettingDescription = "System Security > Device Security > Minimum security patch level" $SettingValue = "Not Configured" $Comment = "" if (!([string]::IsNullOrEmpty($CompliancePolicy.minAndroidSecurityPatchLevel))) { $Status = "Warning" $SettingValue = $CompliancePolicy.minAndroidSecurityPatchLevel $Comment = "This setting can cause sign in issues." $PolicyWarnings++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 16: System Security > All Android devices > Maximum minutes of inactivity before password is required $ID = 16 $Setting = "passwordMinutesOfInactivityBeforeLock" $SettingDescription = "System Security > All Android devices > Maximum minutes of inactivity before password is required" $SettingValue = "Not Configured" $Comment = "" if (!([string]::IsNullOrEmpty($CompliancePolicy.passwordMinutesOfInactivityBeforeLock))) { $Status = "Unsupported" $SettingValue = "" + $CompliancePolicy.passwordMinutesOfInactivityBeforeLock + " minutes" $Comment = $URLSupportedCompliancePolicies $PolicyErrors++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion } elseif ($CompliancePolicy."@odata.type" -in $SupportedWindowsCompliancePolicies) { #region 18: Device Properties > Operation System Version $ID = 18.1 $Setting = "mobileOsMinimumVersion" $SettingDescription = "Device Properties > Operation System Version > Minimum OS version for mobile devices" $SettingValue = "Not Configured" $Comment = "" if (!([string]::IsNullOrEmpty($CompliancePolicy.mobileOsMinimumVersion))) { $Status = "Unsupported" $SettingValue = $CompliancePolicy.mobileOsMinimumVersion $Comment = $URLSupportedCompliancePolicies $PolicyErrors++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) $ID = 18.2 $Setting = "mobileOsMaximumVersion" $SettingDescription = "Device Properties > Operation System Version > Maximum OS version for mobile devices" $SettingValue = "Not Configured" $Comment = "" if (!([string]::IsNullOrEmpty($CompliancePolicy.mobileOsMaximumVersion))) { $Status = "Unsupported" $SettingValue = $CompliancePolicy.mobileOsMaximumVersion $Comment = $URLSupportedCompliancePolicies $PolicyErrors++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 19: Device Properties > Operation System Version > Valid operating system builds $ID = 19 $Setting = "validOperatingSystemBuildRanges" $SettingDescription = "Device Properties > Operation System Version > Valid operating system builds" $SettingValue = "Not Configured" $Comment = "" if (!([string]::IsNullOrEmpty($CompliancePolicy.validOperatingSystemBuildRanges))) { $Status = "Unsupported" $SettingValue = "Found " + ($CompliancePolicy.validOperatingSystemBuildRanges).count + " valid OS configured build(s)" $Comment = $URLSupportedCompliancePolicies $PolicyErrors++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 20: System Security > Defender > Microsoft Defender Antimalware minimum version $ID = 20 $Setting = "defenderVersion" $SettingDescription = "System Security > Defender > Microsoft Defender Antimalware minimum version" $SettingValue = "Not Configured" $Comment = "" if (!([string]::IsNullOrEmpty($CompliancePolicy.defenderVersion))) { $Status = "Unsupported" $SettingValue = $CompliancePolicy.defenderVersion $Comment = "Teams Rooms automatically updates this component so there's no need to set compliance policies." $PolicyErrors++ } else { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $CompliancePolicy.displayName PolicyType = $CPType Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $CompliancePolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed') [void]$output.Add($SettingPSObj) #endregion } if ($PolicyErrors -gt 0) { $StatusSum = "Found " + $PolicyErrors + " unsupported settings." $displayWarning = $true } elseif ($PolicyWarnings -gt 0) { $StatusSum = "Found " + $PolicyWarnings + " settings that may impact users." $displayWarning = $true } else { $StatusSum = "No issues found." } $PolicySum = [PSCustomObject]@{ PolicyID = $CompliancePolicy.id PolicyName = $CompliancePolicy.displayName PolicyType = $CPType AssignedToGroup = $outAssignedToGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroup = $outExcludedFromGroup ExcludedFromGroupList = $ExcludedFromGroup TeamsDevicesStatus = $StatusSum } $PolicySum.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicy') $outputSum.Add($PolicySum) | Out-Null } elseif (($AssignedToGroup.count -eq 0) -and !($UserUPN -or $DeviceID -or $Detailed)) { $skippedCompliancePolicies++ } } } if ($totalCompliancePolicies -eq 0) { if ($UserUPN) { Write-Warning ("The user " + $UserUPN + " doesn't have any Compliance Policies assigned.") } else { Write-Warning "No Compliance Policies assigned to All Users, All Devices or group found. Please use Test-UcTeamsDevicesCompliancePolicy -All to check all policies." } } if ($IncludeSupported -and $Detailed) { if ($ExportCSV) { $output | Sort-Object PolicyName, ID | Select-Object PolicyName, PolicyID, PolicyType, AssignedToGroup, ExcludedFromGroup, TeamsDevicesStatus, Setting, SettingDescription, Value, Comment | Export-Csv -path $OutputFullPath -NoTypeInformation Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan return } else { $output | Sort-Object PolicyName, ID } } elseif ($Detailed) { if ((( $output | Where-Object -Property TeamsDevicesStatus -NE -Value "Supported").count -eq 0) -and !$IncludeSupported) { Write-Warning "No unsupported settings found, please use Test-UcTeamsDevicesCompliancePolicy -IncludeSupported to output all settings." } else { if ($ExportCSV) { $output | Where-Object -Property TeamsDevicesStatus -NE -Value "Supported" | Sort-Object PolicyName, ID | Select-Object PolicyName, PolicyID, PolicyType, AssignedToGroup, ExcludedFromGroup, TeamsDevicesStatus, Setting, SettingDescription, Value, Comment | Export-Csv -path $OutputFullPath -NoTypeInformation Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan return } else { $output | Where-Object -Property TeamsDevicesStatus -NE -Value "Supported" | Sort-Object PolicyName, ID } } } else { if (($skippedCompliancePolicies -gt 0) -and !$All) { Write-Warning ("Skipping $skippedCompliancePolicies compliance policies since will not be applied to Teams Devices.") Write-Warning ("Please use the All switch to check all policies: Test-UcTeamsDevicesCompliancePolicy -All") } if ($displayWarning) { Write-Warning "One or more policies contain unsupported settings, please use Test-UcTeamsDevicesCompliancePolicy -Detailed to identify the unsupported settings." } $outputSum | Sort-Object PolicyName } } } } #EndRegion '.\Public\Test-UcTeamsDevicesCompliancePolicy.ps1' 1059 #Region '.\Public\Test-UcTeamsDevicesConditionalAccessPolicy.ps1' -1 function Test-UcTeamsDevicesConditionalAccessPolicy { param( [switch]$Detailed, [switch]$All, [switch]$IncludeSupported, [string]$UserUPN, [switch]$ExportCSV, [string]$OutputPath ) <# .SYNOPSIS Validate which Conditional Access policies are supported by Microsoft Teams Android Devices .DESCRIPTION This function will validate each setting in a Conditional Access Policy to make sure they are in line with the supported settings: https://docs.microsoft.com/microsoftteams/rooms/supported-ca-and-compliance-policies?tabs=phones#conditional-access-policies" Contributors: Traci Herr, David Paulino Requirements: Microsoft Graph PowerShell Module (Install-Module Microsoft.Graph) .PARAMETER Detailed Displays test results for all settings in each Conditional Access Policy .PARAMETER All Will check all Conditional Access policies independently if they are assigned to a Group(s) or to Teams .PARAMETER IncludeSupported Displays results for all settings in each Conditional Access Policy .PARAMETER UserUPN Specifies a UserUPN that we want to check for applied Conditional Access policies .PARAMETER ExportCSV When present will export the detailed results to a CSV file. By defautl will save the file under the current user downloads, unless we specify the OutputPath. .PARAMETER OutputPath Allows to specify the path where we want to save the results. .EXAMPLE PS> Test-UcTeamsDevicesConditionalAccessPolicy .EXAMPLE PS> Test-UcTeamsDevicesConditionalAccessPolicy -All .EXAMPLE PS> Test-UcTeamsDevicesConditionalAccessPolicy -Detailed .EXAMPLE PS> Test-UcTeamsDevicesConditionalAccessPolicy -Detailed -IncludedSupported .EXAMPLE PS> Test-UcTeamsDevicesConditionalAccessPolicy -UserUPN #> $GraphURI_Users = "https://graph.microsoft.com/v1.0/users" $GraphURI_Groups = "https://graph.microsoft.com/v1.0/groups" $GraphURI_ConditionalAccess = "https://graph.microsoft.com/beta/identity/conditionalAccess/policies" $connectedMSGraph = $false $ConditionalAccessPolicies = $null $totalCAPolicies = 0 $skippedCAPolicies = 0 $URLTeamsDevicesCA = "https://aka.ms/TeamsDevicePolicies#supported-conditional-access-policies" $URLTeamsDevicesKnownIssues = "https://docs.microsoft.com/microsoftteams/troubleshoot/teams-rooms-and-devices/rooms-known-issues#teams-phone-devices" if (Test-UcMgGraphConnection -Scopes "Policy.Read.All", "Directory.Read.All") { Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null $outFileName = "TeamsDevices_ConditionalAccessPolicy_Report_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv" if ($OutputPath) { if (!(Test-Path $OutputPath -PathType Container)) { Write-Host ("Error: Invalid folder " + $OutputPath) -ForegroundColor Red return } $OutputFullPath = [System.IO.Path]::Combine($OutputPath, $outFileName) } else { $OutputFullPath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads", $outFileName) } try { Write-Progress -Activity "Test-UcTeamsDevicesConditionalAccessPolicy" -Status "Getting Conditional Access Policies" $ConditionalAccessPolicies = (Invoke-MgGraphRequest -Uri ($GraphURI_ConditionalAccess + $GraphFilter) -Method GET).Value $connectedMSGraph = $true } catch [System.Net.Http.HttpRequestException] { if ($PSItem.Exception.Response.StatusCode -eq "Forbidden") { Write-Host "Access Denied, please make sure the user connecing to MS Graph is part of one of the following Global Reader/Conditional Access Administrator/Global Administrator roles" return } else { Write-Error $PSItem.Exception.Message } } catch { Write-Error $PSItem.Exception.Message } if ($connectedMSGraph) { $output = [System.Collections.ArrayList]::new() $outputSum = [System.Collections.ArrayList]::new() if ($UserUPN) { try { $UserID = (Invoke-MgGraphRequest -Uri ($GraphURI_Users + "/" + $userUPN + "?`$select=id") -Method GET).id $UserGroups = (Invoke-MgGraphRequest -Uri ($GraphURI_Users + "/" + $userUPN + "/transitiveMemberOf?`$select=id") -Method GET).value.id } catch [System.Net.Http.HttpRequestException] { if ($PSItem.Exception.Response.StatusCode -eq "NotFound") { Write-warning -Message ("User Not Found: " + $UserUPN) } return } } $Groups = New-Object 'System.Collections.Generic.Dictionary[string, string]' try { Write-Progress -Activity "Test-UcTeamsDevicesConditionalAccessPolicy" -Status "Fetching Service Principals details." $ServicePrincipals = Get-MgServicePrincipal -Select AppId, DisplayName -All } catch {} $p = 0 $policyCount = $ConditionalAccessPolicies.Count foreach ($ConditionalAccessPolicy in $ConditionalAccessPolicies) { $p++ Write-Progress -Activity "Test-UcTeamsDevicesConditionalAccessPolicy" -Status ("Checking policy " + $ConditionalAccessPolicy.displayName + " - $p of $policyCount") $AssignedToGroup = [System.Collections.ArrayList]::new() $ExcludedFromGroup = [System.Collections.ArrayList]::new() $AssignedToUserCount = 0 $ExcludedFromUserCount = 0 $outAssignedToGroup = "" $outExcludedFromGroup = "" $userExcluded = $false $StatusSum = "" $totalCAPolicies++ $PolicyErrors = 0 $PolicyWarnings = 0 if ($UserUPN) { if ($UserID -in $ConditionalAccessPolicy.conditions.users.excludeUsers) { $userExcluded = $true Write-Warning ("Skiping conditional access policy " + $ConditionalAccessPolicy.displayName + ", since user " + $UserUPN + " is part of Excluded Users") } elseif ($UserID -in $ConditionalAccessPolicy.conditions.users.includeUsers) { $userIncluded = $true } else { $userIncluded = $false } } #All Users in Conditional Access Policy will show as a 'All' in the includeUsers. if ("All" -in $ConditionalAccessPolicy.conditions.users.includeUsers) { $GroupEntry = New-Object -TypeName PSObject -Property @{ GroupID = "All" GroupDisplayName = "All Users" } $AssignedToGroup.Add($GroupEntry) | Out-Null $userIncluded = $true } elseif ((($ConditionalAccessPolicy.conditions.users.includeUsers).count -gt 0) -and "None" -notin $ConditionalAccessPolicy.conditions.users.includeUsers) { $AssignedToUserCount = ($ConditionalAccessPolicy.conditions.users.includeUsers).count if (!$UserUPN) { $userIncluded = $true } } foreach ($includedGroup in $ConditionalAccessPolicy.conditions.users.includeGroups) { $GroupDisplayName = $includedGroup if ($Groups.ContainsKey($includedGroup)) { $GroupDisplayName = $Groups.Item($includedGroup) } else { try { $GroupInfo = Invoke-MgGraphRequest -Uri ($GraphURI_Groups + "/" + $includedGroup + "/?`$select=id,displayname") -Method GET $Groups.Add($GroupInfo.id, $GroupInfo.displayname) $GroupDisplayName = $GroupInfo.displayname } catch { } } $GroupEntry = New-Object -TypeName PSObject -Property @{ GroupID = $includedGroup GroupDisplayName = $GroupDisplayName } #We only need to add if we didn't specify a UPN or if the user is part of the group that has the CA assigned. if (!$UserUPN) { $AssignedToGroup.Add($GroupEntry) | Out-Null } if ($includedGroup -in $UserGroups) { $userIncluded = $true $AssignedToGroup.Add($GroupEntry) | Out-Null } } foreach ($excludedGroup in $ConditionalAccessPolicy.conditions.users.excludeGroups) { $GroupDisplayName = $excludedGroup if ($Groups.ContainsKey($excludedGroup)) { $GroupDisplayName = $Groups.Item($excludedGroup) } else { try { $GroupInfo = Invoke-MgGraphRequest -Uri ($GraphURI_Groups + "/" + $excludedGroup + "/?`$select=id,displayname") -Method GET $Groups.Add($GroupInfo.id, $GroupInfo.displayname) $GroupDisplayName = $GroupInfo.displayname } catch { } } $GroupEntry = New-Object -TypeName PSObject -Property @{ GroupID = $excludedGroup GroupDisplayName = $GroupDisplayName } $ExcludedFromGroup.Add($GroupEntry) | Out-Null if ($excludedGroup -in $UserGroups) { $userExcluded = $true Write-Warning ("Skiping conditional access policy " + $ConditionalAccessPolicy.displayName + ", since user " + $UserUPN + " is part of an Excluded Group: " + $GroupEntry.GroupDisplayName) } } $ExcludedFromUserCount = ($ConditionalAccessPolicy.conditions.users.excludeUsers).count if ("GuestsOrExternalUsers" -in $ConditionalAccessPolicy.conditions.users.excludeUsers) { $ExcludedFromUserCount-- } #If only assigned/excluded from a group we will show the group display name, otherwise the number of groups assigned/excluded. if (($AssignedToGroup.count -gt 0) -and ($AssignedToUserCount -gt 0)) { $outAssignedToGroup = "$AssignedToUserCount user(s)," + $AssignedToGroup.count + " group(s)" } elseif (($AssignedToGroup.count -eq 0) -and ($AssignedToUserCount -gt 0)) { $outAssignedToGroup = "$AssignedToUserCount user(s)" } elseif (($AssignedToGroup.count -gt 0) -and ($AssignedToUserCount -eq 0)) { if ($AssignedToGroup.count -eq 1) { $outAssignedToGroup = $AssignedToGroup[0].GroupDisplayName } else { $outAssignedToGroup = "" + $AssignedToGroup.count + " group(s)" } } else { $outAssignedToGroup = "None" } if (($ExcludedFromGroup.count -gt 0) -and ($ExcludedFromUserCount -gt 0)) { $outExcludedFromGroup = "$ExcludedFromUserCount user(s), " + $ExcludedFromGroup.count + " group(s)" } elseif (($ExcludedFromGroup.count -eq 0) -and ($ExcludedFromUserCount -gt 0)) { $outExcludedFromGroup = "$ExcludedFromUserCount user(s)" } elseif (($ExcludedFromGroup.count -gt 0) -and ($ExcludedFromUserCount -eq 0)) { if ($ExcludedFromGroup.count -eq 1) { $outExcludedFromGroup = $ExcludedFromGroup[0].GroupDisplayName } else { $outExcludedFromGroup = "" + $ExcludedFromGroup.count + " group(s)" } } else { $outExcludedFromGroup = "None" } $PolicyState = $ConditionalAccessPolicy.State if ($PolicyState -eq "enabledForReportingButNotEnforced") { $PolicyState = "ReportOnly" } #region 2: Assignment > Cloud apps or actions > Cloud Apps #Exchange 00000002-0000-0ff1-ce00-000000000000 #SharePoint 00000003-0000-0ff1-ce00-000000000000 #Teams cc15fd57-2c6c-4117-a88c-83b1d56b4bbe $ID = 2 $Setting = "CloudApps" $SettingDescription = "Assignment > Cloud apps or actions > Cloud Apps" $Comment = "" $hasExchange = $false $hasSharePoint = $false $hasTeams = $false $hasOffice365 = $false $SettingValue = "" foreach ($Application in $ConditionalAccessPolicy.Conditions.Applications.IncludeApplications) { $appDisplayName = ($ServicePrincipals | Where-Object -Property AppId -eq -Value $Application).DisplayName switch ($Application) { "All" { $hasOffice365 = $true; $SettingValue = "All" } "Office365" { $hasOffice365 = $true; $SettingValue = "Office 365" } "00000002-0000-0ff1-ce00-000000000000" { $hasExchange = $true; $SettingValue += $appDisplayName + "; " } "00000003-0000-0ff1-ce00-000000000000" { $hasSharePoint = $true; $SettingValue += $appDisplayName + "; " } "cc15fd57-2c6c-4117-a88c-83b1d56b4bbe" { $hasTeams = $true; $SettingValue += $appDisplayName + "; " } default { $SettingValue += $appDisplayName + "; " } } } if ($SettingValue.EndsWith("; ")) { $SettingValue = $SettingValue.Substring(0, $SettingValue.Length - 2) } if (((($AssignedToGroup.count -gt 0) -and ($hasOffice365 -or $hasTeams) -and ($PolicyState -NE "disabled")) -and (!$userExcluded) -and $userIncluded) -or $all) { if (($hasExchange -and $hasSharePoint -and $hasTeams) -or ($hasOffice365)) { $Status = "Supported" } else { $Status = "Unsupported" $Comment = "Teams Devices needs to access: Office 365 or Exchange Online, SharePoint Online, and Microsoft Teams" $PolicyErrors++ } $SettingPSObj = [PSCustomObject]@{ PolicyName = $ConditionalAccessPolicy.displayName PolicyState = $PolicyState Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $ConditionalAccessPolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 6: Assignment > Conditions > Locations $ID = 6.1 $Setting = "includeLocations" $SettingDescription = "Assignment > Conditions > Locations" $Comment = "" $Status = "Supported" if ($ConditionalAccessPolicy.conditions.locations.includeLocations) { $SettingValue = $ConditionalAccessPolicy.conditions.locations.includeLocations } else { $SettingValue = "Not Configured" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $ConditionalAccessPolicy.displayName PolicyState = $PolicyState Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $ConditionalAccessPolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed') [void]$output.Add($SettingPSObj) $ID = 6.2 $Setting = "excludeLocations" $SettingDescription = "Assignment > Conditions > Locations" $Comment = "" $Status = "Supported" if ($ConditionalAccessPolicy.conditions.locations.excludeLocations) { $SettingValue = $ConditionalAccessPolicy.conditions.locations.excludeLocations } else { $SettingValue = "Not Configured" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $ConditionalAccessPolicy.displayName PolicyState = $PolicyState Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $ConditionalAccessPolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 7: Assignment > Conditions > Client apps $ID = 7 $Setting = "ClientAppTypes" $SettingDescription = "Assignment > Conditions > Client apps" $SettingValue = "" $Comment = "" foreach ($ClientAppType in $ConditionalAccessPolicy.Conditions.ClientAppTypes) { if ($ClientAppType -eq "All") { $Status = "Supported" $SettingValue = $ClientAppType $Comment = "" } else { $Status = "Unsupported" $SettingValue += $ClientAppType + ";" $Comment = $URLTeamsDevicesCA $PolicyErrors++ } } $SettingPSObj = [PSCustomObject]@{ PolicyName = $ConditionalAccessPolicy.displayName PolicyState = $PolicyState Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $ConditionalAccessPolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 8: Assignment > Conditions > Filter for devices $ID = 8 $Setting = "deviceFilter" $SettingDescription = "Assignment > Conditions > Filter for devices" $Comment = "" if ($ConditionalAccessPolicy.conditions.devices.deviceFilter.mode -eq "exclude") { $Status = "Supported" $SettingValue = $ConditionalAccessPolicy.conditions.devices.deviceFilter.mode + ": " + $ConditionalAccessPolicy.conditions.devices.deviceFilter.rule } else { $SettingValue = "Not Configured" $Status = "Warning" $Comment = "https://learn.microsoft.com/microsoftteams/troubleshoot/teams-rooms-and-devices/teams-android-devices-conditional-access-issues" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $ConditionalAccessPolicy.displayName PolicyState = $PolicyState Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $ConditionalAccessPolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #20240924 - Added check authentication flows #region 9: Assignment > Conditions > Authentication flows $ID = 9 $Setting = "authenticationFlows" $SettingDescription = "Assignment > Conditions > Authentication flows" $Comment = "" if ($ConditionalAccessPolicy.conditions.authenticationFlows.transferMethods -like "*deviceCodeFlow*") { $SettingValue = $ConditionalAccessPolicy.conditions.authenticationFlows.transferMethods if($ConditionalAccessPolicy.GrantControls.BuiltInControls -contains "block"){ $Status = "Not Supported" $Comment = "Authentication flows with block will prevent Teams Devices Remote Sign in" $PolicyErrors++ }else{ $Status = "Supported" } } else { $SettingValue = "Not Configured" $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $ConditionalAccessPolicy.displayName PolicyState = $PolicyState Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $ConditionalAccessPolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 10: Access controls > Grant $Setting = "GrantControls" foreach ($BuiltInControl in $ConditionalAccessPolicy.GrantControls.BuiltInControls) { $Comment = "" $SettingValue = "Enabled" switch ($BuiltInControl) { "mfa" { $ID = 11 $Status = "Warning" $SettingDescription = "Access controls > Grant > Require multi-factor authentication" $PolicyWarnings++ $Comment = "Require multi-factor authentication only supported for Teams Phones and Displays." } "compliantDevice" { $ID = 12 $Status = "Supported" $SettingDescription = "Access controls > Grant > Require device to be marked as compliant" } "DomainJoinedDevice" { $ID = 13 $Status = "Unsupported" $SettingDescription = "Access controls > Grant > Require Hybrid Azure AD joined device" $PolicyErrors++ } "ApprovedApplication" { $ID = 14 $Status = "Unsupported" $SettingDescription = "Access controls > Grant > Require approved client app" $Comment = $URLTeamsDevicesCA $PolicyErrors++ } "CompliantApplication" { $ID = 15 $Status = "Unsupported" $SettingDescription = "Access controls > Grant > Require app protection policy" $Comment = $URLTeamsDevicesCA $PolicyErrors++ } "PasswordChange" { $ID = 16 $Status = "Unsupported" $SettingDescription = "Access controls > Grant > Require password change" $Comment = $URLTeamsDevicesCA $PolicyErrors++ } default { $ID = 10 $SettingDescription = "Access controls > Grant > " + $BuiltInControl $Status = "Supported" } } $SettingPSObj = [PSCustomObject]@{ PolicyName = $ConditionalAccessPolicy.displayName PolicyState = $PolicyState Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $ConditionalAccessPolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed') [void]$output.Add($SettingPSObj) } #endregion #region 17: Access controls > Grant > Custom Authentication Factors $ID = 17 $Setting = "CustomAuthenticationFactors" $SettingDescription = "Access controls > Grant > Custom Authentication Factors" if ($ConditionalAccessPolicy.GrantControls.CustomAuthenticationFactors) { $Status = "Unsupported" $SettingValue = "Enabled" $PolicyErrors++ $Comment = $URLTeamsDevicesCA } else { $Status = "Supported" $SettingValue = "Disabled" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $ConditionalAccessPolicy.displayName PolicyState = $PolicyState Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $ConditionalAccessPolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 18: Access controls > Grant > Terms of Use $ID = 18 $Setting = "TermsOfUse" $SettingDescription = "Access controls > Grant > Terms of Use" $Comment = "" if ($ConditionalAccessPolicy.GrantControls.TermsOfUse) { $Status = "Warning" $SettingValue = "Enabled" $Comment = $URLTeamsDevicesKnownIssues $PolicyWarnings++ } else { $Status = "Supported" $SettingValue = "Disabled" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $ConditionalAccessPolicy.displayName PolicyState = $PolicyState Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $ConditionalAccessPolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 19: Access controls > Session > Use app enforced restrictions $ID = 19 $Setting = "ApplicationEnforcedRestrictions" $SettingDescription = "Access controls > Session > Use app enforced restrictions" $Comment = "" if ($ConditionalAccessPolicy.SessionControls.ApplicationEnforcedRestrictions) { $Status = "Unsupported" $SettingValue = "Enabled" $PolicyErrors++ } else { $Status = "Supported" $SettingValue = "Disabled" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $ConditionalAccessPolicy.displayName PolicyState = $PolicyState Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $ConditionalAccessPolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 19: Access controls > Session > Use Conditional Access App Control $ID = 19 $Setting = "CloudAppSecurity" $SettingDescription = "Access controls > Session > Use Conditional Access App Control" $Comment = "" if ($ConditionalAccessPolicy.SessionControls.CloudAppSecurity) { $Status = "Unsupported" $SettingValue = $ConditionalAccessPolicy.SessionControls.CloudAppSecurity.cloudAppSecurityType $PolicyErrors++ } else { $Status = "Supported" $SettingValue = "Not Configured" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $ConditionalAccessPolicy.displayName PolicyState = $PolicyState Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $ConditionalAccessPolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 20: Access controls > Session > Sign-in frequency $ID = 20 $Setting = "SignInFrequency" $SettingDescription = "Access controls > Session > Sign-in frequency" $Comment = "" if ($ConditionalAccessPolicy.SessionControls.SignInFrequency.isEnabled -eq "true") { $Status = "Warning" $SettingValue = "" + $ConditionalAccessPolicy.SessionControls.SignInFrequency.Value + " " + $ConditionalAccessPolicy.SessionControls.SignInFrequency.Type $Comment = "Users will be signout from Teams Device every " + $ConditionalAccessPolicy.SessionControls.SignInFrequency.Value + " " + $ConditionalAccessPolicy.SessionControls.SignInFrequency.Type $PolicyWarnings++ } else { $Status = "Supported" $SettingValue = "Not Configured" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $ConditionalAccessPolicy.displayName PolicyState = $PolicyState Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $ConditionalAccessPolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed') [void]$output.Add($SettingPSObj) #endregion #region 21: Access controls > Session > Persistent browser session $ID = 21 $Setting = "PersistentBrowser" $SettingDescription = "Access controls > Session > Persistent browser session" $Comment = "" if ($ConditionalAccessPolicy.SessionControls.PersistentBrowser.isEnabled -eq "true") { $Status = "Unsupported" $SettingValue = $ConditionalAccessPolicy.SessionControls.persistentBrowser.mode $PolicyErrors++ } else { $Status = "Supported" $SettingValue = "Not Configured" } $SettingPSObj = [PSCustomObject]@{ PolicyName = $ConditionalAccessPolicy.displayName PolicyState = $PolicyState Setting = $Setting Value = $SettingValue TeamsDevicesStatus = $Status Comment = $Comment SettingDescription = $SettingDescription AssignedToGroup = $outAssignedToGroup ExcludedFromGroup = $outExcludedFromGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroupList = $ExcludedFromGroup PolicyID = $ConditionalAccessPolicy.id ID = $ID } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed') [void]$output.Add($SettingPSObj) #endregion if ($PolicyErrors -gt 0) { $StatusSum = "Has " + $PolicyErrors + " unsupported settings." $displayWarning = $true } elseif ($PolicyWarnings -gt 0) { $StatusSum = "Has " + $PolicyWarnings + " settings that may impact users." $displayWarning = $true } else { $StatusSum = "All settings supported." } $PolicySum = [PSCustomObject]@{ PolicyID = $ConditionalAccessPolicy.id PolicyName = $ConditionalAccessPolicy.DisplayName PolicyState = $PolicyState AssignedToGroup = $outAssignedToGroup AssignedToGroupList = $AssignedToGroup ExcludedFromGroup = $outExcludedFromGroup ExcludedFromGroupList = $ExcludedFromGroup TeamsDevicesStatus = $StatusSum } $PolicySum.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicy') [void]$outputSum.Add($PolicySum) } else { $skippedCAPolicies++ } } if ($totalCAPolicies -eq 0) { if ($UserUPN) { Write-Warning ("The user " + $UserUPN + " doesn't have any Compliance Policies assigned.") } else { Write-Warning "No Conditional Access Policies assigned to All Users, All Devices or group found. Please use Test-UcTeamsDevicesConditionalAccessPolicy -IgnoreAssigment to check all policies." } } if ($IncludeSupported -and $Detailed) { if ($ExportCSV) { $output | Sort-Object PolicyName, ID | Select-Object PolicyName, PolicyID, PolicyState, AssignedToGroup, ExcludedFromGroup, TeamsDevicesStatus, Setting, SettingDescription, Value, Comment | Export-Csv -path $OutputFullPath -NoTypeInformation Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan return } else { $output | Sort-Object PolicyName, ID } } elseif ($Detailed) { if ((( $output | Where-Object -Property TeamsDevicesStatus -NE -Value "Supported").count -eq 0) -and !$IncludeSupported) { Write-Warning "No unsupported settings found, please use Test-UcTeamsDevicesConditionalAccessPolicy -IncludeSupported, to output all settings." } else { if ($ExportCSV) { $output | Where-Object -Property TeamsDevicesStatus -NE -Value "Supported" | Sort-Object PolicyName, ID | Select-Object PolicyName, PolicyID, PolicyState, AssignedToGroup, ExcludedFromGroup, TeamsDevicesStatus, Setting, SettingDescription, Value, Comment | Export-Csv -path $OutputFullPath -NoTypeInformation Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan } else { $output | Where-Object -Property TeamsDevicesStatus -NE -Value "Supported" | Sort-Object PolicyName, ID } } } else { if (($skippedCAPolicies -gt 0) -and !$All) { Write-Warning ("Skipping $skippedCAPolicies conditional access policies since they will not be applied to Teams Devices") Write-Warning ("Please use the All switch to check all policies: Test-UcTeamsDevicesConditionalAccessPolicy -All") } if ($displayWarning) { Write-Warning "One or more policies contain unsupported settings, please use Test-UcTeamsDevicesConditionalAccessPolicy -Detailed to identify the unsupported settings." } $outputSum | Sort-Object PolicyName } } } } #EndRegion '.\Public\Test-UcTeamsDevicesConditionalAccessPolicy.ps1' 846 #Region '.\Public\Test-UcTeamsDevicesEnrollmentPolicy.ps1' -1 function Test-UcTeamsDevicesEnrollmentPolicy { param( [string]$UserUPN, [switch]$Detailed, [switch]$ExportCSV, [string]$OutputPath ) <# .SYNOPSIS Validate Intune Enrollment Policies that are supported by Microsoft Teams Android Devices .DESCRIPTION This function will validate each setting in a Conditional Access Policy to make sure they are in line with the supported settings: Contributors: David Paulino, Gonçalo Sepulveda Requirements: Microsoft Graph PowerShell Module (Install-Module Microsoft.Graph) .PARAMETER UserUPN Specifies a UserUPN that we want to check for a user enrollment policies. .PARAMETER Detailed Displays test results for unsupported settings in each Intune Enrollment Policy. .PARAMETER ExportCSV When present will export the detailed results to a CSV file. By defautl will save the file under the current user downloads, unless we specify the OutputPath. .PARAMETER OutputPath Allows to specify the path where we want to save the results. .EXAMPLE PS> Test-UcTeamsDevicesEnrollmentPolicy .EXAMPLE PS> Test-UcTeamsDevicesEnrollmentPolicy -UserUPN #> $GraphURI_Users = "https://graph.microsoft.com/v1.0/users" $GraphURI_EnrollmentPolicies = "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations" $GraphURI_AOSPEnrollmentProfiles = "https://graph.microsoft.com/beta/deviceManagement/androidDeviceOwnerEnrollmentProfiles?`$select=displayName,tokenExpirationDateTime&`$filter=enrollmentMode eq 'corporateOwnedAOSPUserAssociatedDevice' and isTeamsDeviceProfile eq true" $output = [System.Collections.ArrayList]::new() if (Test-UcMgGraphConnection -Scopes "DeviceManagementServiceConfig.Read.All", "DeviceManagementConfiguration.Read.All", "Directory.Read.All") { Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null $outFileName = "TeamsDevices_EnrollmentPolicy_Report_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv" if ($OutputPath) { if (!(Test-Path $OutputPath -PathType Container)) { Write-Host ("Error: Invalid folder " + $OutputPath) -ForegroundColor Red return } $OutputFullPath = [System.IO.Path]::Combine($OutputPath, $outFileName) } else { $OutputFullPath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads", $outFileName) } try { Write-Progress -Activity "Test-UcTeamsDevicesEnrollmentPolicy" -Status "Getting Intune Enrollment Policies" $EnrollmentPolicies = (Invoke-MgGraphRequest -Uri $GraphURI_EnrollmentPolicies -Method GET).value Write-Progress -Activity "Test-UcTeamsDevicesEnrollmentPolicy" -Status "Getting AOSP Enrollment Profiles" $AOSPEnrollmentProfiles = (Invoke-MgGraphRequest -Uri $GraphURI_AOSPEnrollmentProfiles -Method GET).value $connectedMSGraph = $true } catch [System.Net.Http.HttpRequestException] { if ($PSItem.Exception.Response.StatusCode -eq "Unauthorized") { Write-Error "Access Denied, please make sure the user connecing to MS Graph is part of one of the following Global Reader/Intune Service Administrator/Global Administrator roles" } else { Write-Error $PSItem.Exception.Message } } catch { Write-Error 'Please connect to MS Graph with Connect-MgGraph -Scopes "DeviceManagementServiceConfig.Read.All","Directory.Read.All" before running this script' } if ($connectedMSGraph) { if ($UserUPN) { try { $UserGroups = (Invoke-MgGraphRequest -Uri ($GraphURI_Users + "/" + $userUPN + "/transitiveMemberOf?`$select=id") -Method GET).value.id } catch [System.Net.Http.HttpRequestException] { if ($PSItem.Exception.Response.StatusCode -eq "NotFound") { Write-warning -Message ("User Not Found: " + $UserUPN) } return } } #We need to cycle this in order to get the Group IDs that are assigned to the enrollment policies $graphRequests = [System.Collections.ArrayList]::new() foreach ($EnrollmentPolicy in $EnrollmentPolicies) { #Only Android Policies if (($EnrollmentPolicy."@odata.type" -eq "#microsoft.graph.deviceEnrollmentPlatformRestrictionConfiguration") -and ($EnrollmentPolicy.platformType -eq "android") ) { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = $EnrollmentPolicy.id method = "GET" url = "/deviceManagement/deviceEnrollmentConfigurations/" + $EnrollmentPolicy.id + "/assignments?`$select=target" } [void]$graphRequests.Add($gRequestTmp) } } $PolicyGroupAssigment = (Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Test-UcTeamsDevicesEnrollmentPolicy, fetching enrollment policies assignment" -IncludeBody) $graphRequests = [System.Collections.ArrayList]::new() foreach ($Group in $PolicyGroupAssigment.body.value.target.GroupID) { if (!($UserUPN) -or ($PolicyGroupAssigment.body.value.target.GroupID -in $UserGroups)) { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = $Group method = "GET" url = "/groups/" + $Group + "?`$select=id,displayName" } [void]$graphRequests.Add($gRequestTmp) } } if ($graphRequests.Count -gt 0) { $Groups = (Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Test-UcTeamsDevicesEnrollmentPolicy, getting group information") | Select-Object Id, displayName } foreach ($EnrollmentPolicy in $EnrollmentPolicies) { $Status = "Not Supported" #This is the default enrollment policy that is applied to all users/devices if ($EnrollmentPolicy."@odata.type" -eq "#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration") { if (!($EnrollmentPolicy.androidRestriction.platformBlocked) -and !($EnrollmentPolicy.androidRestriction.personalDeviceEnrollmentBlocked)) { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PID = 9999 PolicyName = $EnrollmentPolicy.displayName PolicyPriority = "Default" PlatformType = "Android device administrator" AssignedToGroup = "All devices" TeamsDevicesStatus = $Status PlatformBlocked = $EnrollmentPolicy.androidRestriction.platformBlocked PersonalDeviceEnrollmentBlocked = $EnrollmentPolicy.androidRestriction.personalDeviceEnrollmentBlocked osMinimumVersion = $EnrollmentPolicy.androidRestriction.osMinimumVersion osMaximumVersion = $EnrollmentPolicy.androidRestriction.osMaximumVersion blockedManufacturers = $EnrollmentPolicy.androidRestriction.blockedManufacturers TokenExpirationDate = "" } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceEnrollmentPolicy') [void]$output.Add($SettingPSObj) } $Status = "Not Supported" if (($EnrollmentPolicy."@odata.type" -eq "#microsoft.graph.deviceEnrollmentPlatformRestrictionConfiguration") -and ($EnrollmentPolicy.platformType -eq "android") ) { $AssignedToGroup = [System.Collections.ArrayList]::new() $AssignedGroupsTemp = ($PolicyGroupAssigment | Where-Object -Property "id" -Value $EnrollmentPolicy.id -EQ).body.value.target.GroupID foreach ($AssignedGroup in $AssignedGroupsTemp) { if (!($UserUPN) -or ($AssignedGroup -in $UserGroups)) { $GroupDisplayName = ($Groups | Where-Object -Property "id" -Value $AssignedGroup -EQ).DisplayName $GroupEntry = New-Object -TypeName PSObject -Property @{ GroupID = $AssignedGroup GroupDisplayName = $GroupDisplayName } [void]$AssignedToGroup.Add($GroupEntry) } } if ($AssignedToGroup.Count -gt 0) { $outAssignedToGroup = "None" if ($AssignedToGroup.count -eq 1) { $outAssignedToGroup = $AssignedToGroup[0].GroupDisplayName } elseif ($AssignedToGroup.count -gt 1) { $outAssignedToGroup = "" + $AssignedToGroup.count + " group(s)" } if (!($EnrollmentPolicy.platformRestriction.platformBlocked) -and !($EnrollmentPolicy.platformRestriction.personalDeviceEnrollmentBlocked)) { $Status = "Supported" } $SettingPSObj = [PSCustomObject]@{ PID = $EnrollmentPolicy.priority PolicyName = $EnrollmentPolicy.displayName PolicyPriority = $EnrollmentPolicy.priority PlatformType = "Android device administrator" AssignedToGroup = $outAssignedToGroup AssignedToGroupList = $AssignedToGroup TeamsDevicesStatus = $Status PlatformBlocked = $EnrollmentPolicy.platformRestriction.platformBlocked PersonalDeviceEnrollmentBlocked = $EnrollmentPolicy.platformRestriction.personalDeviceEnrollmentBlocked osMinimumVersion = $EnrollmentPolicy.platformRestriction.osMinimumVersion osMaximumVersion = $EnrollmentPolicy.platformRestriction.osMaximumVersion blockedManufacturers = $EnrollmentPolicy.platformRestriction.blockedManufacturers TokenExpirationDate = "" } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceEnrollmentPolicy') [void]$output.Add($SettingPSObj) } } } #region 20240912 - Support for Android AOSP Enrollment #AOSP Enrollment Profiles are not assigned to users. if (!$UserUPN) { $PolicyName = "" $TokenExpirationDate = "" $CurrentDate = Get-Date if ($AOSPEnrollmentProfiles.Length -ge 1) { #In some cases we can have multiple AOSP profiles enabled for Teams Devices, currently we only support one valid at a time. $ValidAOSPEnrollmentProfiles = $AOSPEnrollmentProfiles | Where-Object { $_.tokenExpirationDateTime -gt $CurrentDate } if ($ValidAOSPEnrollmentProfiles.Length -eq 1) { $PolicyName = $ValidAOSPEnrollmentProfiles.displayName $TokenExpirationDate = $ValidAOSPEnrollmentProfiles.tokenExpirationDateTime $TeamsDevicesStatus = "Supported - Token valid for " + ($TokenExpirationDate - $CurrentDate).days + " day(s)" } elseif ($ValidAOSPEnrollmentProfiles.Length -eq 0) { $ExpiredAOSPEnrollmentProfiles = $AOSPEnrollmentProfiles | Sort-Object -Property tokenExpirationDateTime -Descending $TeamsDevicesStatus = "Not Supported - Token Expired on " + ($ExpiredAOSPEnrollmentProfiles[0].tokenExpirationDateTime.ToString([cultureinfo]::CurrentCulture.DateTimeFormat.ShortDatePattern)) } else { $TeamsDevicesStatus = "Not Supported - Multiple AOSP Enrollment Profile enabled for Teams Devices" } } else { $TeamsDevicesStatus = "Not Supported - Missing AOSP Enrollment" } $SettingPSObj = [PSCustomObject]@{ PID = "10000" PolicyName = $PolicyName PolicyPriority = "ASOP" PlatformType = "Android Open Source Project (AOSP)" TeamsDevicesStatus = $TeamsDevicesStatus PlatformBlocked = "" PersonalDeviceEnrollmentBlocked = "" osMinimumVersion = "" osMaximumVersion = "" blockedManufacturers = "" TokenExpirationDate = $TokenExpirationDate } $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceEnrollmentPolicy') [void]$output.Add($SettingPSObj) } #endregion if ($Detailed) { if ($ExportCSV) { $output | Sort-Object PID | Select-Object PolicyName, PolicyPriority, PlatformType, AssignedToGroup, TeamsDevicesStatus, PlatformBlocked, PersonalDeviceEnrollmentBlocked, osMinimumVersion, osMaximumVersion, blockedManufacturers | Export-Csv -path $OutputFullPath -NoTypeInformation Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan return } #20231116 - Fix for empty output. else { $output | Sort-Object PID | Format-List } } else { $output | Sort-Object PID | Format-Table } } } } #EndRegion '.\Public\Test-UcTeamsDevicesEnrollmentPolicy.ps1' 249 #Region '.\Public\Test-UcTeamsOnlyDNSRequirements.ps1' -1 function Test-UcTeamsOnlyDNSRequirements { param( [Parameter(Mandatory = $false)] [string]$Domain, [switch]$All ) <# .SYNOPSIS Check if the DNS records are OK for a TeamsOnly Tenant. .DESCRIPTION This function will check if the DNS entries that were previously required. .PARAMETER Domain Specifies a domain registered with Microsoft 365 .EXAMPLE PS> Test-UcTeamsOnlyDNSRequirements .EXAMPLE PS> Test-UcTeamsOnlyDNSRequirements -Domain uclobby.com #> $outDNSRecords = [System.Collections.ArrayList]::new() if ($Domain) { $365Domains = Get-UcM365Domains -Domain $Domain | Where-Object { $_.Name -notlike "*.onmicrosoft.com" } } else { try { Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null #We only need to validate the Enable domains and exclude *.onmicrosoft.com $365Domains = Get-CsOnlineSipDomain | Where-Object { $_.Status -eq "Enabled" -and $_.Name -notlike "*.onmicrosoft.com" } } catch { Write-Host "Error: Please connect to Microsoft Teams PowerShell before running this cmdlet with Connect-MicrosoftTeams" -ForegroundColor Red return } } $DomainCount = ($365Domains.Count) $i = 1 foreach ($365Domain in $365Domains) { $tmpDomain = $365Domain.Name Write-Progress -Activity "Teams Only DNS Requirements" -Status "Checking: $tmpDomain - $i of $DomainCount" $i++ $DiscoverFQDN = "lyncdiscover." + $365Domain.Name $SIPFQDN = "sip." + $365Domain.Name $SIPTLSFQDN = "_sip._tls." + $365Domain.Name $FederationFQDN = "_sipfederationtls._tcp." + $365Domain.Name $DNSResultDiscover = (Resolve-DnsName $DiscoverFQDN -Type CNAME -ErrorAction Ignore).NameHost $DNSResultSIP = (Resolve-DnsName $SIPFQDN -Type CNAME -ErrorAction Ignore).NameHost $DNSResultSIPTLS = (Resolve-DnsName $SIPTLSFQDN -Type SRV -ErrorAction Ignore).NameTarget $DNSResultFederation = (Resolve-DnsName $FederationFQDN -Type SRV -ErrorAction Ignore) $DNSDiscover = "" if ($DNSResultDiscover -and ($All -or $DNSResultDiscover.contains("online.lync.com") -or $DNSResultDiscover.contains("online.gov.skypeforbusiness.us"))) { $DNSDiscover = $DNSResultDiscover } $DNSSIP = "" if ($DNSResultSIP -and ($All -or $DNSResultSIP.contains("online.lync.com") -or $DNSResultSIP.contains("online.gov.skypeforbusiness.us"))) { $DNSSIP = $DNSResultSIP } $DNSSIPTLS = "" if ($DNSResultSIPTLS -and ($All -or $DNSResultSIPTLS.contains("online.lync.com") -or $DNSResultSIPTLS.contains("online.gov.skypeforbusiness.us"))) { $DNSSIPTLS = $DNSResultSIPTLS } $DNSFederation = "" if ([string]::IsNullOrEmpty($DNSResultFederation.NameTarget)) { $DNSFederation = "Not configured" } elseif (($DNSResultFederation.NameTarget.equals("sipfed.online.lync.com") -or $DNSResultFederation.NameTarget.equals("sipfed.online.gov.skypeforbusiness.us")) -and $DNSResultFederation.Port -eq 5061) { $DNSFederation = "OK" } else { $DNSFederation = "NOK - " + $DNSResultFederation.NameTarget + ":" + $DNSResultFederation.Port } if ($DNSDiscover -or $DNSSIP -or $DNSSIPTLS -or $DNSFederation) { $tmpDNSRecord = New-Object -TypeName PSObject -Property @{ Domain = $365Domain.Name DiscoverRecord = $DNSDiscover SIPRecord = $DNSSIP SIPTLSRecord = $DNSSIPTLS FederationRecord = $DNSFederation } $tmpDNSRecord.PSObject.TypeNames.Insert(0, 'TeamsOnlyDNSRequirements') [void]$outDNSRecords.Add($tmpDNSRecord) } } return $outDNSRecords | Sort-Object Domain } #EndRegion '.\Public\Test-UcTeamsOnlyDNSRequirements.ps1' 94 #Region '.\Public\Update-UcTeamsDevice.ps1' -1 function Update-UcTeamsDevice { [cmdletbinding(SupportsShouldProcess)] param( [ValidateSet("Firmware", "TeamsClient", "All")] [string]$UpdateType = "All", [ValidateSet("Phone", "MTRA", "Display", "Panel")] [string]$DeviceType, [string]$DeviceID, [string]$SoftwareVersion, [string]$InputCSV, [string]$Subnet, [string]$OutputPath, [switch]$ReportOnly ) <# .SYNOPSIS Update Microsoft Teams Devices .DESCRIPTION This function will send update commands to Teams Android Devices using MS Graph. Contributors: Eileen Beato, David Paulino and Bryan Kendrick Requirements: Microsoft Graph PowerShell Module (Install-Module Microsoft.Graph) Microsoft Graph Scopes: "TeamworkDevice.ReadWrite.All","User.Read.All" .PARAMETER DeviceID Specify the Teams Admin Center Device ID that we want to update. .PARAMETER DeviceType Specifies a filter, valid options: Phone - Teams Native Phones MTRA - Microsoft Teams Room Running Android Display - Microsoft Teams Displays Panel - Microsoft Teams Panels .PARAMETER UpdateType Allow to specify which time of update we want to do: Firmware TeamsClient All .PARAMETER SoftwareVersion Allow to specify which version we want to update. .PARAMETER InputCSV When present will use this file as Input, we only need a column with Device Id. It supports files exported from Teams Admin Center (TAC). .PARAMETER Subnet Only available when using InputCSV and requires a “IP Address” column, it allows to only send updates to Teams Android devices within a subnet. Format examples: 10.0.0.0/8 192.168.0.0/24 .PARAMETER OutputPath Allows to specify the path where we want to save the results. By default will save on current user Download. .PARAMETER ReportOnly Will read Teams Device Android versions info and generate a report .EXAMPLE PS> Update-UcTeamsDevice .EXAMPLE PS> Update-UcTeamsDevice -ReportOnly .EXAMPLE PS> Update-UcTeamsDevice -InputCSV C:\Temp\DevicesList_2023-04-20_15-19-00-UTC.csv #> $regExIPAddressSubnet = "^((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9]))\/(3[0-2]|[1-2]{1}[0-9]{1}|[1-9])$" #20241023: If report Only then we dont need write permission on TeamworkDevice $GraphConnected = $false if($ReportOnly){ $GraphConnected = Test-UcMgGraphConnection -Scopes "TeamworkDevice.Read.All","User.Read.All" -AltScopes "TeamworkDevice.Read.All","User.ReadBasic.All" } else { $GraphConnected = Test-UcMgGraphConnection -Scopes "TeamworkDevice.ReadWrite.All","User.Read.All" -AltScopes "TeamworkDevice.ReadWrite.All","User.ReadBasic.All" } if ($GraphConnected) { $outTeamsDevices = [System.Collections.ArrayList]::new() Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null #Checking if the Subnet is valid if ($Subnet) { if (!($Subnet -match $regExIPAddressSubnet)) { Write-Host ("Error: Subnet " + $Subnet + " is invalid, please make sure the subnet is valid and in this format 10.0.0.0/8, 192.168.0.0/24") -ForegroundColor Red return } } if ($ReportOnly) { $outFileName = "UpdateTeamsDevices_ReportOnly_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv" $StatusType = "offline", "critical", "nonUrgent", "healthy" } else { $outFileName = "UpdateTeamsDevices_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv" $StatusType = "critical", "nonUrgent" } #Verify if the Output Path exists if ($OutputPath) { if (!(Test-Path $OutputPath -PathType Container)) { Write-Host ("Error: Invalid folder " + $OutputPath) -ForegroundColor Red return } else { $OutputFullPath = [System.IO.Path]::Combine($OutputPath, $outFileName) } } else { $OutputFullPath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads", $outFileName) } $graphRequests = [System.Collections.ArrayList]::new() if ($DeviceID) { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = $DeviceID method = "GET" url = "/teamwork/devices/" + $DeviceID } [void]$graphRequests.Add($gRequestTmp) $GraphResponse = Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Update-UcTeamsDevices, getting device info" -IncludeBody if ($GraphResponse.status -eq 200) { $TeamsDeviceList = $GraphResponse.body } elseif ($GraphResponse.status -eq 404) { Write-Host ("Error: Device ID $DeviceID not found.") -ForegroundColor Red return } } elseif ($InputCSV) { if (Test-Path $InputCSV) { try { $TeamsDeviceInput = Import-Csv -Path $InputCSV } catch { Write-Host ("Invalid CSV input file: " + $InputCSV) -ForegroundColor Red return } foreach ($TeamsDevice in $TeamsDeviceInput) { $includeDevice = $true if ($Subnet) { $includeDevice = Test-UcIPaddressInSubnet -IPAddress $TeamsDevice.'IP Address' -Subnet $Subnet } if ($includeDevice) { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = $TeamsDevice.'Device Id' method = "GET" url = "/teamwork/devices/" + $TeamsDevice.'Device Id' } [void]$graphRequests.Add($gRequestTmp) } } if ($graphRequests.Count -gt 0) { $TeamsDeviceList = (Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Update-UcTeamsDevices, getting device info" ) } } else { Write-Host ("Error: File not found " + $InputCSV) -ForegroundColor Red return } } else { #Currently only Android based Teams devices are supported. switch ($DeviceType) { "Phone" { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "ipPhone" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'ipPhone'" } [void]$graphRequests.Add($gRequestTmp) $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "lowCostPhone" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'lowCostPhone'" } [void]$graphRequests.Add($gRequestTmp) } "MTRA" { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "collaborationBar" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'collaborationBar'" } [void]$graphRequests.Add($gRequestTmp) $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "touchConsole" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'touchConsole'" } [void]$graphRequests.Add($gRequestTmp) } "Display" { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "teamsDisplay" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'teamsDisplay'" } [void]$graphRequests.Add($gRequestTmp) } "Panel" { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "teamsPanel" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'teamsPanel'" } [void]$graphRequests.Add($gRequestTmp) } Default { #This is the only way to exclude MTRW and SurfaceHub by creating a request per device type. $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "ipPhone" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'ipPhone'" } [void]$graphRequests.Add($gRequestTmp) $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "lowCostPhone" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'lowCostPhone'" } [void]$graphRequests.Add($gRequestTmp) $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "collaborationBar" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'collaborationBar'" } [void]$graphRequests.Add($gRequestTmp) $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "touchConsole" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'touchConsole'" } [void]$graphRequests.Add($gRequestTmp) $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "teamsDisplay" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'teamsDisplay'" } [void]$graphRequests.Add($gRequestTmp) $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = "teamsPanel" method = "GET" url = "/teamwork/devices/?`$filter=deviceType eq 'teamsPanel'" } [void]$graphRequests.Add($gRequestTmp) } } #Using new cmdlet to get a list of devices $TeamsDeviceList = (Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Update-UcTeamsDevices, getting device info").value } $devicesWithUpdatePending = 0 $graphRequests = [System.Collections.ArrayList]::new() foreach ($TeamsDevice in $TeamsDeviceList) { if (($graphRequests.id -notcontains $TeamsDevice.currentuser.id) -and !([string]::IsNullOrEmpty($TeamsDevice.currentuser.id))) { $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = $TeamsDevice.currentuser.id method = "GET" url = "/users/" + $TeamsDevice.currentuser.id } [void]$graphRequests.Add($gRequestTmp) } if ($TeamsDevice.healthStatus -in $StatusType -or $SoftwareVersion) { $devicesWithUpdatePending++ $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = $TeamsDevice.id + "-health" method = "GET" url = "/teamwork/devices/" + $TeamsDevice.id + "/health" } [void]$graphRequests.Add($gRequestTmp) } $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = $TeamsDevice.id + "-operations" method = "GET" url = "/teamwork/devices/" + $TeamsDevice.id + "/operations" } [void]$graphRequests.Add($gRequestTmp) } if ($graphRequests.Count -gt 0) { $graphResponseExtra = Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Update-UcTeamsDevices, getting device health info" -IncludeBody } #In case we detect more than 5 devices with updates pending we will request confirmation that we can continue. if (($devicesWithUpdatePending -ge 5) -and !$ReportOnly) { if ($ConfirmPreference) { $title = 'Confirm' $question = "There are " + $devicesWithUpdatePending + " Teams Devices pending update. Are you sure that you want to continue?" $choices = '&Yes', '&No' $decision = $Host.UI.PromptForChoice($title, $question, $choices, 1) } else { $decision = 0 } if ($decision -ne 0) { return } } $graphRequests = [System.Collections.ArrayList]::new() foreach ($TeamsDevice in $TeamsDeviceList) { if ($TeamsDevice.healthStatus -in $StatusType -or $SoftwareVersion) { $TeamsDeviceHealth = ($graphResponseExtra | Where-Object { $_.id -eq ($TeamsDevice.id + "-health") }).body #Valid types are: adminAgent, operatingSystem, teamsClient, firmware, partnerAgent, companyPortal. #Currently we only consider Firmware and TeamsApp(teamsClient) #Firmware if (($TeamsDeviceHealth.softwareUpdateHealth.firmwareSoftwareUpdateStatus.softwareFreshness.Equals("updateAvailable") -or $SoftwareVersion) -and ($UpdateType -in ("All", "Firmware"))) { if (!($ReportOnly)) { $requestHeader = New-Object 'System.Collections.Generic.Dictionary[string, string]' $requestHeader.Add("Content-Type", "application/json") $requestBody = New-Object 'System.Collections.Generic.Dictionary[string, string]' $requestBody.Add("softwareType", "firmware") if ($SoftwareVersion) { $requestBody.Add("softwareVersion", $SoftwareVersion) } else { $requestBody.Add("softwareVersion", $TeamsDeviceHealth.softwareUpdateHealth.firmwareSoftwareUpdateStatus.availableVersion) } $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = $TeamsDevice.id + "-updateFirmware" method = "POST" url = "/teamwork/devices/" + $TeamsDevice.id + "/updateSoftware" body = $requestBody headers = $requestHeader } [void]$graphRequests.Add($gRequestTmp) } } #TeamsApp if (($TeamsDeviceHealth.softwareUpdateHealth.teamsClientSoftwareUpdateStatus.softwareFreshness.Equals("updateAvailable") -or $SoftwareVersion) -and ($UpdateType -in ("All", "TeamsClient"))) { if (!($ReportOnly)) { $requestHeader = New-Object 'System.Collections.Generic.Dictionary[string, string]' $requestHeader.Add("Content-Type", "application/json") $requestBody = New-Object 'System.Collections.Generic.Dictionary[string, string]' $requestBody.Add("softwareType", "teamsClient") if ($SoftwareVersion) { $requestBody.Add("softwareVersion", $SoftwareVersion) } else { $requestBody.Add("softwareVersion", $TeamsDeviceHealth.softwareUpdateHealth.teamsClientSoftwareUpdateStatus.availableVersion) } $gRequestTmp = New-Object -TypeName PSObject -Property @{ id = $TeamsDevice.id + "-updateTeamsClient" method = "POST" url = "/teamwork/devices/" + $TeamsDevice.id + "/updateSoftware" body = $requestBody headers = $requestHeader } [void]$graphRequests.Add($gRequestTmp) } } } } if ($graphRequests.Count -gt 0) { $updateGraphResponse = Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Update-UcTeamsDevices, sending update commands" -IncludeBody } foreach ($TeamsDevice in $TeamsDeviceList) { if ($TeamsDevice.healthStatus -in $StatusType -or $SoftwareVersion) { $TeamsDeviceHealth = ($graphResponseExtra | Where-Object { $_.id -eq ($TeamsDevice.id + "-health") }).body if ($ReportOnly) { $UpdateStatus = "Report Only:" $pendingUpdate = $false if ($TeamsDeviceHealth.softwareUpdateHealth.firmwareSoftwareUpdateStatus.softwareFreshness.Equals("updateAvailable") -and ($UpdateType -in ("All", "Firmware"))) { $UpdateStatus += " Firmware Update Pending;" $pendingUpdate = $true } if ($TeamsDeviceHealth.softwareUpdateHealth.teamsClientSoftwareUpdateStatus.softwareFreshness.Equals("updateAvailable") -and ($UpdateType -in ("All", "TeamsClient"))) { $UpdateStatus += " Teams App Update Pending;" $pendingUpdate = $true } if (!$pendingUpdate) { $UpdateStatus = "Report Only: No firmware or Teams App updates pending." } } else { $tmpUpdateStatus = ($updateGraphResponse | Where-Object { $_.id -eq ($TeamsDevice.id + "-updateFirmware") }) $tmpUpdateRequest = $graphRequests | Where-Object { $_.id -eq ($TeamsDevice.id + "-updateFirmware") } if ($tmpUpdateStatus.status -eq 202) { $UpdateStatus = "Firmware update request to version " + $tmpUpdateRequest.body.softwareVersion + " was queued." } elseif ($tmpUpdateStatus.status -eq 404) { $UpdateStatus = "Unknown/Invalid firmware version: " + $tmpUpdateRequest.body.softwareVersion } elseif ($tmpUpdateStatus.status -eq 409) { $UpdateStatus = "There is a firmware update pending, please check the update status." } $tmpUpdateStatus = ($updateGraphResponse | Where-Object { $_.id -eq ($TeamsDevice.id + "-updateTeamsClient") }) $tmpUpdateRequest = $graphRequests | Where-Object { $_.id -eq ($TeamsDevice.id + "-updateTeamsClient") } if ($tmpUpdateStatus.status -eq 202) { $UpdateStatus = "Teams App update request to version " + $tmpUpdateRequest.body.softwareVersion + " was queued." } elseif ($tmpUpdateStatus.status -eq 404) { $UpdateStatus = "Unknown/Invalid Teams App version: " + $tmpUpdateRequest.body.softwareVersion } elseif ($tmpUpdateStatus.status -eq 409) { $UpdateStatus = "There is a Teams App update pending, please check the update status." } } $userUPN = ($graphResponseExtra | Where-Object { $_.id -eq $TeamsDevice.currentuser.id }).body.userPrincipalName $TeamsDeviceOperations = ($graphResponseExtra | Where-Object { $_.id -eq ($TeamsDevice.id + "-operations") }).body.value $LastUpdateStatus = "" $LastUpdateInitiatedBy = "" $LastUpdateModifiedDate = "" #In this case we only need the last time we tried to udpdate. foreach ($TeamsDeviceOperation in $TeamsDeviceOperations) { if ($TeamsDeviceOperation.operationType -eq 'softwareUpdate') { $LastUpdateStatus = $TeamsDeviceOperation.status $LastUpdateInitiatedBy = $TeamsDeviceOperation.createdBy.user.displayName $LastUpdateModifiedDate = $TeamsDeviceOperation.lastActionDateTime break; } } $TDObj = New-Object -TypeName PSObject -Property @{ TACDeviceID = $TeamsDevice.id UserDisplayName = $TeamsDevice.currentUser.displayName UserUPN = $userUPN DeviceType = Convert-UcTeamsDeviceType $TeamsDevice.deviceType Manufacturer = $TeamsDevice.hardwaredetail.manufacturer Model = $TeamsDevice.hardwaredetail.model HealthStatus = $TeamsDevice.healthStatus TeamsAdminAgentCurrentVersion = $TeamsDeviceHealth.softwareUpdateHealth.adminAgentSoftwareUpdateStatus.currentVersion TeamsAdminAgentAvailableVersion = $TeamsDeviceHealth.softwareUpdateHealth.adminAgentSoftwareUpdateStatus.availableVersion FirmwareCurrentVersion = $TeamsDeviceHealth.softwareUpdateHealth.firmwareSoftwareUpdateStatus.currentVersion FirmwareAvailableVersion = $TeamsDeviceHealth.softwareUpdateHealth.firmwareSoftwareUpdateStatus.availableVersion CompanyPortalCurrentVersion = $TeamsDeviceHealth.softwareUpdateHealth.companyPortalSoftwareUpdateStatus.currentVersion CompanyPortalAvailableVersion = $TeamsDeviceHealth.softwareUpdateHealth.companyPortalSoftwareUpdateStatus.availableVersion OEMAgentAppCurrentVersion = $TeamsDeviceHealth.softwareUpdateHealth.partnerAgentSoftwareUpdateStatus.currentVersion OEMAgentAppAvailableVersion = $TeamsDeviceHealth.softwareUpdateHealth.partnerAgentSoftwareUpdateStatus.availableVersion TeamsAppCurrentVersion = $TeamsDeviceHealth.softwareUpdateHealth.teamsClientSoftwareUpdateStatus.currentVersion TeamsAppAvailableVersion = $TeamsDeviceHealth.softwareUpdateHealth.teamsClientSoftwareUpdateStatus.availableVersion UpdateStatus = $UpdateStatus #PreviousUpdate PreviousUpdateStatus = $LastUpdateStatus PreviousUpdateInitiatedBy = $LastUpdateInitiatedBy PreviousUpdateModifiedDate = $LastUpdateModifiedDate } $TDObj.PSObject.TypeNames.Insert(0, 'UpdateTeamsDevice') [void]$outTeamsDevices.Add($TDObj) } } if ( $outTeamsDevices.Count -eq 1) { return $outTeamsDevices } elseif ( $outTeamsDevices.Count -gt 1) { $outTeamsDevices | Sort-Object DeviceType, Manufacturer, Model | Select-Object TACDeviceID, UserDisplayName, UserUPN, DeviceType, Manufacturer, Model, HealthStatus, TeamsAdminAgentCurrentVersion, TeamsAdminAgentAvailableVersion, FirmwareCurrentVersion, FirmwareAvailableVersion, CompanyPortalCurrentVersion, CompanyPortalAvailableVersion, OEMAgentAppCurrentVersion, OEMAgentAppAvailableVersion, TeamsAppCurrentVersion, TeamsAppAvailableVersion, PreviousUpdateStatus, PreviousUpdateInitiatedBy, PreviousUpdateModifiedDate, UpdateStatus | Export-Csv -path $OutputFullPath -NoTypeInformation Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan } else { Write-Host ("No Teams Device(s) found that have pending update.") -ForegroundColor Cyan } } } #EndRegion '.\Public\Update-UcTeamsDevice.ps1' 472 |