UCLobbyTeams.psm1
#Region '.\Private\Convert-UcTeamsDeviceType.ps1' 0 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\Invoke-UcMgGraphBatch.ps1' 0 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 = (c)[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) + '] }' } $GraphResponses += (Invoke-MgGraphRequest -Method Post -Uri $GraphURI_Batch -Body $grapRequestBody).responses } for ($j = 0; $j -lt $GraphResponses.length; $j++) { #In some cases we will need the complete graph response, in that case the calling function will have to process pending pages. if($IncludeBody){ $outBatchResponses += $GraphResponses[$j] } else { $outBatchResponses += $GraphResponses[$j].body #Checking if there are more pages available $GraphURI_NextPage = $GraphResponses[$j].body.'@odata.nextLink' while (![string]::IsNullOrEmpty($GraphURI_NextPage)) { $graphNextPageResponse = Invoke-MgGraphRequest -Method Get -Uri $GraphURI_NextPage $outBatchResponses += $graphNextPageResponse $GraphURI_NextPage = $graphNextPageResponse.'@odata.nextLink' } } } return $outBatchResponses } #EndRegion '.\Private\Invoke-UcMgGraphBatch.ps1' 67 #Region '.\Private\Test-UcIPAddressInSubnet.ps1' 0 function ConvertTo-IPv4MaskString { <# .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 #> param( [parameter(Mandatory = $true)] [ValidateRange(0, 32)] [Int] $MaskBits ) $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 "." } Function Test-UcIPaddressInSubnet { param( [string]$IPAddress, [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 { $Subnet -match $regExIPAddressSubnet | Out-Null $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' 49 #Region '.\Private\Test-UcMgGraphConnection.ps1' 0 <# .SYNOPSIS Test connection to Microsoft Graph .DESCRIPTION This function will validate if the current connection to Microsoft Graph has the required Scopes and will attempt to connect if we are missing one. 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 .EXAMPLE PS> Connect-UcMgGraph "TeamworkDevice.Read.All","Directory.Read.All" #> function Test-UcMgGraphConnection { Param( [string[]]$Scopes ) #Checking if Microsoft.Graph is installed if(!(Get-Module Microsoft.Graph.Authentication -ListAvailable)){ Write-Warning ("Missing Microsoft.Graph PowerShell module. Please install it with:" + [Environment]::NewLine + "Install-Module Microsoft.Graph") return $false } #Checking if we have the required scopes. $currentScopes = (Get-MgContext).Scopes $strScope ="" $missingScope = $false foreach($scope in $Scopes){ $strScope += "`"" + $scope + "`"," if(($scope -notin $currentScopes) -and $currentScopes){ $missingScope = $true Write-Warning -Message ("Missing Scope in Microsoft Graph: " + $scope) } } 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){ Write-Warning ("Some scopes are missing 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 -Scopes " + $strScope.Substring(0,$strScope.Length -1) + " -Environment USGov") return $false } else { return $true } } #EndRegion '.\Private\Test-UcMgGraphConnection.ps1' 54 #Region '.\Public\Get-UcArch.ps1' 0 <# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #> <# .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 #> Function Get-UcArch([string]$FilePath) { 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' 64 #Region '.\Public\Get-UcM365Domains.ps1' 0 <# .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 #> Function Get-UcM365Domains { Param( [Parameter(Mandatory = $true)] [string]$Domain ) $regex = "^(.*@)(.*[.].*)$" $outDomains = [System.Collections.ArrayList]::new() try { Test-UcModuleUpdateAvailable -ModuleName UcLobbyTeams $AllowedAudiences = Invoke-WebRequest -Uri ("https://accounts.accesscontrol.windows.net/" + $Domain + "/metadata/json/1") | ConvertFrom-Json | Select-Object -ExpandProperty allowedAudiences 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 [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 { Write-Warning "Unknown error while checking domain: $Domain" } return $outDomains } #EndRegion '.\Public\Get-UcM365Domains.ps1' 47 #Region '.\Public\Get-UcM365TenantId.ps1' 0 <# .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 #> Function Get-UcM365TenantId { Param( [Parameter(Mandatory = $true)] [string]$Domain ) $regexTenantID = "^(.*@)(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})$" $regexOnMicrosoftDomain = "^(.*@)(?!.*mail)(.*.onmicrosoft.com)$" try { Test-UcModuleUpdateAvailable -ModuleName UcLobbyTeams $AllowedAudiences = Invoke-WebRequest -Uri ("https://accounts.accesscontrol.windows.net/" + $Domain + "/metadata/json/1") | 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' 61 #Region '.\Public\Get-UcTeamsDevice.ps1' 0 <# .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 #> Function Get-UcTeamsDevice { Param( [ValidateSet("Phone","MTR","MTRA","MTRW","SurfaceHub","Display","Panel","SIPPhone")] [string]$Filter, [switch]$Detailed, [switch]$ExportCSV, [string]$OutputPath ) $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"){ Test-UcModuleUpdateAvailable -ModuleName UcLobbyTeams $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) } foreach($TeamsDevice in $TeamsDeviceList){ $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 $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.subString(0,$outMacAddress.length-1) DeviceHealth = $TeamsDevice.healthStatus WhenCreated = $TeamsDevice.createdDateTime WhenChanged = $TeamsDevice.lastModifiedDateTime ChangedByUser = $TeamsDevice.lastModifiedBy.user.displayName #Activity ActivePeripherals = $TeamsDeviceActivity.activePeripherals #Configuration LastUpdate = $TeamsDeviceConfiguration.createdDateTime 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 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 } $TDObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceList') } $outTeamsDevices.Add($TDObj) | Out-Null } #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' 342 #Region '.\Public\Get-UcTeamsForest.ps1' 0 <# .SYNOPSIS Get Teams Forest .DESCRIPTION This function returns the forest for a SIP enabled domain. .PARAMETER Domain Specifies a domain registered with Microsoft 365 .EXAMPLE PS> Get-UcTeamsForest -Domain uclobby.com #> Function Get-UcTeamsForest { Param( [Parameter(Mandatory = $true)] [string]$Domain ) $regex = "^.*[redirect].*(webdir)(\w*)(.online.lync.com).*$" try { Test-UcModuleUpdateAvailable -ModuleName UcLobbyTeams $WebRequest = Invoke-WebRequest -Uri ("https://webdir.online.lync.com/AutoDiscover/AutoDiscoverservice.svc/root?originalDomain=" + $Domain) $temp = [regex]::Match($WebRequest, $regex).captures.groups $result = New-Object -TypeName PSObject -Property @{ Domain = $Domain Forest = $temp[2].Value MigrationURL = "https://admin" + $temp[2].Value + ".online.lync.com/HostedMigration/hostedmigrationService.svc" } return $result } catch { if ($Error[0].Exception.Message -like "*404*") { Write-Warning ($Domain + " is not enabled for SIP." ) } } } #EndRegion '.\Public\Get-UcTeamsForest.ps1' 39 #Region '.\Public\Get-UcTeamsVersion.ps1' 0 <# .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 #> Function Get-UcTeamsVersion { Param( [string]$Path, [string]$Computer, [System.Management.Automation.PSCredential]$Credential, [switch]$SkipModuleCheck ) $regexVersion = '("version":")([0-9.]*)' $regexRing = '("ring":")(\w*)' $regexEnv = '("environment":")(\w*)' $regexCloudEnv = '("cloudEnvironment":")(\w*)' $regexRegion = '("region":")([a-zA-Z0-9._-]*)' $regexWindowsUser = '("upnWindowUserUpn":")([a-zA-Z0-9@._-]*)' $regexTeamsUserName = '("userName":")([a-zA-Z0-9@._-]*)' $outTeamsVersion = [System.Collections.ArrayList]::new() if(!$SkipModuleCheck){ Test-UcModuleUpdateAvailable -ModuleName UcLobbyTeams } if ($Path) { if (Test-Path $Path -ErrorAction SilentlyContinue) { $TeamsSettingsFiles = Get-ChildItem -Path $Path -Include "settings.json" -Recurse foreach ($TeamsSettingsFile in $TeamsSettingsFiles) { $TeamsSettings = Get-Content -Path $TeamsSettingsFile.FullName $Version = "" $Ring = "" $Env = "" $CloudEnv = "" $Region = "" 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 } $RegionTemp = [regex]::Match($TeamsSettings, $regexRegion).captures.groups if ($RegionTemp.Count -ge 2) { $Region = $RegionTemp[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 Version = $Version Ring = $Ring Environment = $Env CloudEnvironment = $CloudEnv Region = $Region Path = $TeamsSettingsFile.Directory.FullName } $TeamsVersion.PSObject.TypeNames.Insert(0, 'TeamsVersionFromPath') $outTeamsVersion.Add($TeamsVersion) | Out-Null } } 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 $ProfileName = (New-Object System.Security.Principal.SecurityIdentifier($UserProfile.PSChildName)).Translate( [System.Security.Principal.NTAccount]).Value } $TeamsSettingPath = $ProfilePath + "\AppData\Roaming\Microsoft\Teams\settings.json" if (Test-Path $TeamsSettingPath -ErrorAction SilentlyContinue) { $TeamsSettings = Get-Content -Path $TeamsSettingPath $Version = "" $Ring = "" $Env = "" $CloudEnv = "" $Region = "" 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 } $RegionTemp = [regex]::Match($TeamsSettings, $regexRegion).captures.groups if ($RegionTemp.Count -ge 2) { $Region = $RegionTemp[2].value } } catch { } $TeamsApp = $ProfilePath + "\AppData\Local\Microsoft\Teams\current\Teams.exe" $InstallDateStr = Get-Content ($ProfilePath + "\AppData\Roaming\Microsoft\Teams\installTime.txt") $TeamsVersion = New-Object -TypeName PSObject -Property @{ Computer = $ComputerName Profile = $ProfileName ProfilePath = $ProfilePath Version = $Version Ring = $Ring Environment = $Env CloudEnvironment = $CloudEnv Region = $Region Arch = Get-UcArch $TeamsApp InstallDate = [Datetime]::ParseExact($InstallDateStr, 'M/d/yyyy', $null) | Get-Date -Format $currentDateFormat } $TeamsVersion.PSObject.TypeNames.Insert(0, 'TeamsVersion') $outTeamsVersion.Add($TeamsVersion) | Out-Null } } if ($Credential -and $PSDriveName) { try { Remove-PSDrive -Name $PSDriveName -ErrorAction SilentlyContinue } catch {} } } return $outTeamsVersion } #EndRegion '.\Public\Get-UcTeamsVersion.ps1' 212 #Region '.\Public\Get-UcTeamsVersionBatch.ps1' 0 <# .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 #> Function Get-UcTeamsVersionBatch { Param( [Parameter(Mandatory = $true)] [string]$InputCSV, [string]$OutputPath, [switch]$ExportCSV, [System.Management.Automation.PSCredential]$Credential ) Test-UcModuleUpdateAvailable -ModuleName UcLobbyTeams 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' 80 #Region '.\Public\Get-UcTeamsWithSingleOwner.ps1' 0 <# .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 #> Function 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' 0 <# .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" #> Function Get-UcTeamUsersEmail { [cmdletbinding(SupportsShouldProcess)] Param( [string]$TeamName, [ValidateSet("Owner", "User", "Guest")] [string]$Role ) $output = [System.Collections.ArrayList]::new() Test-UcModuleUpdateAvailable -ModuleName UcLobbyTeams 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-UcModuleUpdateAvailable.ps1' 0 Function Test-UcModuleUpdateAvailable { Param( [Parameter(Mandatory = $true)] [string]$ModuleName ) try { #Get the current loaded version $tmpCurrentVersion = (get-module $ModuleName | Sort-Object Version -Descending) if ($tmpCurrentVersion){ $currentVersion = $tmpCurrentVersion[0].Version.ToString() } #Get the lastest version available $availableVersion = (Find-Module -Name $ModuleName -Repository PSGallery -ErrorAction SilentlyContinue).Version #Get all installed versions $installedVersions = (get-module $ModuleName -ListAvailable).Version 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: Import-Module $ModuleName -RequiredVersion $availableVersion") } else { Write-Warning ("There is a new version available ($availableVersion), please update the module with: Update-Module $ModuleName") } } } catch { } } #EndRegion '.\Public\Test-UcModuleUpdateAvailable.ps1' 30 #Region '.\Public\Test-UcTeamsDevicesCompliancePolicy.ps1' 0 <# .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 #> Function Test-UcTeamsDevicesCompliancePolicy { Param( [switch]$Detailed, [switch]$All, [switch]$IncludeSupported, [string]$PolicyID, [string]$PolicyName, [string]$UserUPN, [string]$DeviceID, [switch]$ExportCSV, [string]$OutputPath ) $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-UcModuleUpdateAvailable -ModuleName UcLobbyTeams $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 } } $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 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 if ($CompliancePolicy."@odata.type" -in $SupportedAndroidCompliancePolicies) { #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 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 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 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 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 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 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 #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' 1058 #Region '.\Public\Test-UcTeamsDevicesConditionalAccessPolicy.ps1' 0 <# .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 #> Function Test-UcTeamsDevicesConditionalAccessPolicy { Param( [switch]$Detailed, [switch]$All, [switch]$IncludeSupported, [string]$UserUPN, [switch]$ExportCSV, [string]$OutputPath ) $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-UcModuleUpdateAvailable -ModuleName UcLobbyTeams $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 #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' 807 #Region '.\Public\Test-UcTeamsOnlyModeReadiness.ps1' 0 <# .SYNOPSIS Check if a Tenant can be changed to TeamsOnly .DESCRIPTION This function will check if there is a lyncdiscover DNS Record that prevents a tenant to be switched to TeamsOnly. .PARAMETER Domain Specifies a domain registered with Microsoft 365 .EXAMPLE PS> Test-UcTeamsOnlyModeReadiness .EXAMPLE PS> Test-UcTeamsOnlyModeReadiness -Domain uclobby.com #> Function Test-UcTeamsOnlyModeReadiness { Param( [Parameter(Mandatory = $false)] [string]$Domain ) $outTeamsOnly = [System.Collections.ArrayList]::new() $connectedMSTeams = $false if ($Domain) { $365Domains = Get-UcM365Domains -Domain $Domain } else { try { Test-UcModuleUpdateAvailable -ModuleName UcLobbyTeams $365Domains = Get-CsOnlineSipDomain $connectedMSTeams = $true } catch { Write-Host "Error: Please Connect to 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 Mode Readiness" -Status "Checking: $tmpDomain - $i of $DomainCount" -PercentComplete (($i / $DomainCount) * 100) $i++ $status = "Not Ready" $DNSRecord = "" $hasARecord = $false $enabledSIP = $false # 20220522 - Skipping if the domain is not SIP Enabled. if (!($connectedMSTeams)) { try { Invoke-WebRequest -Uri ("https://webdir.online.lync.com/AutoDiscover/AutoDiscoverservice.svc/root?originalDomain=" + $tmpDomain) | Out-Null $enabledSIP = $true } catch { if ($Error[0].Exception.Message -like "*404*") { $enabledSIP = $false } } } else { if ($365Domain.Status -eq "Enabled") { $enabledSIP = $true } } if ($enabledSIP) { $DiscoverFQDN = "lyncdiscover." + $365Domain.Name $FederationFQDN = "_sipfederationtls._tcp." + $365Domain.Name $DNSResultA = (Resolve-DnsName $DiscoverFQDN -ErrorAction Ignore -Type A) $DNSResultSRV = (Resolve-DnsName $FederationFQDN -ErrorAction Ignore -Type SRV).NameTarget foreach ($tmpRecord in $DNSResultA) { if (($tmpRecord.NameHost -eq "webdir.online.lync.com") -and ($DNSResultSRV -eq "sipfed.online.lync.com")) { break } if ($tmpRecord.Type -eq "A") { $hasARecord = $true $DNSRecord = $tmpRecord.IPAddress } } if (!($hasARecord)) { $DNSResultCNAME = (Resolve-DnsName $DiscoverFQDN -ErrorAction Ignore -Type CNAME) if ($DNSResultCNAME.count -eq 0) { $status = "Ready" } if (($DNSResultCNAME.NameHost -eq "webdir.online.lync.com") -and ($DNSResultSRV -eq "sipfed.online.lync.com")) { $status = "Ready" $DNSRecord = $DNSResultCNAME.NameHost } else { $DNSRecord = $DNSResultCNAME.NameHost } if ($DNSResultCNAME.Type -eq "SOA") { $status = "Ready" } } $Validation = New-Object -TypeName PSObject -Property @{ DiscoverRecord = $DNSRecord FederationRecord = $DNSResultSRV Status = $status Domain = $365Domain.Name } } else { $Validation = New-Object -TypeName PSObject -Property @{ DiscoverRecord = "" FederationRecord = "" Status = "Not SIP Enabled" Domain = $365Domain.Name } } $Validation.PSObject.TypeNames.Insert(0, 'TeamsOnlyModeReadiness') $outTeamsOnly.Add($Validation) | Out-Null } return $outTeamsOnly } #EndRegion '.\Public\Test-UcTeamsOnlyModeReadiness.ps1' 119 #Region '.\Public\Update-UcTeamsDevice.ps1' 0 <# .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" .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 TeamsApp All .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 #> Function Update-UcTeamsDevice { [cmdletbinding(SupportsShouldProcess)] Param( [ValidateSet("Firmware", "TeamsApp", "All")] [string]$UpdateType = "All", [ValidateSet("Phone", "MTRA", "Display", "Panel")] [string]$DeviceType, [string]$DeviceID, [string]$InputCSV, [string]$Subnet, [string]$OutputPath, [switch]$ReportOnly ) $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])$" if (Test-UcMgGraphConnection -Scopes "TeamworkDevice.ReadWrite.All") { $outTeamsDevices = [System.Collections.ArrayList]::new() Test-UcModuleUpdateAvailable -ModuleName UcLobbyTeams #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) { $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 -gt 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) { $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") -and ($UpdateType.Equals("All") -or $UpdateType.Equals("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") $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") -and ($UpdateType.Equals("All") -or $UpdateType.Equals("TeamsApp"))) { 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") $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) { $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.Equals("All") -or $UpdateType.Equals("Firmware"))) { $UpdateStatus += " Firmware Update Pending;" $pendingUpdate = $true } if ($TeamsDeviceHealth.softwareUpdateHealth.teamsClientSoftwareUpdateStatus.softwareFreshness.Equals("updateAvailable") -and ($UpdateType.Equals("All") -or $UpdateType.Equals("TeamsApp"))) { $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") }) if ($tmpUpdateStatus.status -eq 202) { $UpdateStatus = "Firmware update Command was sent to the device" } 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") }) if ($tmpUpdateStatus.status -eq 202) { $UpdateStatus = "Teams App update Command was sent to the device" } 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 = "" 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 @{ 'Device Id' = $TeamsDevice.id DisplayName = $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 #LastUpdate LastUpdateStatus = $LastUpdateStatus LastUpdateInitiatedBy = $LastUpdateInitiatedBy LastUpdateModifiedDate = $LastUpdateModifiedDate } [void]$outTeamsDevices.Add($TDObj) } } if ( $outTeamsDevices.Count -gt 0) { $outTeamsDevices | Sort-Object DeviceType, Manufacturer, Model | Select-Object 'Device Id', DisplayName, UserUPN, DeviceType, Manufacturer, Model, HealthStatus, TeamsAdminAgentCurrentVersion, TeamsAdminAgentAvailableVersion, FirmwareCurrentVersion, FirmwareAvailableVersion, CompanyPortalCurrentVersion, CompanyPortalAvailableVersion, OEMAgentAppCurrentVersion, OEMAgentAppAvailableVersion, TeamsAppCurrentVersion, TeamsAppAvailableVersion, UpdateStatus, LastUpdateStatus, LastUpdateInitiatedBy, LastUpdateModifiedDate | 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' 440 |