UCLobbyTeams.psm1

#Region '.\Private\Convert-UcTeamsDeviceType.ps1' -1

function Convert-UcTeamsDeviceType {
    param (
        [string]$DeviceType
    )
    switch ($DeviceType) {
        "ipPhone" { return "Phone" }
        "lowCostPhone" { return "Phone" }
        "teamsRoom" { return "MTR Windows" }
        "collaborationBar" { return "MTR Android" }
        "surfaceHub" { return "Surface Hub" }
        "teamsDisplay" { return "Display" }
        "touchConsole" { return "Touch Console (MTRA)" }
        "teamsPanel" { return "Panel" }
        "sip" { return "SIP Phone" }
        Default { return $DeviceType}
    }
}
#EndRegion '.\Private\Convert-UcTeamsDeviceType.ps1' 18
#Region '.\Private\ConvertTo-IPv4MaskString.ps1' -1

function ConvertTo-IPv4MaskString {
    param(
        [parameter(Mandatory = $true)]
        [ValidateRange(0, 32)]
        [Int] $MaskBits
    )
    <#
        .SYNOPSIS
        Converts a number of bits (0-32) to an IPv4 network mask string (e.g., "255.255.255.0").
     
        .DESCRIPTION
        Converts a number of bits (0-32) to an IPv4 network mask string (e.g., "255.255.255.0").
     
        .PARAMETER MaskBits
        Specifies the number of bits in the mask.
 
        Credits to: Bill Stewart - https://www.itprotoday.com/powershell/working-ipv4-addresses-powershell
    #>


    $mask = ([Math]::Pow(2, $MaskBits) - 1) * [Math]::Pow(2, (32 - $MaskBits))
    $bytes = [BitConverter]::GetBytes([UInt32] $mask)
    (($bytes.Count - 1)..0 | ForEach-Object { [String] $bytes[$_] }) -join "."
}
#EndRegion '.\Private\ConvertTo-IPv4MaskString.ps1' 24
#Region '.\Private\Get-UcUPNFromString.ps1' -1

    function Get-UcUPNFromString {
        param (
            [string]$InputStr
        )
        $regexUPN = "([^|]+@[a-zA-Z0-9_\-\.]+\.[a-zA-Z]*)"
        try {
            $RegexTemp = [regex]::Match($InputStr, $regexUPN).captures.groups
            if ($RegexTemp.Count -ge 2) {
                $outUPN = $RegexTemp[1].value
            }
            return $outUPN
        }
        catch {
            return ""
        }
    }
#EndRegion '.\Private\Get-UcUPNFromString.ps1' 17
#Region '.\Private\Invoke-UcMgGraphBatch.ps1' -1

function Invoke-UcMgGraphBatch {
    param(
        [object]$Requests,
        [ValidateSet("beta", "v1.0")]
        [string]$MgProfile,
        [string]$Activity,
        [switch]$IncludeBody
    )
    $GraphURI_BetaAPIBatch = "https://graph.microsoft.com/beta/`$batch"
    $GraphURI_ProdAPIBatch = "https://graph.microsoft.com/v1.0/`$batch"
    $outBatchResponses = [System.Collections.ArrayList]::new()
    $tmpGraphRequests = [System.Collections.ArrayList]::new()

    if ($MgProfile.Equals("beta")) {
        $GraphURI_Batch = $GraphURI_BetaAPIBatch
    }
    else {
        $GraphURI_Batch = $GraphURI_ProdAPIBatch
    }

    #If activity is null then we can use this to get the function that call this function.
    if (!($Activity)) {
        $Activity = [string]$(Get-PSCallStack)[1].FunctionName
    }

    $g = 1
    $batchCount = [int][Math]::Ceiling(($Requests.count / 20))
    foreach ($GraphRequest in $Requests) {
        Write-Progress -Activity $Activity -Status "Running batch $g of $batchCount"
        [void]$tmpGraphRequests.Add($GraphRequest) 
        if ($tmpGraphRequests.Count -ge 20) {
            $g++
            $grapRequestBody = ' { "requests": ' + ($tmpGraphRequests  | ConvertTo-Json) + ' }' 
            $GraphResponses += (Invoke-MgGraphRequest -Method Post -Uri $GraphURI_Batch -Body $grapRequestBody).responses
            $tmpGraphRequests = [System.Collections.ArrayList]::new()
        }
    }

    if ($tmpGraphRequests.Count -gt 0) {
        Write-Progress -Activity $Activity -Status "Running batch $g of $batchCount"
        #TO DO: Look for alternatives instead of doing this.
        if ($tmpGraphRequests.Count -gt 1) {
            $grapRequestBody = ' { "requests": ' + ($tmpGraphRequests | ConvertTo-Json) + ' }' 
        }
        else {
            $grapRequestBody = ' { "requests": [' + ($tmpGraphRequests | ConvertTo-Json) + '] }' 
        }
        try{
        $GraphResponses += (Invoke-MgGraphRequest -Method Post -Uri $GraphURI_Batch -Body $grapRequestBody).responses
        } catch
        {
            Write-Warning "Error while getting the Graph Request."
        }
    }

    #In some cases we will need the complete graph response, in that case the calling function will have to process pending pages.
    $attempts = 1
    for ($j = 0; $j -lt $GraphResponses.length; $j++) {
        $ResponseCount = 0
        if($IncludeBody){
            $outBatchResponses += $GraphResponses[$j]
        } else {
            $outBatchResponses += $GraphResponses[$j].body
            if ($GraphResponses[$j].status -eq "200"){
                #Checking if there are more pages available
                $GraphURI_NextPage = $GraphResponses[$j].body.'@odata.nextLink'
                $GraphTotalCount = $GraphResponses[$j].body.'@odata.count'
                $ResponseCount += $GraphResponses[$j].body.value.count
                while (![string]::IsNullOrEmpty($GraphURI_NextPage)) {
                    try{
                        $graphNextPageResponse = Invoke-MgGraphRequest -Method Get -Uri $GraphURI_NextPage
                        $outBatchResponses += $graphNextPageResponse
                        $GraphURI_NextPage = $graphNextPageResponse.'@odata.nextLink'
                        $ResponseCount += $graphNextPageResponse.value.count
                        Write-Progress -Activity $Activity -Status "$ResponseCount of $GraphTotalCount"
                    } catch {
                        Write-Warning "Failed to get the next batch page, retrying..."
                        $attempts--
                    }
                    if($attempts -eq 0){
                        Write-Warning "Could not get next batch page, skiping it."
                        break
                    }
                }
            } else {
                Write-Warning ("Failed to get Graph Response" + [Environment]::NewLine + `
                "Error Code: " + $GraphResponses[$j].status + " " + $GraphResponses[$j].body.error.code + [Environment]::NewLine + `
                "Error Message: " + $GraphResponses[$j].body.error.message + [Environment]::NewLine + `
                "Request Date: " + $GraphResponses[$j].body.error.innerError.date + [Environment]::NewLine + `
                "Request ID: " + $GraphResponses[$j].body.error.innerError.'request-id' + [Environment]::NewLine + `
                "Client Request Id: " + $GraphResponses[$j].body.error.innerError.'client-request-id')
            } 
        }
    }
    return $outBatchResponses
}
#EndRegion '.\Private\Invoke-UcMgGraphBatch.ps1' 97
#Region '.\Private\Test-UcIPAddressInSubnet.ps1' -1

function Test-UcIPaddressInSubnet {
    <#
        .SYNOPSIS
        Check if an IP address is part of an Subnet.
 
        .DESCRIPTION
        Returns true if the given IP address is part of the subnet, false for not or invalid ip address.
     
        Contributors: David Paulino
 
        .PARAMETER IPAddress
        IP Address that we want to confirm that belongs to a range.
 
        .PARAMETER Subnet
        Subnet in the IPaddress/SubnetMaskBits.
 
        .EXAMPLE
        PS> Test-UcIPaddressInSubnet -IPAddress 192.168.0.1 -Subnet 192.168.0.0/24
 
    #>

    param(
        [Parameter(mandatory = $true)]    
        [string]$IPAddress,
        [Parameter(mandatory = $true)]
        [string]$Subnet
    )
    $regExIPAddressSubnet = "^((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9]))\/(3[0-2]|[1-2]{1}[0-9]{1}|[1-9])$"
    try {
        [void]($Subnet -match $regExIPAddressSubnet)
        $IPSubnet = [ipaddress]$Matches[1]
        $tmpIPAddress = [ipaddress]$IPAddress
        $subnetMask = ConvertTo-IPv4MaskString $Matches[6]
        $tmpSubnet = [ipaddress] ($subnetMask)
        $netidSubnet = [ipaddress]($IPSubnet.address -band $tmpSubnet.address)
        $netidIPAddress = [ipaddress]($tmpIPAddress.address -band $tmpSubnet.address)
        return ($netidSubnet.ipaddresstostring -eq $netidIPAddress.ipaddresstostring)
    }
    catch {
        return $false
    }
}
#EndRegion '.\Private\Test-UcIPAddressInSubnet.ps1' 42
#Region '.\Private\Test-UcMgGraphConnection.ps1' -1

function Test-UcMgGraphConnection {
    param(
        [Parameter(mandatory = $false)]
        [string[]]$Scopes,
        [string[]]$AltScopes,
        [ValidateSet("AppOnly", "Delegated")]
        [string]$AuthType
    )
    <#
        .SYNOPSIS
        Test connection to Microsoft Graph
 
        .DESCRIPTION
        This function will validate if the current connection to Microsoft Graph has the required Scopes.
  
        Contributors: Daniel Jelinek, David Paulino
 
        Requirements: Microsoft Graph PowerShell Module (Install-Module Microsoft.Graph)
 
        .PARAMETER Scopes
        When present it will get detailed information from Teams Devices
    #>

    #Checking if Microsoft.Graph is installed
    if (!(Get-Module Microsoft.Graph.Authentication -ListAvailable)) {
        Write-Warning ("Missing Microsoft.Graph.Authentication PowerShell module. Please install it with:" + [Environment]::NewLine + "Install-Module Microsoft.Graph.Authentication") 
        return $false
    }
    $MgGraphContext = Get-MgContext

    if ($AuthType -and $MgGraphContext.AuthType -ne $AuthType) {
        Write-Warning ("Wrong Permission Type: " + $MgGraphContext.AuthType + ", this PowerShell cmdlet requires: $AuthType") 
        return $false
    }

    #Checking if we have the required scopes.
    $currentScopes = $MgGraphContext.Scopes
 
    $strScope = ""
    $strAltScope = ""
    $missingScopes = ""
    $missingAltScopes = ""
    $missingScope = $false
    $missingAltScope = $false

    foreach ($scope in $Scopes) {
        $strScope += "`"" + $scope + "`","
        if ($scope -notin $currentScopes) {
            $missingScope = $true
            $missingScopes += $scope + ","
        }
    }

    #20231013 - Added option to specify alternative scopes
    if ($missingScope -and $AltScopes) {
        foreach ($altScope in $AltScopes) {
            $strAltScope += "`"" + $altScope + "`","
            if ($altScope -notin $currentScopes) {
                $missingAltScope = $true
                $missingAltScopes += $altScope + ","
            }
        }
        #20231117 - Issue when AltScopes wasnt submitted
    }
    else {
        $missingAltScope = $true
    }

    if (!($currentScopes)) {
        Write-Warning  ("Not Connected to Microsoft Graph" + [Environment]::NewLine + "Please connect to Microsoft Graph before running this cmdlet." + [Environment]::NewLine + "Commercial Tenant: Connect-MgGraph -Scopes " + $strScope.Substring(0, $strScope.Length - 1) + [Environment]::NewLine + "US Gov (GCC-H) Tenant: Connect-MgGraph -Scopes " + $strScope.Substring(0, $strScope.Length - 1) + " -Environment USGov")
        return $false
    }

    #If scopes are missing we need to connect using the required scopes
    if ($missingScope -and $missingAltScope) {
        if ($Scopes -and $AltScopes) {
            Write-Warning  ("Missing scope(s): " + $missingScopes.Substring(0, $missingScopes.Length - 1) + " and missing alternative Scope(s): " + $missingAltScopes.Substring(0, $missingAltScopes.Length - 1) + `
                    [Environment]::NewLine + "Please reconnect to Microsoft Graph before running this cmdlet." + `
                    [Environment]::NewLine + "Commercial Tenant: Connect-MgGraph -Scopes " + $strScope.Substring(0, $strScope.Length - 1) + " or Connect-MgGraph -Scopes " + $strAltScope.Substring(0, $strAltScope.Length - 1) + `
                    [Environment]::NewLine + "US Gov (GCC-H) Tenant: Connect-MgGraph -Environment USGov -Scopes " + $strScope.Substring(0, $strScope.Length - 1) + " or Connect-MgGraph -Environment USGov -Scopes " + $strAltScope.Substring(0, $strAltScope.Length - 1) )
        }
        else {
            Write-Warning  ("Missing scope(s): " + $missingScopes.Substring(0, $missingScopes.Length - 1) + `
                    [Environment]::NewLine + "Please reconnect to Microsoft Graph before running this cmdlet." + `
                    [Environment]::NewLine + "Commercial Tenant: Connect-MgGraph -Scopes " + $strScope.Substring(0, $strScope.Length - 1) + `
                    [Environment]::NewLine + "US Gov (GCC-H) Tenant: Connect-MgGraph -Environment USGov -Scopes " + $strScope.Substring(0, $strScope.Length - 1))
        }
        return $false
    }
    else {
        return $true
    }
}
#EndRegion '.\Private\Test-UcMgGraphConnection.ps1' 93
#Region '.\Private\Test-UcMicrosoftTeamsConnection.ps1' -1

function Test-UcMicrosoftTeamsConnection {
    <#
        .SYNOPSIS
        Test connection to Microsoft Teams PowerShell
 
        .DESCRIPTION
        This function will validate if the current session is connected to Microsoft Teams PowerShell.
    #>

    #Checking if MicrosoftTeams module is installed
    if (!(Get-Module MicrosoftTeams -ListAvailable)) {
        Write-Warning ("Missing MicrosoftTemas PowerShell module. Please install it with:" + [Environment]::NewLine + "Install-Module MicrosoftTeams") 
        return $false
    }
    #We need to use a cmdlet to know if we are connected to MicrosoftTeams PowerShell
    try {
        Get-CsTenant -ErrorAction SilentlyContinue | Out-Null
        return $true
    }
    catch [System.UnauthorizedAccessException] {
        Write-Warning ("Please connect to Microsoft Teams PowerShell with Connect-MicrosoftTeams before running this cmdlet")
        return $false
    }
}
#EndRegion '.\Private\Test-UcMicrosoftTeamsConnection.ps1' 24
#Region '.\Public\Export-UcM365LicenseAssignment.ps1' -1

function Export-UcM365LicenseAssignment {
    param(
        [string]$SKU,    
        [switch]$UseFriendlyNames,
        [switch]$SkipServicePlan,
        [string]$OutputPath,
        [switch]$DuplicateServicePlansOnly
    )
    <#
        .SYNOPSIS
        Generate a report of the User assigned licenses either direct or assigned by group (Inherited)
 
        .DESCRIPTION
        This script will get a report of all Service Plans assigned to users and how the license is assigned to the user (Direct, Inherited)
 
        Contributors: David Paulino, Freydem Fernandez Lopez, Gal Naor
 
        Requirements: Microsoft Graph PowerShell Module (Install-Module Microsoft.Graph)
                        Microsoft Graph Scopes:
                            "Directory.Read.All"
         
        .PARAMETER UseFriendlyNames
        When present will download a csv file containing the License/ServicePlans friendly names
 
        Product names and service plan identifiers for licensing
        https://learn.microsoft.com/en-us/azure/active-directory/enterprise-users/licensing-service-plan-reference
 
        .PARAMETER SkipServicePlan
        When present will just check the licenses and not the service plans assigned to the user.
 
        .PARAMETER OutputPath
        Allows to specify the path where we want to save the results. By default, it will save on current user Download.
 
        .PARAMETER DuplicateServicePlansOnly
        When present the report will be the users that have the same service plan from different assigned licenses.
 
        .EXAMPLE
        PS> Get-UcM365LicenseAssignment
 
        .EXAMPLE
        PS> Get-UcM365LicenseAssignment -UseFriendlyNames
    #>

    $startTime = Get-Date
    if ((Test-UcMgGraphConnection -Scopes "Directory.Read.All" -AltScopes ("User.Read.All", "Organization.Read.All"))) {
        Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null
        $outFile = "M365LicenseAssigment_" 
        #region 20240905 - Users with Duplicate Service Plans
        if($DuplicateServicePlansOnly){
            $outFile += "DuplicateServicePlansOnly_"
        }
        #endregion
        $outFile += (Get-Date).ToString('yyyyMMdd-HHmmss') + ".csv"

        #Verify if the Output Path exists
        if ($OutputPath) {
            if (!(Test-Path $OutputPath -PathType Container)) {
                Write-Host ("Error: Invalid folder " + $OutputPath) -ForegroundColor Red
                return
            }
            $OutputFilePath = [System.IO.Path]::Combine($OutputPath, $outFile)
        }
        else {                
            $OutputFilePath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads", $outFile)
        }
        
        if ($UseFriendlyNames) {
            #20231019 - Change: OutputPath will be for both report and Product names and service plan identifiers for licensing.csv
            $SKUnSPFilePath = [System.IO.Path]::Combine($OutputPath, "Product names and service plan identifiers for licensing.csv")
            if (!(Test-Path -Path $SKUnSPFilePath)) {
                try {
                    Write-Warning "M365 Product Names and Service Plans file not found, attempting to download it."
                    Invoke-WebRequest -Uri "https://download.microsoft.com/download/e/3/e/e3e9faf2-f28b-490a-9ada-c6089a1fc5b0/Product%20names%20and%20service%20plan%20identifiers%20for%20licensing.csv" -OutFile $SKUnSPFilePath
                }
                catch {
                    Write-Warning "Could not download M365 Product Names and Service Plans."
                }
            }
            try {
                $SKUnSP = import-CSV -Path $SKUnSPFilePath
            }
            catch {
                Write-Warning "Could not import Service Plan ID file."
                $UseFriendlyNames = $false
            }
        }

        #region 20240905 - Combined Graph calls for SKUs, Licensed Groups
        #Tenant SKUs - All Licenses that exist in the tenant
        $graphRequests = [System.Collections.ArrayList]::new()
        $gRequestTmp = New-Object -TypeName PSObject -Property @{
            id     = "TenantSKUs"
            method = "GET"
            url    = "/subscribedSkus?`$select=skuID,skuPartNumber,servicePlans,appliesTo,consumedUnits"
        }
        [void]$graphRequests.Add($gRequestTmp)

        #Groups with Licenses Assignment.
        $GraphRequestHeader = New-Object 'System.Collections.Generic.Dictionary[string, string]'
        $GraphRequestHeader.Add("ConsistencyLevel", "eventual")
        $gRequestTmp = New-Object -TypeName PSObject -Property @{
            id      = "GroupsWithLicenses"
            method  = "GET"
            headers = $GraphRequestHeader
            url     = "/groups?`$filter=assignedLicenses/`$count ne 0&`$count=true&`$select=id,displayName,assignedLicenses&`$top=999"
        }
        [void]$graphRequests.Add($gRequestTmp)
        $BatchResponse = Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -IncludeBody -Activity "Get-UcM365LicenseAssignment, Step 1: Getting Tenant License details"

        $tmpGraphResponse = $BatchResponse | Where-Object { $_.id -eq ("TenantSKUs") }
        if ($tmpGraphResponse.status -eq 200) {
            $TenantSKUs = $tmpGraphResponse.body.value
        }
        $tmpGraphResponse = $BatchResponse | Where-Object { $_.id -eq ("GroupsWithLicenses") }
        if ($tmpGraphResponse.status -eq 200) {
            $GroupsWithLicenses = $tmpGraphResponse.body.value
        }
        #endregion

        #region 20232019 - Adding filter to SKU
        if ($SKU) {
            if ($UseFriendlyNames) {
                #20241022 - Change to allow to search using SKU parameter instead of exact match.
                $SKUGUID = ($SKUnSP | Where-Object { $_.String_Id -match $SKU -or $_.Product_Display_Name -match $SKU } | Sort-Object GUID -Unique ).GUID
                $TenantSKUs = $TenantSKUs | Where-Object { $_.skuId -in $SKUGUID -or $_.skuPartNumber -match $SKU}
                if ($TenantSKUs.count -eq 0) {
                    Write-Warning "Could not find `"$SKU`" (SKU Name/Part Number) subscription associated with the tenant."
                    return 
                }
            }
            else {
                #20241022 - Change to allow to search using SKU parameter instead of exact match.
                $TenantSKUs = $TenantSKUs | Where-Object { $_.skuPartNumber -match $SKU }
                if ($TenantSKUs.count -eq 0) {
                    Write-Warning "Could not find `"$SKU`" (SKU Part Number) subscription associated with the tenant."
                    return 
                }
            }
        }
        else {
            $TenantSKUs = $TenantSKUs | Where-Object -Property consumedUnits -GT -Value 0 | Sort-Object skuPartNumber
        }
        #endregion

        #region 20131019 - Getting all Service Plans for new matrix style report
        $allServicePlans = [System.Collections.ArrayList]::new()
        foreach ($TenantSKU in $TenantSKUs) {
            $tmpUserServicePlans = $TenantSKU.ServicePlans | Where-Object -Property appliesTo -EQ -Value "User" 
            foreach ($ServicePlan in $tmpUserServicePlans) {
                if (!($ServicePlan.ServicePlanId -in $allServicePlans.ServicePlanId)) {
                    if ($UseFriendlyNames) {
                        $servicePlanName = ($SKUnSP | Where-Object { $_.Service_Plan_Id -eq $ServicePlan.ServicePlanId -and $_.GUID -eq $TenantSKU.skuID } | Sort-Object Service_Plans_Included_Friendly_Names -Unique).Service_Plans_Included_Friendly_Names
                        if ([string]::IsNullOrEmpty($servicePlanName)) {
                            $servicePlanName = $ServicePlan.servicePlanName    
                        }
                    }
                    else {
                        $servicePlanName = $ServicePlan.ServicePlanName
                    }
                    $tmpSP = New-Object -TypeName PSObject -Property @{
                        servicePlanId   = $ServicePlan.ServicePlanId
                        servicePlanName = $servicePlanName
                    }
                    [void]$allServicePlans.Add($tmpSP)
                }
            }
        }
        #Sorting service plans by name and creating the file header
        $allServicePlans = $allServicePlans | Sort-Object ServicePlanName
        $row = "UserPrincipalName,LicenseAssigned,LicenseAssignment,LicenseAssignmentGroup"
        if (!($SkipServicePlan)) {
            foreach ($ServicePlan in $allServicePlans) {
                $row += "," + $ServicePlan.servicePlanName
            }
        }
        $row += [Environment]::NewLine
        #endregion

        Write-Progress -Id 2 -Activity "Get-UcM365LicenseAssignment, Step 2: Reading users assigned licenses/service plans"
        if ($DuplicateServicePlansOnly) {
            #region 20240905 - Users with Duplicate Service Plans
            #We need to check license per user, this is slower than check per SKU like in Licensing Assignment but required in order to detect duplicates.
            $TotalUsers = 0
            $usersProcessed = 0
            $GraphNextPage = "https://graph.microsoft.com/v1.0/users?`$filter=assignedLicenses/`$count ne 0&`$count=true&`$select=userPrincipalName,licenseAssignmentStates&`$top=999"
            do {
                $GraphResponse = Invoke-MgGraphRequest -Method Get -Uri $GraphNextPage -Headers $GraphRequestHeader
                $GraphNextPage = $GraphResponse.'@odata.nextLink'
                $UsersWithLicenses = $GraphResponse.value
                if (![string]::IsNullOrEmpty($GraphResponse.'@odata.count')) {
                    $TotalUsers = $GraphResponse.'@odata.count'
                }
            
                foreach ($LicensedUser in $UsersWithLicenses) {
                    $usersProcessed++
                    #Update the status every 100 users
                    if (($usersProcessed % 100 -eq 0) -or ($usersProcessed -eq $TotalUsers) -or ($usersProcessed -eq 1) ) {
                        Write-Progress -Id 2 -Activity "Get-UcM365LicenseAssignment, Step 2: Reading users assigned licenses/service plans" -Status "$usersProcessed of $TotalUsers"
                    }
                    #We only need to process users that have 2 or more licenses assigned.
                    if ($LicensedUser.licenseAssignmentStates.count -gt 1) {
                        $tmpUserServicePlans = [System.Collections.ArrayList]::new()
                        foreach ($licenseState in $LicensedUser.licenseAssignmentStates) {
                            #If not a in the Tenant SKUs we can skip it
                            if ($licenseState.skuId -in $TenantSKUs.skuId) {
                                $tmpLicenseInfo = ($TenantSKUs | Where-Object { $_.skuId -eq $licenseState.skuId })
                                $LicenseDisplayName = $tmpLicenseInfo.skuPartNumber
                                if ($UseFriendlyNames) {
                                    $LicenseDisplayName = ($SKUnSP | Where-Object { $_.GUID -eq $licenseState.skuId } | Sort-Object Product_Display_Name -Unique).Product_Display_Name
                                }
                                if ([string]::IsNullOrEmpty($LicenseDisplayName)) {
                                    $LicenseDisplayName = $licenseState.skuId
                                }
                                $licenseAssignment = "Direct"
                                $licenseAssignmentGroup = "NA"
                                if (!([string]::IsNullOrEmpty($licenseState.assignedByGroup))) {
                                    $licenseAssignment = "Inherited"
                                    $licenseAssignmentGroup = ($GroupsWithLicenses | Where-Object -Property "id" -EQ -Value $licenseState.assignedByGroup).displayName
                                    if ([string]::IsNullOrEmpty($licenseAssignmentGroup)) {
                                        $licenseAssignmentGroup = $licenseState.assignedByGroup
                                    }
                                }
                            
                                $SKUUserServicePlans = $tmpLicenseInfo.servicePlans | Where-Object -Property appliesTo -EQ -Value "User" | Sort-Object servicePlanName
                                foreach ($SKUUserServicePlan in $SKUUserServicePlans) {
                                    $SPStatus = "Off"
                                    if ($SKUUserServicePlan.servicePlanId -notin $licenseState.disabledPlans) {
                                        $SPStatus = "On"
                                    }
                                    $ObjUserServicePlans = [PSCustomObject]@{
                                        LicenseSkuId           = $licenseState.skuId
                                        LicenseDisplayName     = $LicenseDisplayName
                                        LicenseAssignment      = $licenseAssignment
                                        LicenseAssignmentGroup = $licenseAssignmentGroup
                                        ServicePlanId          = $SKUUserServicePlan.servicePlanId
                                        ServicePlanName        = $SKUUserServicePlan.servicePlanName
                                        Status                 = $SPStatus
                                    }
                                    [void]$tmpUserServicePlans.Add($ObjUserServicePlans)
                                }
                            }
                        }
                    
                        # Checking if we have more then one Service Plan
                        # In the future we can add filters, like only if both are ON or Ignore Direct/Inherited
                        $skuWithDupServicePlans = $tmpUserServicePlans | Group-Object -Property ServicePlanId | Where-Object { $_.Count -gt 1 } | Select-Object -ExpandProperty Group | Select-Object LicenseSkuId, LicenseDisplayName, LicenseAssignment, LicenseAssignmentGroup  | Sort-Object -Property LicenseDisplayName, LicenseAssignment, LicenseAssignmentGroup -Unique 
                        if (($skuWithDupServicePlans.Count -gt 0)) {
                            foreach ($UserLicenseState in  $skuWithDupServicePlans) {
                                foreach ($ServicePlan in $allServicePlans) {
                                    $tmpSPStatus = $tmpUserServicePlans | Where-Object { $UserLicenseState.LicenseSkuId -eq $_.LicenseSkuId -and $UserLicenseState.LicenseAssignment -eq $_.LicenseAssignment -and $UserLicenseState.LicenseAssignmentGroup -eq $_.LicenseAssignmentGroup -and $_.servicePlanId -eq $servicePlan.servicePlanId }
                                    if ($tmpSPStatus.Status -in ("On", "Off")) {
                                        $userServicePlans += "," + $tmpSPStatus.Status
                                    }
                                    else {
                                        $userServicePlans += ","
                                    }
                                }
                                $row += $LicensedUser.userPrincipalName + "," + $UserLicenseState.LicenseDisplayName + "," + $UserLicenseState.LicenseAssignment + "," + $UserLicenseState.LicenseAssignmentGroup + $userServicePlans
                                Out-File -FilePath $OutputFilePath -InputObject $row -Encoding UTF8 -append
                                $row = ""
                                $userServicePlans = ""
                            }
                        }
                    }
                }
            } while (!([string]::IsNullOrEmpty($GraphNextPage)))
            #endregion
        }
        else {
            #region License Assignment
            foreach ($TenantSKU in $TenantSKUs) {
                $LicenseDisplayName = $TenantSKU.skuPartNumber
                if ($UseFriendlyNames) {
                    $tmpFriendlyName = ($SKUnSP | Where-Object { $_.GUID -eq $TenantSKU.skuID } | Sort-Object Product_Display_Name -Unique).Product_Display_Name
                    #20241022 - To prevent empty name when a license exists in the tenant but the data is not available in "Products names and Services Identifiers" file.
                    if ($tmpFriendlyName) {
                        $LicenseDisplayName = $tmpFriendlyName
                    }  
                }
                $SKUUserServicePlans = $TenantSKU.servicePlans | Where-Object -Property appliesTo -EQ -Value "User" | Sort-Object servicePlanName
                $usersProcessed = 0       
                $GraphRequestURI = "https://graph.microsoft.com/v1.0/users?`$filter=assignedLicenses/any(u:u/skuId eq " + $TenantSKU.skuId + " )&`$select=userPrincipalName,licenseAssignmentStates&`$orderby=userPrincipalName&`$count=true&`$top=999"
                do {
                    try {
                        $UsersWithLicenses = Invoke-MgGraphRequest -Method Get -Uri $GraphRequestURI -Headers $GraphRequestHeader
                        if (![string]::IsNullOrEmpty($UsersWithLicenses.'@odata.count')) {
                            $TotalUsers = $UsersWithLicenses.'@odata.count'
                        }
                        $GraphRequestURI = $UsersWithLicenses.'@odata.nextLink'
                        foreach ($UserWithLicense in $UsersWithLicenses.value) {
                            if (($usersProcessed % 1000 -eq 0) -or ($usersProcessed -eq $TotalUsers)) {
                                Write-Progress -ParentId 2 -Activity "Checking license assignments for $LicenseDisplayName" -Status "$usersProcessed of $TotalUsers"
                            }
                            $tmpLicenseAssignmentStates = $UserWithLicense.licenseAssignmentStates | Where-Object -Property skuId -EQ -Value $TenantSKU.skuId | Sort-Object assignedByGroup
                            foreach ($licenseState in $tmpLicenseAssignmentStates) {
                                $licenseAssignment = "Direct"
                                $licenseAssignmentGroup = ""
                                if (!([string]::IsNullOrEmpty($licenseState.assignedByGroup))) {
                                    $licenseAssignment = "Inherited"
                                    $licenseAssignmentGroup = ($GroupsWithLicenses | Where-Object -Property "id" -EQ -Value $licenseState.assignedByGroup).displayName
                                    if ([string]::IsNullOrEmpty($licenseAssignmentGroup)) {
                                        $licenseAssignmentGroup = $licenseState.assignedByGroup
                                    }
                                }
                                $userServicePlans = ""
                                if (!($SkipServicePlan)) {
                                    foreach ($ServicePlan in $allServicePlans) {
                                        if ($servicePlan.servicePlanId -in $SKUUserServicePlans.servicePlanId) {
                                            if ($servicePlan.servicePlanId -notin $licenseState.disabledPlans) {
                                                $userServicePlans += ",On"
                                            }
                                            else {
                                                $userServicePlans += ",Off"
                                            }
                                        }
                                        else {
                                            $userServicePlans += ","
                                        }
                                    }
                                }
                                $row += $UserWithLicense.userPrincipalName + "," + $LicenseDisplayName + "," + $LicenseAssignment + "," + $LicenseAssignmentGroup + $userServicePlans
                                Out-File -FilePath $OutputFilePath -InputObject $row -Encoding UTF8 -append
                                $row = ""
                            }
                            $usersProcessed++
                        }
                    }
                    catch {
                        Write-Warning ("Failed to get Users with assigned SKU Id: " + $TenantSKU.skuID)
                        $GraphRequestURI = ""
                    }
                } while (![string]::IsNullOrEmpty($GraphRequestURI))
            }
            #endregion
        }

        if ($usersProcessed -gt 0) {
            Write-Host ("Results available in " + $OutputFilePath) -ForegroundColor Cyan
            #region 20231019 - Change: Added execution time to the output.
            $endTime = Get-Date
            $totalSeconds = [math]::round(($endTime - $startTime).TotalSeconds, 2)
            $totalTime = New-TimeSpan -Seconds $totalSeconds
            Write-Host "Execution time:" $totalTime.Hours "Hours" $totalTime.Minutes "Minutes" $totalTime.Seconds "Seconds" -ForegroundColor Green
            #endregion
        }
    }
}
#EndRegion '.\Public\Export-UcM365LicenseAssignment.ps1' 347
#Region '.\Public\Export-UcOneDriveWithMultiplePermissions.ps1' -1

function Export-UcOneDriveWithMultiplePermissions {
    param(
        [string]$OutputPath,
        [switch]$MultiGeo
    )
    <#
        .SYNOPSIS
        Generate a report with OneDrive's that have more than a user with access permissions.
 
        .DESCRIPTION
        This script will check all OneDrives and return the OneDrive that have additional users with permissions.
 
        Author: David Paulino
 
        Requirements: Microsoft Graph Authentication PowerShell Module (Microsoft.Graph.Authentication)
                        Microsoft Graph Scopes:
                            "Sites.Read.All"
                        Note: Currently the SharePoint Sites requires to authenticate to Graph API with AppOnly https://learn.microsoft.com/graph/auth/auth-concepts
         
        .PARAMETER OutputPath
        Allows to specify the path where we want to save the results. By default, it will save on current user Download.
 
        .PARAMETER MultiGeo
        Required if Tenant is MultiGeo
 
        .EXAMPLE
        PS> Get-UcOneDriveWithMultiplePermissions
 
        .EXAMPLE
        PS> Get-UcOneDriveWithMultiplePermissions -MultiGeo
    #>

    $startTime = Get-Date
    if ((Test-UcMgGraphConnection -Scopes "Sites.Read.All" -AltScopes ("Sites.ReadWrite.All") -AuthType "AppOnly" )) {
        Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null
        #Verify if the Output Path exists
        if ($OutputPath) {
            if (!(Test-Path $OutputPath -PathType Container)) {
                Write-Host ("Error: Invalid folder " + $OutputPath) -ForegroundColor Red
                return
            }
            $OutputFilePath = [System.IO.Path]::Combine($OutputPath, $outFile)
        }
        else {                
            $OutputFilePath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads", $outFile)
        }
        #Graph API request is different when the tenant has multigeo
        if ($MultiGeo) {
            $outFile = "OneDrivePermissions_MultiGeo_" + (Get-Date).ToString('yyyyMMdd-HHmmss') + ".csv"
            $GraphRequestSites = "https://graph.microsoft.com/v1.0/sites/getAllSites?`$select=id,displayName,isPersonalSite,WebUrl&`$top=999"
        }
        else {
            $outFile = "OneDrivePermissions_" + (Get-Date).ToString('yyyyMMdd-HHmmss') + ".csv"
            $GraphRequestSites = "https://graph.microsoft.com/v1.0/sites?`$select=id,displayName,isPersonalSite,WebUrl&`$top=999"
        }

        $OneDriveProcessed = 0
        $OneDriveFound = 0 
        $BatchNumber = 1
        $row = "OneDriveDisplayName,OneDriveUrl,Role,UserWithAccessDisplayName,UserWithAccessUPN,UserWithAccessSharePointLogin,OneDriveID,PermissionID" + [Environment]::NewLine
        do {
            try {
                $ResponseSites = Invoke-MgGraphRequest -Method Get -Uri $GraphRequestSites
                $GraphRequestSites = $ResponseSites.'@odata.nextLink'
                #Currently the SharePoint API doenst support filter for isPersonalSite, so we need to filter it
                $tempOneDrives = $ResponseSites.value | Where-Object { $_.isPersonalSite -eq $true }
                #Adding a progress messsage to show status
                foreach ($OneDrive in $tempOneDrives) {
                    if ($OneDriveProcessed % 10 -eq 0) {
                        Write-Progress -Activity "Looking for addtional users in OneDrive permissions" -Status "Batch #$BatchNumber - Number of OneDrives Processed $OneDriveProcessed"
                    }
                    $GROneDrivePermission = "https://graph.microsoft.com/v1.0/sites/" + $OneDrive.id + "/drive/root/permissions"
                    try {
                        $OneDrivePermissions = (Invoke-MgGraphRequest -Method Get -Uri $GROneDrivePermission).value
                        if ($OneDrivePermissions.count -gt 1) {
                            foreach ($OneDrivePermission in $OneDrivePermissions) {
                                if ($OneDrivePermission.grantedToV2.siteuser.displayName -ne $OneDrive.displayName) {
                                    $tempUPN = Get-UcUPNFromString $OneDrivePermission.grantedToV2.siteuser.loginName
                                    $row += $OneDrive.displayName + "," + $OneDrive.WebUrl + "," + $OneDrivePermission.roles + ",`"" + $OneDrivePermission.grantedToV2.siteuser.displayName + "`"," + $tempUPN + "," + $OneDrivePermission.grantedToV2.siteuser.loginName + ",`"" + $OneDrive.id + "`"," + $OneDrivePermission.id
                                    Out-File -FilePath $OutputFilePath -InputObject $row -Encoding UTF8 -append
                                    $row = ""
                                    $OneDriveFound++
                                }
                            }
                        }
                        $OneDriveProcessed++
                    }
                    catch { 
                    }
                }
                $BatchNumber++
            }
            catch { break }
        } while (![string]::IsNullOrEmpty($GraphRequestSites))
        $endTime = Get-Date
        $totalSeconds = [math]::round(($endTime - $startTime).TotalSeconds, 2)
        $totalTime = New-TimeSpan -Seconds $totalSeconds
        Write-Host "Total of OneDrives processed: $OneDriveProcessed, total OneDrives with additional users with permissions: $OneDriveFound" -ForegroundColor Cyan
        if ($OneDriveFound -gt 0) {
            Write-Host ("Results available in " + $OutputFilePath) -ForegroundColor Cyan
        }
        Write-Host "Execution time:" $totalTime.Hours "Hours" $totalTime.Minutes "Minutes" $totalTime.Seconds "Seconds" -ForegroundColor Green
    }
}
#EndRegion '.\Public\Export-UcOneDriveWithMultiplePermissions.ps1' 104
#Region '.\Public\Get-UcArch.ps1' -1

function Get-UcArch {
    param(
        [string]$FilePath
    )
    <#
        .SYNOPSIS
        Funcion to get the Architecture from .exe file
 
        .DESCRIPTION
        Based on PowerShell script Get-ExecutableType.ps1 by David Wyatt, please check the complete script in:
 
        Identify 16-bit, 32-bit and 64-bit executables with PowerShell
        https://gallery.technet.microsoft.com/scriptcenter/Identify-16-bit-32-bit-and-522eae75
 
        .PARAMETER FilePath
        Specifies the executable full file path.
 
        .EXAMPLE
        PS> Get-UcArch -FilePath C:\temp\example.exe
    #>

    try {
        $stream = New-Object System.IO.FileStream(
            $FilePath,
            [System.IO.FileMode]::Open,
            [System.IO.FileAccess]::Read,
            [System.IO.FileShare]::Read )
        $exeType = 'Unknown'
        $bytes = New-Object byte[](4)
        if ($stream.Seek(0x3C, [System.IO.SeekOrigin]::Begin) -eq 0x3C -and $stream.Read($bytes, 0, 4) -eq 4) {
            if (-not [System.BitConverter]::IsLittleEndian) { [Array]::Reverse($bytes, 0, 4) }
            $peHeaderOffset = [System.BitConverter]::ToUInt32($bytes, 0)

            if ($stream.Length -ge $peHeaderOffset + 6 -and
                $stream.Seek($peHeaderOffset, [System.IO.SeekOrigin]::Begin) -eq $peHeaderOffset -and
                $stream.Read($bytes, 0, 4) -eq 4 -and
                $bytes[0] -eq 0x50 -and $bytes[1] -eq 0x45 -and $bytes[2] -eq 0 -and $bytes[3] -eq 0) {
                $exeType = 'Unknown'
                if ($stream.Read($bytes, 0, 2) -eq 2) {
                    if (-not [System.BitConverter]::IsLittleEndian) { [Array]::Reverse($bytes, 0, 2) }
                    $machineType = [System.BitConverter]::ToUInt16($bytes, 0)
                    switch ($machineType) {
                        0x014C { $exeType = 'x86' }
                        0x8664 { $exeType = 'x64' }
                    }
                }
            }
        }
        return $exeType
    }
    catch {
        return "Unknown"
    }
    finally {
        if ($null -ne $stream) { $stream.Dispose() }
    }
}
#EndRegion '.\Public\Get-UcArch.ps1' 57
#Region '.\Public\Get-UcM365Domains.ps1' -1

function Get-UcM365Domains {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Domain
    )
    <#
        .SYNOPSIS
        Get Microsoft 365 Domains from a Tenant
 
        .DESCRIPTION
        This function returns a list of domains that are associated with a Microsoft 365 Tenant.
 
        .PARAMETER Domain
        Specifies a domain registered with Microsoft 365
 
        .EXAMPLE
        PS> Get-UcM365Domains -Domain uclobby.com
    #>

    $regex = "^(.*@)(.*[.].*)$"
    $outDomains = [System.Collections.ArrayList]::new()
    try {
        Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null
        $AllowedAudiences = Invoke-WebRequest -Uri ("https://accounts.accesscontrol.windows.net/" + $Domain + "/metadata/json/1") -UseBasicParsing | ConvertFrom-Json | Select-Object -ExpandProperty allowedAudiences
    }
    catch [System.Net.WebException] {
        if ($PSItem.Exception.Message -eq "The remote server returned an error: (400) Bad Request.") {
            Write-Warning "The domain $Domain is not part of a Microsoft 365 Tenant."
        }
        else {
            Write-Warning $PSItem.Exception.Message
        }
    }
    catch {
        #20240318 - Support for GCC High tenants.
        try {
            $AllowedAudiences = Invoke-WebRequest -Uri ("https://login.microsoftonline.us/" + $Domain + "/metadata/json/1") -UseBasicParsing | ConvertFrom-Json | Select-Object -ExpandProperty allowedAudiences
        }
        catch {
            Write-Warning "Unknown error while checking domain: $Domain"
        }
    }
    try {
        foreach ($AllowedAudience in $AllowedAudiences) {
            $temp = [regex]::Match($AllowedAudience , $regex).captures.groups
            if ($temp.count -ge 2) {
                $tempObj = New-Object -TypeName PSObject -Property @{
                    Name = $temp[2].value
                }
                $outDomains.Add($tempObj) | Out-Null
            }
        }
    }
    catch {
        Write-Warning "Unknown error while checking domain: $Domain"
    }
    return $outDomains
}
#EndRegion '.\Public\Get-UcM365Domains.ps1' 58
#Region '.\Public\Get-UcM365LicenseAssignment.ps1' -1

function Get-UcM365LicenseAssignment {
    param(
        [string]$SKU,    
        [switch]$UseFriendlyNames,
        [switch]$SkipServicePlan,
        [string]$OutputPath,
        [switch]$DuplicateServicePlansOnly
    )
    Write-Warning "Get-UcM365LicenseAssignment will be deprecated in a future release, please use the Export-UcM365LicenseAssignment instead."
    Export-UcM365LicenseAssignment @params
}
#EndRegion '.\Public\Get-UcM365LicenseAssignment.ps1' 12
#Region '.\Public\Get-UcM365TenantId.ps1' -1

function Get-UcM365TenantId {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Domain
    )
    <#
        .SYNOPSIS
        Get Microsoft 365 Tenant Id
 
        .DESCRIPTION
        This function returns the Tenant ID associated with a domain that is part of a Microsoft 365 Tenant.
 
        .PARAMETER Domain
        Specifies a domain registered with Microsoft 365
 
        .EXAMPLE
        PS> Get-UcM365TenantId -Domain uclobby.com
    #>


    $regexTenantID = "^(.*@)(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})$"
    $regexOnMicrosoftDomain = "^(.*@)(?!.*mail)(.*.onmicrosoft.com)$"

    try {
        Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null
        $AllowedAudiences = Invoke-WebRequest -Uri ("https://accounts.accesscontrol.windows.net/" + $Domain + "/metadata/json/1") -UseBasicParsing | ConvertFrom-Json | Select-Object -ExpandProperty allowedAudiences
    }
    catch [System.Net.Http.HttpRequestException] {
        if ($PSItem.Exception.Response.StatusCode -eq "BadRequest") {
            Write-Error "The domain $Domain is not part of a Microsoft 365 Tenant."
        }
        else {
            Write-Error $PSItem.Exception.Message
        }
    }
    catch {
        Write-Error "Unknown error while checking domain: $Domain"
    }
    $output = [System.Collections.ArrayList]::new()
    $OnMicrosoftDomains = [System.Collections.ArrayList]::new()
    $TenantID = ""
    foreach ($AllowedAudience in $AllowedAudiences) {
        $tempTID = [regex]::Match($AllowedAudience , $regexTenantID).captures.groups
        $tempID = [regex]::Match($AllowedAudience , $regexOnMicrosoftDomain).captures.groups
        if ($tempTID.count -ge 2) {
            $TenantID = $tempTID[2].value 
        }
        if ($tempID.count -ge 2) {
            [void]$OnMicrosoftDomains.Add($tempID[2].value)
        }
    }
    #Multi Geo will have multiple OnMicrosoft Domains
    foreach ($OnMicrosoftDomain in $OnMicrosoftDomains) {
        if ($TenantID -and $OnMicrosoftDomain) {
            $M365TidPSObj = [PSCustomObject]@{ TenantID = $TenantID
                OnMicrosoftDomain                       = $OnMicrosoftDomain
            }
            $M365TidPSObj.PSObject.TypeNames.Insert(0, 'M365TenantId')
            [void]$output.Add($M365TidPSObj)
        }
    }
    return $output 
}
#EndRegion '.\Public\Get-UcM365TenantId.ps1' 63
#Region '.\Public\Get-UcOneDriveWithMultiplePermissions.ps1' -1

function Get-UcOneDriveWithMultiplePermissions {
    param(
        [string]$OutputPath,
        [switch]$MultiGeo
    )
    Write-Warning "Get-UcOneDriveWithMultiplePermissions will be deprecated in a future release, please use the Export-UcOneDriveWithMultiplePermissions instead."
    Export-UcOneDriveWithMultiplePermissions @params
}
#EndRegion '.\Public\Get-UcOneDriveWithMultiplePermissions.ps1' 9
#Region '.\Public\Get-UcTeamsDevice.ps1' -1

function Get-UcTeamsDevice {
    param(
        [ValidateSet("Phone", "MTR", "MTRA", "MTRW", "SurfaceHub", "Display", "Panel", "SIPPhone")]
        [string]$Filter,
        [switch]$Detailed,
        [switch]$ExportCSV,
        [string]$OutputPath
    )    
    <#
        .SYNOPSIS
        Get Microsoft Teams Devices information
 
        .DESCRIPTION
        This function fetch Teams Devices provisioned in a M365 Tenant using MS Graph.
         
 
        Contributors: David Paulino, Silvio Schanz, Gonçalo Sepulveda, Bryan Kendrick and Daniel Jelinek
 
        Requirements: Microsoft Graph PowerShell Module (Install-Module Microsoft.Graph)
                        Microsoft Graph Scopes:
                                "TeamworkDevice.Read.All"
                                "User.Read.All"
 
        .PARAMETER Filter
        Specifies a filter, valid options:
            Phone - Teams Native Phones
            MTR - Microsoft Teams Rooms running Windows or Android
            MTRW - Microsoft Teams Room Running Windows
            MTRA - Microsoft Teams Room Running Android
            SurfaceHub - Surface Hub
            Display - Microsoft Teams Displays
            Panel - Microsoft Teams Panels
 
        .PARAMETER Detailed
        When present it will get detailed information from Teams Devices
 
        .PARAMETER ExportCSV
        When present will export the detailed results to a CSV file. By defautl will save the file under the current user downloads, unless we specify the OutputPath.
 
        .PARAMETER OutputPath
        Allows to specify the path where we want to save the results.
 
        .EXAMPLE
        PS> Get-UcTeamsDevice
 
        .EXAMPLE
        PS> Get-UcTeamsDevice -Filter MTR
 
        .EXAMPLE
        PS> Get-UcTeamsDevice -Detailed
 
    #>


    $outTeamsDevices = [System.Collections.ArrayList]::new()

    if ($ExportCSV) {
        $Detailed = $true
    }

    #Verify if the Output Path exists
    if ($OutputPath) {
        if (!(Test-Path $OutputPath -PathType Container)) {
            Write-Host ("Error: Invalid folder " + $OutputPath) -ForegroundColor Red
            return
        } 
    }
    else {                
        $OutputPath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads")
    }

    if (Test-UcMgGraphConnection -Scopes "TeamworkDevice.Read.All,User.Read.All" -AltScopes ("TeamworkDevice.Read.All","Directory.Read.All")) {
        Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null
        $graphRequests = [System.Collections.ArrayList]::new()
        $tmpFileName = "MSTeamsDevices_" + $Filter + "_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv"
        switch ($filter) {
            "Phone" { 
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = "ipPhone"
                    method = "GET"
                    url    = "/teamwork/devices/?`$filter=deviceType eq 'ipPhone'"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = "lowCostPhone"
                    method = "GET"
                    url    = "/teamwork/devices/?`$filter=deviceType eq 'lowCostPhone'"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
            }
            "MTR" {
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = "teamsRoom"
                    method = "GET"
                    url    = "/teamwork/devices/?`$filter=deviceType eq 'teamsRoom'"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = "collaborationBar"
                    method = "GET"
                    url    = "/teamwork/devices/?`$filter=deviceType eq 'collaborationBar'"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = "touchConsole"
                    method = "GET"
                    url    = "/teamwork/devices/?`$filter=deviceType eq 'touchConsole'"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
            }
            "MTRW" {
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = "teamsRoom"
                    method = "GET"
                    url    = "/teamwork/devices/?`$filter=deviceType eq 'teamsRoom'"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
            }
            "MTRA" {            
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = "collaborationBar"
                    method = "GET"
                    url    = "/teamwork/devices/?`$filter=deviceType eq 'collaborationBar'"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = "touchConsole"
                    method = "GET"
                    url    = "/teamwork/devices/?`$filter=deviceType eq 'touchConsole'"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
            }
            "SurfaceHub" {
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = "surfaceHub"
                    method = "GET"
                    url    = "/teamwork/devices/?`$filter=deviceType eq 'surfaceHub'"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
            }
            "Display" {
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = "teamsDisplay"
                    method = "GET"
                    url    = "/teamwork/devices/?`$filter=deviceType eq 'teamsDisplay'"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
            }
            "Panel" {
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = "teamsPanel"
                    method = "GET"
                    url    = "/teamwork/devices/?`$filter=deviceType eq 'teamsPanel'"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
            }
            "SIPPhone" {
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = "sip"
                    method = "GET"
                    url    = "/teamwork/devices/?`$filter=deviceType eq 'sip'"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
            }
            Default {
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = 1
                    method = "GET"
                    url    = "/teamwork/devices"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
                $tmpFileName = "MSTeamsDevices_All_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv"
            }
        }
        
        $TeamsDeviceList = (Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Get-UcTeamsDevice, getting Teams device info").value
        
        #To improve performance we will use batch requests
        $graphRequests = [System.Collections.ArrayList]::new()
        foreach ($TeamsDevice in $TeamsDeviceList) {
            if (($graphRequests.id -notcontains $TeamsDevice.currentuser.id) -and !([string]::IsNullOrEmpty($TeamsDevice.currentuser.id))) {
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = $TeamsDevice.currentuser.id
                    method = "GET"
                    url    = "/users/" + $TeamsDevice.currentuser.id
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
            }
            if ($Detailed) {
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = $TeamsDevice.id + "-activity"
                    method = "GET"
                    url    = "/teamwork/devices/" + $TeamsDevice.id + "/activity"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = $TeamsDevice.id + "-configuration"
                    method = "GET"
                    url    = "/teamwork/devices/" + $TeamsDevice.id + "/configuration"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = $TeamsDevice.id + "-health"
                    method = "GET"
                    url    = "/teamwork/devices/" + $TeamsDevice.id + "/health"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = $TeamsDevice.id + "-operations"
                    method = "GET"
                    url    = "/teamwork/devices/" + $TeamsDevice.id + "/operations"
                }
                $graphRequests.Add($gRequestTmp) | Out-Null
            } 
        }
        if ($graphRequests.Count -gt 0) {
            
            if ($Detailed) {
                $ActivityInfo = "Get-UcTeamsDevice, getting Teams device addtional information (User UPN/Health/Operations/Configurarion)."
            }
            else {
                $ActivityInfo = "Get-UcTeamsDevice, getting Teams device user information."
            }
            $graphResponseExtra = (Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity $ActivityInfo -IncludeBody)
        }
        $devicesProcessed = 0
        foreach ($TeamsDevice in $TeamsDeviceList) {
            $devicesProcessed++
            $userUPN = ($graphResponseExtra | Where-Object { $_.id -eq $TeamsDevice.currentuser.id }).body.userPrincipalName

            if ($Detailed) {
                $TeamsDeviceActivity = ($graphResponseExtra | Where-Object { $_.id -eq ($TeamsDevice.id + "-activity") }).body
                $TeamsDeviceConfiguration = ($graphResponseExtra | Where-Object { $_.id -eq ($TeamsDevice.id + "-configuration") }).body
                $TeamsDeviceHealth = ($graphResponseExtra | Where-Object { $_.id -eq ($TeamsDevice.id + "-health") }).body
                $TeamsDeviceOperations = ($graphResponseExtra | Where-Object { $_.id -eq ($TeamsDevice.id + "-operations") }).body.value

                if ($TeamsDeviceOperations.count -gt 0) {
                    $LastHistoryAction = $TeamsDeviceOperations[0].operationType
                    $LastHistoryStatus = $TeamsDeviceOperations[0].status
                    $LastHistoryInitiatedBy = $TeamsDeviceOperations[0].createdBy.user.displayName
                    $LastHistoryModifiedDate = ($TeamsDeviceOperations[0].lastActionDateTime).ToLocalTime()
                    $LastHistoryErrorCode = $TeamsDeviceOperations[0].error.code
                    $LastHistoryErrorMessage = $TeamsDeviceOperations[0].error.message
                }
                else {
                    $LastHistoryAction = ""
                    $LastHistoryStatus = ""
                    $LastHistoryInitiatedBy = ""
                    $LastHistoryModifiedDate = ""
                    $LastHistoryErrorCode = ""
                    $LastHistoryErrorMessage = ""
                }

                $outMacAddress = ""
                foreach ($macAddress in $TeamsDevice.hardwaredetail.macAddresses) {
                    $outMacAddress += $macAddress + ";"
                }
        
                $TDObj = New-Object -TypeName PSObject -Property @{
                    UserDisplayName               = $TeamsDevice.currentuser.displayName
                    UserUPN                       = $userUPN 
            
                    TACDeviceID                   = $TeamsDevice.id
                    DeviceType                    = Convert-UcTeamsDeviceType $TeamsDevice.deviceType
                    Notes                         = $TeamsDevice.notes
                    CompanyAssetTag               = $TeamsDevice.companyAssetTag
        
                    Manufacturer                  = $TeamsDevice.hardwaredetail.manufacturer
                    Model                         = $TeamsDevice.hardwaredetail.model
                    SerialNumber                  = $TeamsDevice.hardwaredetail.serialNumber 
                    MacAddresses                  = $outMacAddress
                                
                    DeviceHealth                  = $TeamsDevice.healthStatus
                    WhenCreated                   = ($TeamsDevice.createdDateTime).ToLocalTime()
                    WhenChanged                   = ($TeamsDevice.lastModifiedDateTime).ToLocalTime()
                    ChangedByUser                 = $TeamsDevice.lastModifiedBy.user.displayName
        
                    #Activity
                    ActivePeripherals             = $TeamsDeviceActivity.activePeripherals
        
                    #Configuration
                    ConfigurationCreateDate       = ($TeamsDeviceConfiguration.createdDateTime).ToLocalTime()
                    ConfigurationCreatedBy        = $TeamsDeviceConfiguration.createdBy
                    ConfigurationLastModifiedDate = ($TeamsDeviceConfiguration.lastModifiedDateTime).ToLocalTime()
                    ConfigurationLastModifiedBy   = $TeamsDeviceConfiguration.lastModifiedBy
                    DisplayConfiguration          = $TeamsDeviceConfiguration.displayConfiguration
                    CameraConfiguration           = $TeamsDeviceConfiguration.cameraConfiguration.contentCameraConfiguration
                    SpeakerConfiguration          = $TeamsDeviceConfiguration.speakerConfiguration
                    MicrophoneConfiguration       = $TeamsDeviceConfiguration.microphoneConfiguration
                    TeamsClientConfiguration      = $TeamsDeviceConfiguration.teamsClientConfiguration
                    SupportedMeetingMode          = $TeamsDeviceConfiguration.teamsClientConfiguration.accountConfiguration.supportedClient
                    HardwareProcessor             = $TeamsDeviceConfiguration.hardwareConfiguration.processorModel
                    SystemConfiguration           = $TeamsDeviceConfiguration.systemConfiguration
        
                    #Health
                    #20240417 - Added connection fields
                    ConnectionStatus              = $TeamsDeviceHealth.connection.connectionStatus
                    ConnectionLastActivity        = ($TeamsDeviceHealth.connection.lastModifiedDateTime).ToLocalTime()
                    
                    ComputeStatus                 = $TeamsDeviceHealth.hardwareHealth.computeHealth.connection.connectionStatus
                    HdmiIngestStatus              = $TeamsDeviceHealth.hardwareHealth.hdmiIngestHealth.connection.connectionStatus
                    RoomCameraStatus              = $TeamsDeviceHealth.peripheralsHealth.roomCameraHealth.connection.connectionStatus
                    ContentCameraStatus           = $TeamsDeviceHealth.peripheralsHealth.contentCameraHealth.connection.connectionStatus
                    SpeakerStatus                 = $TeamsDeviceHealth.peripheralsHealth.speakerHealth.connection.connectionStatus
                    CommunicationSpeakerStatus    = $TeamsDeviceHealth.peripheralsHealth.communicationSpeakerHealth.connection.connectionStatus
                    #DisplayCollection = $TeamsDeviceHealth.peripheralsHealth.displayHealthCollection.connectionStatus
                    MicrophoneStatus              = $TeamsDeviceHealth.peripheralsHealth.microphoneHealth.connection.connectionStatus

                    TeamsAdminAgentVersion        = $TeamsDeviceHealth.softwareUpdateHealth.adminAgentSoftwareUpdateStatus.currentVersion
                    FirmwareVersion               = $TeamsDeviceHealth.softwareUpdateHealth.firmwareSoftwareUpdateStatus.currentVersion
                    CompanyPortalVersion          = $TeamsDeviceHealth.softwareUpdateHealth.companyPortalSoftwareUpdateStatus.currentVersion
                    OEMAgentAppVersion            = $TeamsDeviceHealth.softwareUpdateHealth.partnerAgentSoftwareUpdateStatus.currentVersion
                    TeamsAppVersion               = $TeamsDeviceHealth.softwareUpdateHealth.teamsClientSoftwareUpdateStatus.currentVersion
                    
                    #LastOperation
                    LastHistoryAction             = $LastHistoryAction
                    LastHistoryStatus             = $LastHistoryStatus
                    LastHistoryInitiatedBy        = $LastHistoryInitiatedBy
                    LastHistoryModifiedDate       = $LastHistoryModifiedDate
                    LastHistoryErrorCode          = $LastHistoryErrorCode
                    LastHistoryErrorMessage       = $LastHistoryErrorMessage 
                }
                $TDObj.PSObject.TypeNames.Insert(0, 'TeamsDevice')
        
            }
            else {
                $TDObj = New-Object -TypeName PSObject -Property @{
                    UserDisplayName = $TeamsDevice.currentuser.displayName
                    UserUPN         = $userUPN 
                    TACDeviceID     = $TeamsDevice.id
                    DeviceType      = Convert-UcTeamsDeviceType $TeamsDevice.deviceType
                    Manufacturer    = $TeamsDevice.hardwaredetail.manufacturer
                    Model           = $TeamsDevice.hardwaredetail.model
                    SerialNumber    = $TeamsDevice.hardwaredetail.serialNumber 
                    MacAddresses    = $TeamsDevice.hardwaredetail.macAddresses
                                
                    DeviceHealth    = $TeamsDevice.healthStatus

                    #20240419 - Adding additional fields that are available on graph api
                    WhenCreated     = ($TeamsDevice.createdDateTime).ToLocalTime()
                    WhenChanged     = ($TeamsDevice.lastModifiedDateTime).ToLocalTime()
                    ChangedByUser   = $TeamsDevice.lastModifiedBy.user.displayName
                }
                $TDObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceList')
            }
            $outTeamsDevices.Add($TDObj) | Out-Null
        }
        #20231020 - We only need to output if we have results.
        if ($devicesProcessed -gt 0) {
            #region: Modified by Daniel Jelinek
            if ($ExportCSV) {
                $OutputFullPath = [System.IO.Path]::Combine($OutputPath, $tmpFileName)
                $outTeamsDevices | Sort-Object DeviceType, Manufacturer, Model | Select-Object TACDeviceID, DeviceType, Manufacturer, Model, UserDisplayName, UserUPN, Notes, CompanyAssetTag, SerialNumber, MacAddresses, WhenCreated, WhenChanged, ChangedByUser, HdmiIngestStatus, ComputeStatus, RoomCameraStatus, SpeakerStatus, CommunicationSpeakerStatus, MicrophoneStatus, SupportedMeetingMode, HardwareProcessor, SystemConfiguratio, TeamsAdminAgentVersion, FirmwareVersion, CompanyPortalVersion, OEMAgentAppVersion, TeamsAppVersion, LastUpdate, LastHistoryAction, LastHistoryStatus, LastHistoryInitiatedBy, LastHistoryModifiedDate, LastHistoryErrorCode, LastHistoryErrorMessage | Export-Csv -path $OutputFullPath -NoTypeInformation
                Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan
            }
            else {
                $outTeamsDevices | Sort-Object DeviceType, Manufacturer, Model
            }
            #endregion
        }
    }
}
#EndRegion '.\Public\Get-UcTeamsDevice.ps1' 362
#Region '.\Public\Get-UcTeamsVersion.ps1' -1

function Get-UcTeamsVersion {
    param(
        [string]$Path,
        [string]$Computer,
        [System.Management.Automation.PSCredential]$Credential,
        [switch]$SkipModuleCheck
    )
    <#
        .SYNOPSIS
        Get Microsoft Teams Desktop Version
 
        .DESCRIPTION
        This function returns the installed Microsoft Teams desktop version for each user profile.
 
        .PARAMETER Path
        Specify the path with Teams Log Files
 
        .PARAMETER Computer
        Specify the remote computer
 
        .PARAMETER Credential
        Specify the credential to be used to connect to the remote computer
 
        .EXAMPLE
        PS> Get-UcTeamsVersion
 
        .EXAMPLE
        PS> Get-UcTeamsVersion -Path C:\Temp\
 
        .EXAMPLE
        PS> Get-UcTeamsVersion -Computer workstation124
 
        .EXAMPLE
        PS> $cred = Get-Credential
        PS> Get-UcTeamsVersion -Computer workstation124 -Credential $cred
    #>


    $regexVersion = '("version":")([0-9.]*)'
    $regexRing = '("ring":")(\w*)'
    $regexEnv = '("environment":")(\w*)'
    $regexCloudEnv = '("cloudEnvironment":")(\w*)'
    
    $regexWindowsUser = '("upnWindowUserUpn":")([a-zA-Z0-9@._-]*)'
    $regexTeamsUserName = '("userName":")([a-zA-Z0-9@._-]*)'

    #20240309 - REGEX to get New Teams version from log file DesktopApp: Version: 23202.1500.2257.3700
    $regexNewVersion = '(DesktopApp: Version: )(\d{5}.\d{4}.\d{4}.\d{4})'

    $outTeamsVersion = [System.Collections.ArrayList]::new()
    if (!$SkipModuleCheck) {
        Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null
    }

    if ($Path) {
        if (Test-Path $Path -ErrorAction SilentlyContinue) {
            #region Teams Classic Path
            $TeamsSettingsFiles = Get-ChildItem -Path $Path -Include "settings.json" -Recurse
            foreach ($TeamsSettingsFile in $TeamsSettingsFiles) {
                $TeamsSettings = Get-Content -Path $TeamsSettingsFile.FullName
                $Version = ""
                $Ring = ""
                $Env = ""
                $CloudEnv = ""
                try {
                    $VersionTemp = [regex]::Match($TeamsSettings, $regexVersion).captures.groups
                    if ($VersionTemp.Count -ge 2) {
                        $Version = $VersionTemp[2].value
                    }
                    $RingTemp = [regex]::Match($TeamsSettings, $regexRing).captures.groups
                    if ($RingTemp.Count -ge 2) {
                        $Ring = $RingTemp[2].value
                    }
                    $EnvTemp = [regex]::Match($TeamsSettings, $regexEnv).captures.groups
                    if ($EnvTemp.Count -ge 2) {
                        $Env = $EnvTemp[2].value
                    }
                    $CloudEnvTemp = [regex]::Match($TeamsSettings, $regexCloudEnv).captures.groups
                    if ($CloudEnvTemp.Count -ge 2) {
                        $CloudEnv = $CloudEnvTemp[2].value
                    }
                }
                catch { }
                $TeamsDesktopSettingsFile = $TeamsSettingsFile.Directory.FullName + "\desktop-config.json"
                if (Test-Path $TeamsDesktopSettingsFile -ErrorAction SilentlyContinue) {
                    $TeamsDesktopSettings = Get-Content -Path $TeamsDesktopSettingsFile
                    $WindowsUser = ""
                    $TeamsUserName = ""
                    $RegexTemp = [regex]::Match($TeamsDesktopSettings, $regexWindowsUser).captures.groups
                    if ($RegexTemp.Count -ge 2) {
                        $WindowsUser = $RegexTemp[2].value
                    }
                    $RegexTemp = [regex]::Match($TeamsDesktopSettings, $regexTeamsUserName).captures.groups
                    if ($RegexTemp.Count -ge 2) {
                        $TeamsUserName = $RegexTemp[2].value
                    }
                }
                $TeamsVersion = New-Object -TypeName PSObject -Property @{
                    WindowsUser      = $WindowsUser
                    TeamsUser        = $TeamsUserName
                    Type             = "Teams Classic"
                    Version          = $Version
                    Ring             = $Ring
                    Environment      = $Env
                    CloudEnvironment = $CloudEnv
                    Path             = $TeamsSettingsFile.Directory.FullName
                }
                $TeamsVersion.PSObject.TypeNames.Insert(0, 'TeamsVersionFromPath')
                $outTeamsVersion.Add($TeamsVersion) | Out-Null
            }
            #endregion
            #region New Teams Path
            $TeamsSettingsFiles = Get-ChildItem -Path $Path -Include "tma_settings.json" -Recurse
            foreach ($TeamsSettingsFile in $TeamsSettingsFiles) {
                if (Test-Path $TeamsSettingsFile -ErrorAction SilentlyContinue) {
                    $NewTeamsSettings = Get-Content -Path $TeamsSettingsFile | ConvertFrom-Json
                    $tmpAccountID = $NewTeamsSettings.primary_user.accounts.account_id
                    try {
                        $Version = ""
                        $MostRecentTeamsLogFile = Get-ChildItem -Path $TeamsSettingsFile.Directory.FullName -Include "MSTeams_*.log" -Recurse | Sort-Object -Property CreationTime -Descending | Select-Object -First 1
                        $TeamLogContents = Get-Content $MostRecentTeamsLogFile
                        $RegexTemp = [regex]::Match($TeamLogContents, $regexNewVersion).captures.groups
                        if ($RegexTemp.Count -ge 2) {
                            $Version = $RegexTemp[2].value
                        }
                    }
                    catch {}

                    $TeamsVersion = New-Object -TypeName PSObject -Property @{
                        WindowsUser      = "NA"
                        TeamsUser        = $NewTeamsSettings.primary_user.accounts.account_upn
                        Type             = "New Teams"
                        Version          = $Version
                        Ring             = $NewTeamsSettings.tma_ecs_settings.$tmpAccountID.ring
                        Environment      = $NewTeamsSettings.tma_ecs_settings.$tmpAccountID.environment
                        CloudEnvironment = $NewTeamsSettings.primary_user.accounts.cloud
                        Path             = $TeamsSettingsFile.Directory.FullName
                    }
                    $TeamsVersion.PSObject.TypeNames.Insert(0, 'TeamsVersionFromPath')
                    [void]$outTeamsVersion.Add($TeamsVersion)
                }
            }
            #endregion
        }
        else {
            Write-Error -Message ("Invalid Path, please check if path: " + $path + " is correct and exists.")
        }
    }
    else {
        $currentDateFormat = [cultureinfo]::CurrentCulture.DateTimeFormat.ShortDatePattern
        if ($Computer) {
            $RemotePath = "\\" + $Computer + "\C$\Users"
            $ComputerName = $Computer
            if ($Credential) {
                if ($Computer.IndexOf('.') -gt 0) {
                    $PSDriveName = $Computer.Substring(0, $Computer.IndexOf('.')) + "_TmpTeamsVersion"
                }
                else {
                    $PSDriveName = $Computer + "_TmpTeamsVersion"
                }
                New-PSDrive -Root $RemotePath -Name $PSDriveName -PSProvider FileSystem -Credential $Credential | Out-Null
            }

            if (Test-Path -Path $RemotePath) {
                $Profiles = Get-ChildItem -Path $RemotePath -ErrorAction SilentlyContinue
            }
            else {
                Write-Error -Message ("Error: Cannot get users on " + $computer + ", please check if name is correct and if the current user has permissions.")
            }
        }
        else {
            $ComputerName = $Env:COMPUTERNAME
            $Profiles = Get-childItem 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' | ForEach-Object { Get-ItemProperty $_.pspath } | Where-Object { $_.fullprofile -eq 1 }
        }
       
        foreach ($UserProfile in $Profiles) {
            if ($Computer) {
                $ProfilePath = $UserProfile.FullName
                $ProfileName = $UserProfile.Name
            }
            else {
                $ProfilePath = $UserProfile.ProfileImagePath
                #20231013 Added exception handeling, only known case is when a windows profile was created when the machine was joined to a previous domain.
                try {
                    $ProfileName = (New-Object System.Security.Principal.SecurityIdentifier($UserProfile.PSChildName)).Translate( [System.Security.Principal.NTAccount]).Value
                }
                catch {
                    $ProfileName = "Unknown Windows User"
                }
            }
            #region classic teams
            $TeamsSettingPath = $ProfilePath + "\AppData\Roaming\Microsoft\Teams\settings.json"
            if (Test-Path $TeamsSettingPath -ErrorAction SilentlyContinue) {
                $TeamsSettings = Get-Content -Path $TeamsSettingPath
                $Version = ""
                $Ring = ""
                $Env = ""
                $CloudEnv = ""
                try {
                    $VersionTemp = [regex]::Match($TeamsSettings, $regexVersion).captures.groups
                    if ($VersionTemp.Count -ge 2) {
                        $Version = $VersionTemp[2].value
                    }
                    $RingTemp = [regex]::Match($TeamsSettings, $regexRing).captures.groups
                    if ($RingTemp.Count -ge 2) {
                        $Ring = $RingTemp[2].value
                    }
                    $EnvTemp = [regex]::Match($TeamsSettings, $regexEnv).captures.groups
                    if ($EnvTemp.Count -ge 2) {
                        $Env = $EnvTemp[2].value
                    }
                    $CloudEnvTemp = [regex]::Match($TeamsSettings, $regexCloudEnv).captures.groups
                    if ($CloudEnvTemp.Count -ge 2) {
                        $CloudEnv = $CloudEnvTemp[2].value
                    }
                }
                catch { }
                $TeamsApp = $ProfilePath + "\AppData\Local\Microsoft\Teams\current\Teams.exe"
                $TeamsInstallTimePath = $ProfilePath + "\AppData\Roaming\Microsoft\Teams\installTime.txt"
                #20240228 - In some cases the install file can be missing.
                $tmpInstallDate = ""
                if (Test-Path $TeamsInstallTimePath -ErrorAction SilentlyContinue) {
                    $InstallDateStr = Get-Content ($ProfilePath + "\AppData\Roaming\Microsoft\Teams\installTime.txt")
                    $tmpInstallDate = [Datetime]::ParseExact($InstallDateStr, 'M/d/yyyy', $null) | Get-Date -Format $currentDateFormat
                }
                
                $TeamsVersion = New-Object -TypeName PSObject -Property @{
                    Computer         = $ComputerName
                    Profile          = $ProfileName
                    ProfilePath      = $ProfilePath
                    Type             = "Teams Classic"
                    Version          = $Version
                    Ring             = $Ring
                    Environment      = $Env
                    CloudEnvironment = $CloudEnv
                    Arch             = Get-UcArch $TeamsApp
                    InstallDate      = $tmpInstallDate
                }
                $TeamsVersion.PSObject.TypeNames.Insert(0, 'TeamsVersion')
                [void]$outTeamsVersion.Add($TeamsVersion)
            }
            #endregion

            #region New Teams
            $NewTeamsSettingPath = $ProfilePath + "\AppData\Local\Publishers\8wekyb3d8bbwe\TeamsSharedConfig\tma_settings.json"
            if (Test-Path $NewTeamsSettingPath -ErrorAction SilentlyContinue) {
                $NewTeamsSettings = Get-Content -Path $NewTeamsSettingPath | ConvertFrom-Json
                $tmpAccountID = $NewTeamsSettings.primary_user.accounts.account_id
                if ($Computer) {
                    $newTeamsLocation = Get-ChildItem -Path ( $RemotePath + "\..\Program Files\Windowsapps" ) -Filter "ms-teams.exe" -Recurse -Depth 1 | Sort-Object -Property CreationTime -Descending | Select-Object -First 1                    
                }
                else {
                    #20240103 - Using Get-AppPackage drops the requirement to run with Administrative Rights
                    $newTeamsInstallPath = (Get-AppPackage MSTeams).InstallLocation + ".\ms-teams.exe"
                    $newTeamsLocation = Get-ItemProperty -Path ($newTeamsInstallPath)
                }
                if (Test-Path -Path $newTeamsLocation.FullName -ErrorAction SilentlyContinue) {
                    $TeamsVersion = New-Object -TypeName PSObject -Property @{
                        Computer         = $ComputerName
                        Profile          = $ProfileName
                        ProfilePath      = $ProfilePath
                        Type             = "New Teams"
                        Version          = $newTeamsLocation.VersionInfo.ProductVersion
                        Ring             = $NewTeamsSettings.tma_ecs_settings.$tmpAccountID.ring
                        Environment      = $NewTeamsSettings.tma_ecs_settings.$tmpAccountID.environment
                        CloudEnvironment = $NewTeamsSettings.primary_user.accounts.cloud
                        Arch             = Get-UcArch $newTeamsLocation.FullName
                        InstallDate      = $newTeamsLocation.CreationTime | Get-Date -Format $currentDateFormat
                    }
                    $TeamsVersion.PSObject.TypeNames.Insert(0, 'TeamsVersion')
                    [void]$outTeamsVersion.Add($TeamsVersion)
                }
            }
            #endregion
        }
        if ($Credential -and $PSDriveName) {
            try {
                Remove-PSDrive -Name $PSDriveName -ErrorAction SilentlyContinue
            }
            catch {}
        }
    }
    return $outTeamsVersion
}
#EndRegion '.\Public\Get-UcTeamsVersion.ps1' 284
#Region '.\Public\Get-UcTeamsVersionBatch.ps1' -1

function Get-UcTeamsVersionBatch {
    param(
        [Parameter(Mandatory = $true)]
        [string]$InputCSV,
        [string]$OutputPath,
        [switch]$ExportCSV,
        [System.Management.Automation.PSCredential]$Credential
    )
    <#
        .SYNOPSIS
        Get Microsoft Teams Desktop Version from all computers in a csv file.
 
        .DESCRIPTION
        This function returns the installed Microsoft Teams desktop version for each user profile.
 
        .PARAMETER InputCSV
        CSV with the list of computers that we want to get the Teams Version
 
        .PARAMETER OutputPath
        Specify the output path
 
        .PARAMETER ExportCSV
        Export the output to a CSV file
 
        .PARAMETER Credential
        Specify the credential to be used to connect to the remote computers
 
        .EXAMPLE
        PS> Get-UcTeamsVersionBatch
 
        .EXAMPLE
        PS> Get-UcTeamsVersionBatch -InputCSV C:\Temp\ComputerList.csv -Credential $cred
 
        .EXAMPLE
        PS> Get-UcTeamsVersionBatch -InputCSV C:\Temp\ComputerList.csv -Credential $cred -ExportCSV
    #>


    Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null
    if (Test-Path $InputCSV) {
        try {
            $Computers = Import-Csv -Path $InputCSV
        }
        catch {
            Write-Host ("Invalid CSV input file: " + $InputCSV) -ForegroundColor Red
            return
        }
        $outTeamsVersion = [System.Collections.ArrayList]::new()
        #Verify if the Output Path exists
        if ($OutputPath) {
            if (!(Test-Path $OutputPath -PathType Container)) {
                Write-Host ("Error: Invalid folder: " + $OutputPath) -ForegroundColor Red
                return
            } 
        }
        else {                
            $OutputPath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads")
        }

        $c = 0
        $compCount = $Computers.count
        
        foreach ($computer in $Computers) {
            $c++
            Write-Progress -Activity ("Getting Teams Version from: " + $computer.Computer)  -Status "Computer $c of $compCount "
            $tmpTV = Get-UcTeamsVersion -Computer $computer.Computer -Credential $cred -SkipModuleCheck
            $outTeamsVersion.Add($tmpTV) | Out-Null
        }
        if ($ExportCSV) {
            $tmpFileName = "MSTeamsVersion_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv"
            $OutputFullPath = [System.IO.Path]::Combine($OutputPath, $tmpFileName)
            $outTeamsVersion | Sort-Object Computer, Profile | Select-Object Computer, Profile, ProfilePath, Arch, Version, Environment, Ring, InstallDate | Export-Csv -path $OutputFullPath -NoTypeInformation
            Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan
        }
        else {
            return $outTeamsVersion 
        }
    }
    else {
        Write-Host ("Error: File not found " + $InputCSV) -ForegroundColor Red
    }
}
#EndRegion '.\Public\Get-UcTeamsVersionBatch.ps1' 82
#Region '.\Public\Get-UcTeamsWithSingleOwner.ps1' -1

function Get-UcTeamsWithSingleOwner {
    <#
        .SYNOPSIS
        Get Teams that have a single owner
 
        .DESCRIPTION
        This function returns a list of Teams that only have a single owner.
 
        .EXAMPLE
        PS> Get-UcTeamsWithSingleOwner
    #>

    Get-UcTeamUsersEmail -Role Owner -Confirm:$false | Group-Object -Property TeamDisplayName | Where-Object { $_.Count -lt 2 } | Select-Object -ExpandProperty Group
}
#EndRegion '.\Public\Get-UcTeamsWithSingleOwner.ps1' 14
#Region '.\Public\Get-UcTeamUsersEmail.ps1' -1

function Get-UcTeamUsersEmail {
    [cmdletbinding(SupportsShouldProcess)]
    param(
        [string]$TeamName,
        [ValidateSet("Owner", "User", "Guest")] 
        [string]$Role
    )
    <#
        .SYNOPSIS
        Get Users Email Address that are in a Team
 
        .DESCRIPTION
        This function returns a list of users email address that are part of a Team.
 
        .PARAMETER TeamName
        Specifies Team Name
 
        .PARAMETER Role
        Specifies which roles to filter (Owner, User, Guest)
 
        .EXAMPLE
        PS> Get-UcTeamUsersEmail
 
        .EXAMPLE
        PS> Get-UcTeamUsersEmail -TeamName "Marketing"
 
        .EXAMPLE
        PS> Get-UcTeamUsersEmail -Role "Guest"
 
        .EXAMPLE
        PS> Get-UcTeamUsersEmail -TeamName "Marketing" -Role "Guest"
    #>


    $output = [System.Collections.ArrayList]::new()
    Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null
    if ($TeamName) {
        $Teams = Get-Team -DisplayName $TeamName
    }
    else {
        if ($ConfirmPreference) {
            $title = 'Confirm'
            $question = 'Are you sure that you want to list all Teams?'
            $choices = '&Yes', '&No'
            $decision = $Host.UI.PromptForChoice($title, $question, $choices, 1)
        }
        else {
            $decision = 0
        }
        if ($decision -eq 0) {
            $Teams = Get-Team
        }
        else {
            return
        }
    }
    foreach ($Team in $Teams) { 
        if ($Role) {
            $TeamMembers = Get-TeamUser -GroupId $Team.GroupID -Role $Role
        }
        else {
            $TeamMembers = Get-TeamUser -GroupId $Team.GroupID 
        }
        foreach ($TeamMember in $TeamMembers) {
            $Email = ( Get-csOnlineUser $TeamMember.User | Select-Object @{Name = 'PrimarySMTPAddress'; Expression = { $_.ProxyAddresses -cmatch '^SMTP:' -creplace 'SMTP:' } }).PrimarySMTPAddress
            $Member = New-Object -TypeName PSObject -Property @{
                TeamGroupID     = $Team.GroupID
                TeamDisplayName = $Team.DisplayName
                TeamVisibility  = $Team.Visibility
                UPN             = $TeamMember.User
                Role            = $TeamMember.Role
                Email           = $Email
            }
            $Member.PSObject.TypeNames.Insert(0, 'TeamUsersEmail')
            $output.Add($Member) | Out-Null
        }
    }
    return $output
}
#EndRegion '.\Public\Get-UcTeamUsersEmail.ps1' 79
#Region '.\Public\Test-UcPowerShellModule.ps1' -1

function Test-UcPowerShellModule {
    param(
        [Parameter(Mandatory = $true)]    
        [string]$ModuleName
    )
    <#
        .SYNOPSIS
        Test if PowerShell module is installed and updated
 
        .DESCRIPTION
        This function returns FALSE if PowerShell module is not installed.
 
        .PARAMETER ModuleName
        Specifies PowerShell module name
 
        .EXAMPLE
        PS> Test-UcPowerShellModule -ModuleName UCLobbyTeams
    #>

    try { 
        #Get all installed versions
        $installedVersions = (Get-Module $ModuleName -ListAvailable | Sort-Object Version -Descending).Version

        #Get the lastest version available
        $availableVersion = (Find-Module -Name $ModuleName -Repository PSGallery -ErrorAction SilentlyContinue).Version

        if (!($installedVersions)) {
            if ($availableVersion ) {
                #Module not installed and there is an available version to install.
                Write-Warning ("The PowerShell Module $ModuleName is not installed, please install the latest available version ($availableVersion) with:" + [Environment]::NewLine + "Install-Module $ModuleName")
            }
            else {
                #Wrong name or not found in the registered PS Repository.
                Write-Warning ("The PowerShell Module $ModuleName not found in the registered PS Repository, please check the module name and try again.")
            }
            return $false
        }

        #Get the current loaded version
        $tmpCurrentVersion = (Get-Module $ModuleName | Sort-Object Version -Descending)
        if ($tmpCurrentVersion) {
            $currentVersion = $tmpCurrentVersion[0].Version.ToString()
        }

        if (!($currentVersion)) {
            #Module is installed but not imported, in this case we check if there is a newer version available.
            if ($availableVersion -in $installedVersions) {
                Write-Warning ("The lastest available version of $ModuleName module is installed, however the module is not imported." + [Environment]::NewLine + "Please make sure you import it with:" + [Environment]::NewLine + "Import-Module $ModuleName -RequiredVersion $availableVersion")
            }
            else {
                Write-Warning ("There is a new version available $availableVersion, the lastest installed version is " + $installedVersions[0] + "." + [Environment]::NewLine + "Please update the module with:" + [Environment]::NewLine + "Update-Module $ModuleName")
            }
        }

        if ($currentVersion -ne $availableVersion ) {
            if ($availableVersion -in $installedVersions) {
                Write-Warning ("The lastest available version of $ModuleName module is installed, however version $currentVersion is imported." + [Environment]::NewLine + "Please make sure you import it with:" + [Environment]::NewLine + "Import-Module $ModuleName -RequiredVersion $availableVersion")
            }
            else {
                Write-Warning ("There is a new version available $availableVersion, current version $currentVersion." + [Environment]::NewLine + "Please update the module with:" + [Environment]::NewLine + "Update-Module $ModuleName")
            }
        }
        return $true
    }
    catch {
    }
    return $false
}
#EndRegion '.\Public\Test-UcPowerShellModule.ps1' 68
#Region '.\Public\Test-UcTeamsDevicesCompliancePolicy.ps1' -1

function Test-UcTeamsDevicesCompliancePolicy {
    param(
        [switch]$Detailed,
        [switch]$All,
        [switch]$IncludeSupported,
        [string]$PolicyID,
        [string]$PolicyName,
        [string]$UserUPN,
        [string]$DeviceID,
        [switch]$ExportCSV,
        [string]$OutputPath
    )
    <#
        .SYNOPSIS
        Validate which Intune Compliance policies are supported by Microsoft Teams Android Devices
 
        .DESCRIPTION
        This function will validate each setting in the Intune Compliance Policy to make sure they are in line with the supported settings:
 
            https://docs.microsoft.com/en-us/microsoftteams/rooms/supported-ca-and-compliance-policies?tabs=phones#supported-device-compliance-policies
 
        Contributors: Traci Herr, David Paulino
 
        Requirements: Microsoft Graph PowerShell Module (Install-Module Microsoft.Graph)
 
        .PARAMETER Detailed
        Displays test results for unsupported settings in each Intune Compliance Policy
 
        .PARAMETER All
        Will check all Intune Compliance policies independently if they are assigned to a Group(s)
 
        .PARAMETER IncludeSupported
        Displays results for all settings in each Intune Compliance Policy
 
        .PARAMETER PolicyID
        Specifies a Policy ID that will be checked if is supported by Microsoft Teams Android Devices
 
        .PARAMETER PolicyName
        Specifies a Policy Name that will be checked if is supported by Microsoft Teams Android Devices
 
        .PARAMETER UserUPN
        Specifies a UserUPN that we want to check for applied compliance policies
 
        .PARAMETER DeviceID
        Specifies DeviceID that we want to check for applied compliance policies
 
        .PARAMETER ExportCSV
        When present will export the detailed results to a CSV file. By defautl will save the file under the current user downloads, unless we specify the OutputPath.
 
        .PARAMETER OutputPath
        Allows to specify the path where we want to save the results.
 
        .EXAMPLE
        PS> Test-UcTeamsDevicesCompliancePolicy
 
        .EXAMPLE
        PS> Test-UcTeamsDevicesCompliancePolicy -Detailed
    #>


    $connectedMSGraph = $false
    $CompliancePolicies = $null
    $totalCompliancePolicies = 0
    $skippedCompliancePolicies = 0

    $GraphURI_CompliancePolicies = "https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies/"
    $GraphURI_Users = "https://graph.microsoft.com/v1.0/users"
    $GraphURI_Groups = "https://graph.microsoft.com/v1.0/groups"
    $GraphURI_Devices = "https://graph.microsoft.com/v1.0/devices"

    $SupportedAndroidCompliancePolicies = "#microsoft.graph.androidCompliancePolicy", "#microsoft.graph.androidDeviceOwnerCompliancePolicy", "#microsoft.graph.aospDeviceOwnerCompliancePolicy"
    $SupportedWindowsCompliancePolicies = "#microsoft.graph.windows10CompliancePolicy"

    $URLSupportedCompliancePoliciesAndroid = "https://aka.ms/TeamsDevicePolicies?tabs=phones#supported-device-compliance-policies"
    $URLSupportedCompliancePoliciesWindows = "https://aka.ms/TeamsDevicePolicies?tabs=mtr-w#supported-device-compliance-policies"

    if (Test-UcMgGraphConnection -Scopes "DeviceManagementConfiguration.Read.All", "Directory.Read.All") {
        Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null
        $outFileName = "TeamsDevices_CompliancePolicy_Report_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv"
        if ($OutputPath) {
            if (!(Test-Path $OutputPath -PathType Container)) {
                Write-Host ("Error: Invalid folder " + $OutputPath) -ForegroundColor Red
                return
            } 
            $OutputFullPath = [System.IO.Path]::Combine($OutputPath, $outFileName)
        }
        else {                
            $OutputFullPath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads", $outFileName)
        }

        try {
            Write-Progress -Activity "Test-UcTeamsDeviceCompliancePolicy" -Status "Getting Compliance Policies"
            $CompliancePolicies = (Invoke-MgGraphRequest -Uri $GraphURI_CompliancePolicies -Method GET).value
            $connectedMSGraph = $true
        }
        catch [System.Net.Http.HttpRequestException] {
            if ($PSItem.Exception.Response.StatusCode -eq "Unauthorized") {
                Write-Error "Access Denied, please make sure the user connecing to MS Graph is part of one of the following Global Reader/Intune Service Administrator/Global Administrator roles"
            }
            else {
                Write-Error $PSItem.Exception.Message
            }
        }
        catch {
            Write-Error 'Please connect to MS Graph with Connect-MgGraph -Scopes "DeviceManagementConfiguration.Read.All","Directory.Read.All" before running this script'
        }

        if ($connectedMSGraph) {
            $output = [System.Collections.ArrayList]::new()
            $outputSum = [System.Collections.ArrayList]::new()
            if ($UserUPN) {
                try {
                    $UserGroups = (Invoke-MgGraphRequest -Uri ($GraphURI_Users + "/" + $userUPN + "/transitiveMemberOf?`$select=id") -Method GET).value.id
                }
                catch [System.Net.Http.HttpRequestException] {
                    if ($PSItem.Exception.Response.StatusCode -eq "NotFound") {
                        Write-warning -Message ("User Not Found: " + $UserUPN)
                    }
                    return
                }
                #We also need to take in consideration devices that are registered to this user
                $DeviceGroups = [System.Collections.ArrayList]::new()
                $userDevices = (Invoke-MgGraphRequest -Uri ($GraphURI_Users + "/" + $userUPN + "/registeredDevices?`$select=deviceId,displayName") -Method GET).value
                foreach ($userDevice in $userDevices) {
                    $tmpGroups = (Invoke-MgGraphRequest -Uri ($GraphURI_Devices + "(deviceId='{" + $userDevice.deviceID + "}')/transitiveMemberOf?`$select=id") -Method GET).value.id
                    foreach ($tmpGroup in $tmpGroups) {
                        $tmpDG = New-Object -TypeName PSObject -Property @{
                            GroupId           = $tmpGroup
                            DeviceId          = $userDevice.deviceID
                            DeviceDisplayName = $userDevice.displayName
                        }
                        $DeviceGroups.Add($tmpDG) | Out-Null
                    }
                }
            }
        
            if ($DeviceID) {
                try {
                    $DeviceGroups = (Invoke-MgGraphRequest -Uri ($GraphURI_Devices + "(deviceId='{" + $DeviceID + "}')/transitiveMemberOf?`$select=id") -Method GET).value.id
                }
                catch [System.Net.Http.HttpRequestException] {
                    if ($PSItem.Exception.Response.StatusCode -eq "BadRequest") {
                        Write-warning -Message ("Device ID Not Found: " + $DeviceID)
                    }
                    return
                }
            }
            #TO DO: We should only get display names from assigned groups
            $Groups = New-Object 'System.Collections.Generic.Dictionary[string, string]'
            $p = 0
            $policyCount = $CompliancePolicies.Count
            foreach ($CompliancePolicy in $CompliancePolicies) {
                $p++
                Write-Progress -Activity "Test-UcTeamsDeviceCompliancePolicy" -Status ("Checking policy " + $CompliancePolicy.displayName + " - $p of $policyCount")
                if ((($PolicyID -eq $CompliancePolicy.id) -or ($PolicyName -eq $CompliancePolicy.displayName) -or (!$PolicyID -and !$PolicyName)) -and (($CompliancePolicy."@odata.type" -in $SupportedAndroidCompliancePolicies) -or ($CompliancePolicy."@odata.type" -in $SupportedWindowsCompliancePolicies))) {
                    
                    #We need to check if the policy has assignments (Groups/All Users/All Devices)
                    $CompliancePolicyAssignments = (Invoke-MgGraphRequest -Uri ($GraphURI_CompliancePolicies + $CompliancePolicy.id + "/assignments" ) -Method GET).value
                    $AssignedToGroup = [System.Collections.ArrayList]::new()
                    $ExcludedFromGroup = [System.Collections.ArrayList]::new()
                    $outAssignedToGroup = ""
                    $outExcludedFromGroup = ""
                    #We wont need to check the settings if the policy is not assigned to a user
                    if ($UserUPN -or $DeviceID) {
                        $userOrDeviceIncluded = $false
                    }
                    else {
                        $userOrDeviceIncluded = $true
                    }

                    #Define the Compliance Policy type
                    switch ($CompliancePolicy."@odata.type") {
                        "#microsoft.graph.androidCompliancePolicy" { $CPType = "Android Device" }
                        "#microsoft.graph.androidDeviceOwnerCompliancePolicy" { $CPType = "Android Enterprise" }
                        "#microsoft.graph.aospDeviceOwnerCompliancePolicy" { $CPType = "Android (AOSP)" }
                        "#microsoft.graph.windows10CompliancePolicy" { $CPType = "Windows 10 or later" }
                        Default { $CPType = $CompliancePolicy."@odata.type".split('.')[2] }
                    }

                    #Checking Compliance Policy assigments since we can skip non assigned policies.
                    foreach ($CompliancePolicyAssignment in $CompliancePolicyAssignments) {
                        $GroupDisplayName = $CompliancePolicyAssignment.target.Groupid
                        if ($Groups.ContainsKey($CompliancePolicyAssignment.target.Groupid)) {
                            $GroupDisplayName = $Groups.Item($CompliancePolicyAssignment.target.Groupid)
                        }
                        else {
                            try {
                                $GroupInfo = Invoke-MgGraphRequest -Uri ($GraphURI_Groups + "/" + $CompliancePolicyAssignment.target.Groupid + "/?`$select=id,displayname") -Method GET
                                $Groups.Add($GroupInfo.id, $GroupInfo.displayname)
                                $GroupDisplayName = $GroupInfo.displayname
                            }
                            catch {
                            }
                        }
                        $GroupEntry = New-Object -TypeName PSObject -Property @{
                            GroupID          = $CompliancePolicyAssignment.target.Groupid
                            GroupDisplayName = $GroupDisplayName
                        }
                        switch ($CompliancePolicyAssignment.target."@odata.type") {
                            #Policy assigned to all users
                            "#microsoft.graph.allLicensedUsersAssignmentTarget" {
                                $GroupEntry = New-Object -TypeName PSObject -Property @{
                                    GroupID          = "allLicensedUsersAssignment"
                                    GroupDisplayName = "All Users"
                                }
                                $AssignedToGroup.Add($GroupEntry) | Out-Null
                                $userOrDeviceIncluded = $true
                            }
                            #Policy assigned to all devices
                            "#microsoft.graph.allDevicesAssignmentTarget" {
                                $GroupEntry = New-Object -TypeName PSObject -Property @{
                                    GroupID          = "allDevicesAssignmentTarget"
                                    GroupDisplayName = "All Devices"
                                }
                                $AssignedToGroup.Add($GroupEntry) | Out-Null
                                $userOrDeviceIncluded = $true
                            }
                            #Group that this policy is assigned
                            "#microsoft.graph.groupAssignmentTarget" {
                                $AssignedToGroup.Add($GroupEntry) | Out-Null
                                if (($UserUPN -or $DeviceID) -and (($CompliancePolicyAssignment.target.Groupid -in $UserGroups) -or ($CompliancePolicyAssignment.target.Groupid -in $DeviceGroups))) {
                                    $userOrDeviceIncluded = $true
                                }
                            }
                            #Group that this policy is excluded
                            "#microsoft.graph.exclusionGroupAssignmentTarget" {
                                $ExcludedFromGroup.Add($GroupEntry) | Out-Null
                                #If user is excluded then we dont need to check the policy
                                if ($UserUPN -and ($CompliancePolicyAssignment.target.Groupid -in $UserGroups)) {
                                    Write-Warning ("Skiping compliance policy " + $CompliancePolicy.displayName + ", since user " + $UserUPN + " is part of an Excluded Group: " + $GroupEntry.GroupDisplayName)
                                    $userOrDeviceExcluded = $true
                                }
                                elseif ($DeviceID -and ($CompliancePolicyAssignment.target.Groupid -in $DeviceGroups)) {
                                    Write-Warning ("Skiping compliance policy " + $CompliancePolicy.displayName + ", since device " + $DeviceID + " is part of an Excluded Group: " + $GroupEntry.GroupDisplayName)
                                    $userOrDeviceExcluded = $true
                                }
                                elseif ($UserUPN -and (($CompliancePolicyAssignment.target.Groupid -in $DeviceGroups.GroupId))) {
                                    #In case a device is excluded we will check the policy but output a message
                                    $tmpDev = ($DeviceGroups | Where-Object -Property GroupId -eq -Value $CompliancePolicyAssignment.target.Groupid)
                                    Write-Warning ("Compliance policy " + $CompliancePolicy.displayName + " will not be applied to device " + $tmpDev.DeviceDisplayName + " (" + $tmpDev.DeviceID + "), since this device is part of an Excluded Group: " + $GroupEntry.GroupDisplayName)
                                }
                            }
                        }
                    }
                
                    if ((($AssignedToGroup.count -gt 0) -and !$userOrDeviceExcluded -and $userOrDeviceIncluded) -or $all) {
                        $totalCompliancePolicies++ 
                        $PolicyErrors = 0
                        $PolicyWarnings = 0

                        #If only assigned/excluded from a group we will show the group display name, otherwise the number of groups assigned/excluded.
                        if ($AssignedToGroup.count -eq 1) {
                            $outAssignedToGroup = $AssignedToGroup.GroupDisplayName
                        }
                        elseif ($AssignedToGroup.count -eq 0) {
                            $outAssignedToGroup = "None"
                        }
                        else {
                            $outAssignedToGroup = "" + $AssignedToGroup.count + " groups"
                        }

                        if ($ExcludedFromGroup.count -eq 1) {
                            $outExcludedFromGroup = $ExcludedFromGroup.GroupDisplayName
                        }
                        elseif ($ExcludedFromGroup.count -eq 0) {
                            $outExcludedFromGroup = "None"
                        }
                        else {
                            $outExcludedFromGroup = "" + $ExcludedFromGroup.count + " groups"
                        }

                        if ($CompliancePolicy."@odata.type" -in $SupportedAndroidCompliancePolicies) {
                            $URLSupportedCompliancePolicies = $URLSupportedCompliancePoliciesAndroid
                        }
                        elseif ($CompliancePolicy."@odata.type" -in $SupportedWindowsCompliancePolicies) {
                            $URLSupportedCompliancePolicies = $URLSupportedCompliancePoliciesWindows
                        }

                        #region Common settings between Android (ADA and AOSP) and Windows
                        #region 9: Device Properties > Operation System Version
                        $ID = 9.1
                        $Setting = "osMinimumVersion"
                        $SettingDescription = "Device Properties > Operation System Version > Minimum OS version"
                        $SettingValue = "Not Configured"
                        $Comment = ""
                        if (!([string]::IsNullOrEmpty($CompliancePolicy.osMinimumVersion))) {
                            if ($CompliancePolicy."@odata.type" -in $SupportedWindowsCompliancePolicies) {
                                $Status = "Unsupported"
                                $Comment = "Teams Rooms automatically updates to newer versions of Windows and setting values here could prevent successful sign-in after an OS update."
                                $PolicyErrors++
                            }
                            else {
                                $Status = "Warning"
                                $Comment = "This setting can cause sign in issues."
                                $PolicyWarnings++
                            }
                            $SettingValue = $CompliancePolicy.osMinimumVersion
                        }
                        else {
                            $Status = "Supported"
                        }
                        $SettingPSObj = [PSCustomObject]@{
                            PolicyName            = $CompliancePolicy.displayName
                            PolicyType            = $CPType
                            Setting               = $Setting
                            Value                 = $SettingValue
                            TeamsDevicesStatus    = $Status 
                            Comment               = $Comment
                            SettingDescription    = $SettingDescription
                            AssignedToGroup       = $outAssignedToGroup
                            ExcludedFromGroup     = $outExcludedFromGroup 
                            AssignedToGroupList   = $AssignedToGroup
                            ExcludedFromGroupList = $ExcludedFromGroup
                            PolicyID              = $CompliancePolicy.id
                            ID                    = $ID
                        }
                        $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                        [void]$output.Add($SettingPSObj)
            
                        $ID = 9.2
                        $Setting = "osMaximumVersion"
                        $SettingDescription = "Device Properties > Operation System Version > Maximum OS version"
                        $SettingValue = "Not Configured"
                        $Comment = ""
                        if (!([string]::IsNullOrEmpty($CompliancePolicy.osMaximumVersion))) {
                            if ($CompliancePolicy."@odata.type" -in $SupportedWindowsCompliancePolicies) {
                                $Status = "Unsupported"
                                $Comment = "Teams Rooms automatically updates to newer versions of Windows and setting values here could prevent successful sign-in after an OS update."
                                $PolicyErrors++
                            }
                            else {
                                $Status = "Warning"
                                $Comment = "This setting can cause sign in issues."
                                $PolicyWarnings++
                            }
                            $SettingValue = $CompliancePolicy.osMaximumVersion
                        }
                        else {
                            $Status = "Supported"
                        }
                        $SettingPSObj = [PSCustomObject]@{
                            PolicyName            = $CompliancePolicy.displayName
                            PolicyType            = $CPType
                            Setting               = $Setting
                            Value                 = $SettingValue
                            TeamsDevicesStatus    = $Status 
                            Comment               = $Comment
                            SettingDescription    = $SettingDescription
                            AssignedToGroup       = $outAssignedToGroup
                            ExcludedFromGroup     = $outExcludedFromGroup 
                            AssignedToGroupList   = $AssignedToGroup
                            ExcludedFromGroupList = $ExcludedFromGroup
                            PolicyID              = $CompliancePolicy.id
                            ID                    = $ID
                        }
                        $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                        [void]$output.Add($SettingPSObj)
                        #endregion

                        #region 17: System Security > All Android devices > Require a password to unlock mobile devices
                        $ID = 17
                        $Setting = "passwordRequired"
                        $SettingDescription = "System Security > All Android devices > Require a password to unlock mobile devices"
                        $SettingValue = "Not Configured"
                        $Comment = ""
                        if ($CompliancePolicy.passwordRequired) {
                            $Status = "Unsupported"
                            $SettingValue = "Require"
                            $Comment = $URLSupportedCompliancePolicies
                            $PolicyErrors++
                        }
                        else {
                            $Status = "Supported"
                        }
                        $SettingPSObj = [PSCustomObject]@{
                            PolicyName            = $CompliancePolicy.displayName
                            PolicyType            = $CPType
                            Setting               = $Setting
                            Value                 = $SettingValue
                            TeamsDevicesStatus    = $Status 
                            Comment               = $Comment
                            SettingDescription    = $SettingDescription
                            AssignedToGroup       = $outAssignedToGroup
                            ExcludedFromGroup     = $outExcludedFromGroup 
                            AssignedToGroupList   = $AssignedToGroup
                            ExcludedFromGroupList = $ExcludedFromGroup
                            PolicyID              = $CompliancePolicy.id
                            ID                    = $ID
                        }
                        $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                        [void]$output.Add($SettingPSObj)
                        #endregion
                        #endregion

                        #20240508 - We need to limit the settings since not all are available in AOSP compliance policies.
                        if ($CompliancePolicy."@odata.type" -in $SupportedAndroidCompliancePolicies -and $CompliancePolicy."@odata.type" -ne "#microsoft.graph.aospDeviceOwnerCompliancePolicy") {

                            #region 1: Microsoft Defender for Endpoint > Require the device to be at or under the machine risk score
                            $ID = 1
                            $Setting = "deviceThreatProtectionEnabled"
                            $SettingDescription = "Microsoft Defender for Endpoint > Require the device to be at or under the machine risk score"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if ($CompliancePolicy.deviceThreatProtectionEnabled) {
                                $Status = "Unsupported"
                                $PolicyErrors++
                                $SettingValue = $CompliancePolicy.advancedThreatProtectionRequiredSecurityLevel
                                $Comment = $URLSupportedCompliancePolicies
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion

                            #region 2: Device Health > Device managed with device administrator
                            $ID = 2
                            $Setting = "securityBlockDeviceAdministratorManagedDevices"
                            $SettingDescription = "Device Health > Device managed with device administrator"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if ($CompliancePolicy.securityBlockDeviceAdministratorManagedDevices) {
                                $Status = "Unsupported"
                                $SettingValue = "Block"
                                $Comment = "Teams Android devices management requires device administrator to be enabled."
                                $PolicyErrors++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion

                            #region 4: Device Health > Require the device to be at or under the Device Threat Level
                            $ID = 4
                            $Setting = "deviceThreatProtectionRequiredSecurityLevel"
                            $SettingDescription = "Device Health > Require the device to be at or under the Device Threat Level"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if ($CompliancePolicy.deviceThreatProtectionRequiredSecurityLevel -ne "unavailable") {
                                $Status = "Unsupported"
                                $SettingValue = $CompliancePolicy.deviceThreatProtectionRequiredSecurityLevel
                                $Comment = $URLSupportedCompliancePolicies
                                $PolicyErrors++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion

                            #region 5: Device Health > Google Protect > Google Play Services is Configured
                            $ID = 5
                            $Setting = "securityRequireGooglePlayServices"
                            $SettingDescription = "Device Health > Google Protect > Google Play Services is Configured"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if ($CompliancePolicy.securityRequireGooglePlayServices) {
                                $Status = "Unsupported"
                                $SettingValue = "Require"
                                $Comment = "Google play isn't installed on Teams Android devices."
                                $PolicyErrors++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion

                            #region 6: Device Health > Google Protect > Up-to-date security provider
                            $ID = 6
                            $Setting = "securityRequireUpToDateSecurityProviders"
                            $SettingDescription = "Device Health > Google Protect > Up-to-date security provider"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if ($CompliancePolicy.securityRequireUpToDateSecurityProviders) {
                                $Status = "Unsupported"
                                $SettingValue = "Require"
                                $Comment = "Google play isn't installed on Teams Android devices."
                                $PolicyErrors++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion
                
                            #region 7: Device Health > Google Protect > Threat scan on apps
                            $ID = 7
                            $Setting = "securityRequireVerifyApps"
                            $SettingDescription = "Device Health > Google Protect > Threat scan on apps"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if ($CompliancePolicy.securityRequireVerifyApps) {
                                $Status = "Unsupported"
                                $SettingValue = "Require"
                                $Comment = "Google play isn't installed on Teams Android devices."
                                $PolicyErrors++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion

                            #region 8: Device Health > Google Protect > SafetyNet device attestation
                            $ID = 8
                            $Setting = "securityRequireSafetyNetAttestation"
                            $SettingDescription = "Device Health > Google Protect > SafetyNet device attestation"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if (($CompliancePolicy.securityRequireSafetyNetAttestationBasicIntegrity) -or ($CompliancePolicy.securityRequireSafetyNetAttestationCertifiedDevice)) {
                                $Status = "Unsupported"
                                $Comment = "Google play isn't installed on Teams Android devices."
                                $PolicyErrors++
                                if ($CompliancePolicy.securityRequireSafetyNetAttestationCertifiedDevice) {
                                    $SettingValue = "Check basic integrity and certified devices"
                                }
                                elseif ($CompliancePolicy.securityRequireSafetyNetAttestationBasicIntegrity) {
                                    $SettingValue = "Check basic integrity"
                                }
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion

                            #region 11: System Security > Device Security > Block apps from unknown sources
                            $ID = 11
                            $Setting = "securityPreventInstallAppsFromUnknownSources"
                            $SettingDescription = "System Security > Device Security > Block apps from unknown sources"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if ($CompliancePolicy.securityPreventInstallAppsFromUnknownSources) {
                                $Status = "Unsupported"
                                $SettingValue = "Block"
                                $Comment = "Only Teams admins install apps or OEM tools"
                                $PolicyErrors++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion

                            #region 15: System Security > Device Security > Restricted apps
                            $ID = 15
                            $Setting = "securityPreventInstallAppsFromUnknownSources"
                            $SettingDescription = "System Security > Device Security > Restricted apps"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if (($CompliancePolicy.restrictedApps).count -gt 0 ) {
                                $Status = "Unsupported"
                                $SettingValue = "Found " + ($CompliancePolicy.restrictedApps).count + " restricted app(s)"
                                $Comment = $URLSupportedCompliancePolicies
                                $PolicyErrors++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion
                        }
                        
                        if ($CompliancePolicy."@odata.type" -in $SupportedAndroidCompliancePolicies) {

                            #region 3: Device Health > Rooted devices
                            $ID = 3
                            $Setting = "securityBlockJailbrokenDevices"
                            $SettingDescription = "Device Health > Rooted devices"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if ($CompliancePolicy.securityBlockJailbrokenDevices) {
                                $Status = "Warning"
                                $SettingValue = "Block"
                                $Comment = "This setting can cause sign in issues."
                                $PolicyWarnings++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion

                            #region 10: System Security > Encryption > Require encryption of data storage on device.
                            $ID = 10
                            $Setting = "storageRequireEncryption"
                            $SettingDescription = "System Security > Encryption > Require encryption of data storage on device"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if ($CompliancePolicy.storageRequireEncryption) {
                                $Status = "Warning"
                                $SettingValue = "Require"
                                $Comment = "Manufacturers might configure encryption attributes on their devices in a way that Intune doesn't recognize. If this happens, Intune marks the device as noncompliant."
                                $PolicyWarnings++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion
                            
                            #region 14: System Security > Device Security > Minimum security patch level
                            $ID = 14
                            $Setting = "minAndroidSecurityPatchLevel"
                            $SettingDescription = "System Security > Device Security > Minimum security patch level"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if (!([string]::IsNullOrEmpty($CompliancePolicy.minAndroidSecurityPatchLevel))) {
                                $Status = "Warning"
                                $SettingValue = $CompliancePolicy.minAndroidSecurityPatchLevel
                                $Comment = "This setting can cause sign in issues."
                                $PolicyWarnings++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion

                            #region 16: System Security > All Android devices > Maximum minutes of inactivity before password is required
                            $ID = 16
                            $Setting = "passwordMinutesOfInactivityBeforeLock"
                            $SettingDescription = "System Security > All Android devices > Maximum minutes of inactivity before password is required"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if (!([string]::IsNullOrEmpty($CompliancePolicy.passwordMinutesOfInactivityBeforeLock))) {
                                $Status = "Unsupported"
                                $SettingValue = "" + $CompliancePolicy.passwordMinutesOfInactivityBeforeLock + " minutes"
                                $Comment = $URLSupportedCompliancePolicies
                                $PolicyErrors++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion
                        } 
                        elseif ($CompliancePolicy."@odata.type" -in $SupportedWindowsCompliancePolicies) {

                            #region 18: Device Properties > Operation System Version
                            $ID = 18.1
                            $Setting = "mobileOsMinimumVersion"
                            $SettingDescription = "Device Properties > Operation System Version > Minimum OS version for mobile devices"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if (!([string]::IsNullOrEmpty($CompliancePolicy.mobileOsMinimumVersion))) {
                                $Status = "Unsupported"
                                $SettingValue = $CompliancePolicy.mobileOsMinimumVersion
                                $Comment = $URLSupportedCompliancePolicies
                                $PolicyErrors++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                
                            $ID = 18.2
                            $Setting = "mobileOsMaximumVersion"
                            $SettingDescription = "Device Properties > Operation System Version > Maximum OS version for mobile devices"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if (!([string]::IsNullOrEmpty($CompliancePolicy.mobileOsMaximumVersion))) {
                                $Status = "Unsupported"
                                $SettingValue = $CompliancePolicy.mobileOsMaximumVersion
                                $Comment = $URLSupportedCompliancePolicies
                                $PolicyErrors++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion

                            #region 19: Device Properties > Operation System Version > Valid operating system builds
                            $ID = 19
                            $Setting = "validOperatingSystemBuildRanges"
                            $SettingDescription = "Device Properties > Operation System Version > Valid operating system builds"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if (!([string]::IsNullOrEmpty($CompliancePolicy.validOperatingSystemBuildRanges))) {
                                $Status = "Unsupported"
                                $SettingValue = "Found " + ($CompliancePolicy.validOperatingSystemBuildRanges).count + " valid OS configured build(s)"
                                $Comment = $URLSupportedCompliancePolicies
                                $PolicyErrors++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion

                            #region 20: System Security > Defender > Microsoft Defender Antimalware minimum version
                            $ID = 20
                            $Setting = "defenderVersion"
                            $SettingDescription = "System Security > Defender > Microsoft Defender Antimalware minimum version"
                            $SettingValue = "Not Configured"
                            $Comment = ""
                            if (!([string]::IsNullOrEmpty($CompliancePolicy.defenderVersion))) {
                                $Status = "Unsupported"
                                $SettingValue = $CompliancePolicy.defenderVersion
                                $Comment = "Teams Rooms automatically updates this component so there's no need to set compliance policies."
                                $PolicyErrors++
                            }
                            else {
                                $Status = "Supported"
                            }
                            $SettingPSObj = [PSCustomObject]@{
                                PolicyName            = $CompliancePolicy.displayName
                                PolicyType            = $CPType
                                Setting               = $Setting
                                Value                 = $SettingValue
                                TeamsDevicesStatus    = $Status 
                                Comment               = $Comment
                                SettingDescription    = $SettingDescription
                                AssignedToGroup       = $outAssignedToGroup
                                ExcludedFromGroup     = $outExcludedFromGroup 
                                AssignedToGroupList   = $AssignedToGroup
                                ExcludedFromGroupList = $ExcludedFromGroup
                                PolicyID              = $CompliancePolicy.id
                                ID                    = $ID
                            }
                            $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicyDetailed')
                            [void]$output.Add($SettingPSObj)
                            #endregion
                        }

                        if ($PolicyErrors -gt 0) {
                            $StatusSum = "Found " + $PolicyErrors + " unsupported settings."
                            $displayWarning = $true
                        }
                        elseif ($PolicyWarnings -gt 0) {
                            $StatusSum = "Found " + $PolicyWarnings + " settings that may impact users."
                            $displayWarning = $true
                        }
                        else {
                            $StatusSum = "No issues found."
                        }
                        $PolicySum = [PSCustomObject]@{
                            PolicyID              = $CompliancePolicy.id
                            PolicyName            = $CompliancePolicy.displayName
                            PolicyType            = $CPType
                            AssignedToGroup       = $outAssignedToGroup
                            AssignedToGroupList   = $AssignedToGroup
                            ExcludedFromGroup     = $outExcludedFromGroup 
                            ExcludedFromGroupList = $ExcludedFromGroup
                            TeamsDevicesStatus    = $StatusSum
                        }
                        $PolicySum.PSObject.TypeNames.Insert(0, 'TeamsDeviceCompliancePolicy')
                        $outputSum.Add($PolicySum) | Out-Null
                    }
                    elseif (($AssignedToGroup.count -eq 0) -and !($UserUPN -or $DeviceID -or $Detailed)) {
                        $skippedCompliancePolicies++
                    }
                }
            }
            if ($totalCompliancePolicies -eq 0) {
                if ($UserUPN) {
                    Write-Warning ("The user " + $UserUPN + " doesn't have any Compliance Policies assigned.")
                }
                else {
                    Write-Warning "No Compliance Policies assigned to All Users, All Devices or group found. Please use Test-UcTeamsDevicesCompliancePolicy -All to check all policies."
                }
            }
            if ($IncludeSupported -and $Detailed) {
                if ($ExportCSV) {
                    $output | Sort-Object PolicyName, ID | Select-Object PolicyName, PolicyID, PolicyType, AssignedToGroup, ExcludedFromGroup, TeamsDevicesStatus, Setting, SettingDescription, Value, Comment | Export-Csv -path $OutputFullPath -NoTypeInformation
                    Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan
                    return
                }
                else {
                    $output | Sort-Object PolicyName, ID
                }
            }
            elseif ($Detailed) {
                if ((( $output | Where-Object -Property TeamsDevicesStatus -NE -Value "Supported").count -eq 0) -and !$IncludeSupported) {
                    Write-Warning "No unsupported settings found, please use Test-UcTeamsDevicesCompliancePolicy -IncludeSupported to output all settings."
                }
                else {
                    if ($ExportCSV) {
                        $output | Where-Object -Property TeamsDevicesStatus -NE -Value "Supported" | Sort-Object PolicyName, ID | Select-Object PolicyName, PolicyID, PolicyType, AssignedToGroup, ExcludedFromGroup, TeamsDevicesStatus, Setting, SettingDescription, Value, Comment | Export-Csv -path $OutputFullPath -NoTypeInformation
                        Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan
                        return
                    }
                    else {
                        $output | Where-Object -Property TeamsDevicesStatus -NE -Value "Supported" | Sort-Object PolicyName, ID
                    }
                }
            }
            else {
                if (($skippedCompliancePolicies -gt 0) -and !$All) {
                    Write-Warning ("Skipping $skippedCompliancePolicies compliance policies since will not be applied to Teams Devices.")
                    Write-Warning ("Please use the All switch to check all policies: Test-UcTeamsDevicesCompliancePolicy -All")
                }
                if ($displayWarning) {
                    Write-Warning "One or more policies contain unsupported settings, please use Test-UcTeamsDevicesCompliancePolicy -Detailed to identify the unsupported settings."
                }
                $outputSum | Sort-Object PolicyName
            }
        }
    }
}
#EndRegion '.\Public\Test-UcTeamsDevicesCompliancePolicy.ps1' 1059
#Region '.\Public\Test-UcTeamsDevicesConditionalAccessPolicy.ps1' -1

function Test-UcTeamsDevicesConditionalAccessPolicy {
    param(
        [switch]$Detailed,
        [switch]$All,
        [switch]$IncludeSupported,
        [string]$UserUPN,
        [switch]$ExportCSV,
        [string]$OutputPath
    )
    <#
        .SYNOPSIS
        Validate which Conditional Access policies are supported by Microsoft Teams Android Devices
 
        .DESCRIPTION
        This function will validate each setting in a Conditional Access Policy to make sure they are in line with the supported settings:
 
            https://docs.microsoft.com/microsoftteams/rooms/supported-ca-and-compliance-policies?tabs=phones#conditional-access-policies"
 
        Contributors: Traci Herr, David Paulino
 
        Requirements: Microsoft Graph PowerShell Module (Install-Module Microsoft.Graph)
 
        .PARAMETER Detailed
        Displays test results for all settings in each Conditional Access Policy
 
        .PARAMETER All
        Will check all Conditional Access policies independently if they are assigned to a Group(s) or to Teams
 
        .PARAMETER IncludeSupported
        Displays results for all settings in each Conditional Access Policy
 
        .PARAMETER UserUPN
        Specifies a UserUPN that we want to check for applied Conditional Access policies
 
        .PARAMETER ExportCSV
        When present will export the detailed results to a CSV file. By defautl will save the file under the current user downloads, unless we specify the OutputPath.
 
        .PARAMETER OutputPath
        Allows to specify the path where we want to save the results.
 
        .EXAMPLE
        PS> Test-UcTeamsDevicesConditionalAccessPolicy
 
        .EXAMPLE
        PS> Test-UcTeamsDevicesConditionalAccessPolicy -All
 
        .EXAMPLE
        PS> Test-UcTeamsDevicesConditionalAccessPolicy -Detailed
 
        .EXAMPLE
        PS> Test-UcTeamsDevicesConditionalAccessPolicy -Detailed -IncludedSupported
 
        .EXAMPLE
        PS> Test-UcTeamsDevicesConditionalAccessPolicy -UserUPN
    #>


    $GraphURI_Users = "https://graph.microsoft.com/v1.0/users"
    $GraphURI_Groups = "https://graph.microsoft.com/v1.0/groups"
    $GraphURI_ConditionalAccess = "https://graph.microsoft.com/beta/identity/conditionalAccess/policies"

    $connectedMSGraph = $false
    $ConditionalAccessPolicies = $null
    $totalCAPolicies = 0
    $skippedCAPolicies = 0

    $URLTeamsDevicesCA = "https://aka.ms/TeamsDevicePolicies#supported-conditional-access-policies"
    $URLTeamsDevicesKnownIssues = "https://docs.microsoft.com/microsoftteams/troubleshoot/teams-rooms-and-devices/rooms-known-issues#teams-phone-devices"

    if (Test-UcMgGraphConnection -Scopes "Policy.Read.All", "Directory.Read.All") {
        Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null
        $outFileName = "TeamsDevices_ConditionalAccessPolicy_Report_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv"

        if ($OutputPath) {
            if (!(Test-Path $OutputPath -PathType Container)) {
                Write-Host ("Error: Invalid folder " + $OutputPath) -ForegroundColor Red
                return
            } 
            $OutputFullPath = [System.IO.Path]::Combine($OutputPath, $outFileName)
        }
        else {                
            $OutputFullPath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads", $outFileName)
        }
        try {
            Write-Progress -Activity "Test-UcTeamsDevicesConditionalAccessPolicy" -Status "Getting Conditional Access Policies"
            $ConditionalAccessPolicies = (Invoke-MgGraphRequest -Uri ($GraphURI_ConditionalAccess + $GraphFilter) -Method GET).Value
            $connectedMSGraph = $true
        }
        catch [System.Net.Http.HttpRequestException] {
            if ($PSItem.Exception.Response.StatusCode -eq "Forbidden") {
                Write-Host "Access Denied, please make sure the user connecing to MS Graph is part of one of the following Global Reader/Conditional Access Administrator/Global Administrator roles"
                return
            }
            else {
                Write-Error $PSItem.Exception.Message
            }
        }
        catch {
            Write-Error $PSItem.Exception.Message
        }

        if ($connectedMSGraph) {
            $output = [System.Collections.ArrayList]::new()
            $outputSum = [System.Collections.ArrayList]::new()
            if ($UserUPN) {
                try {
                    $UserID = (Invoke-MgGraphRequest -Uri ($GraphURI_Users + "/" + $userUPN + "?`$select=id") -Method GET).id
                    $UserGroups = (Invoke-MgGraphRequest -Uri ($GraphURI_Users + "/" + $userUPN + "/transitiveMemberOf?`$select=id") -Method GET).value.id
                }
                catch [System.Net.Http.HttpRequestException] {
                    if ($PSItem.Exception.Response.StatusCode -eq "NotFound") {
                        Write-warning -Message ("User Not Found: " + $UserUPN)
                    }
                    return
                }
            }

            $Groups = New-Object 'System.Collections.Generic.Dictionary[string, string]'
            try {
                Write-Progress -Activity "Test-UcTeamsDevicesConditionalAccessPolicy" -Status "Fetching Service Principals details."
                $ServicePrincipals = Get-MgServicePrincipal -Select AppId, DisplayName -All
            }
            catch {}

            $p = 0
            $policyCount = $ConditionalAccessPolicies.Count
            foreach ($ConditionalAccessPolicy in $ConditionalAccessPolicies) {
                $p++
                Write-Progress -Activity "Test-UcTeamsDevicesConditionalAccessPolicy" -Status ("Checking policy " + $ConditionalAccessPolicy.displayName + " - $p of $policyCount")
                $AssignedToGroup = [System.Collections.ArrayList]::new()
                $ExcludedFromGroup = [System.Collections.ArrayList]::new()
                $AssignedToUserCount = 0
                $ExcludedFromUserCount = 0
                $outAssignedToGroup = ""
                $outExcludedFromGroup = ""
                $userExcluded = $false
                $StatusSum = ""
            
                $totalCAPolicies++
                $PolicyErrors = 0
                $PolicyWarnings = 0

                if ($UserUPN) {
                    if ($UserID -in $ConditionalAccessPolicy.conditions.users.excludeUsers) {
                        $userExcluded = $true
                        Write-Warning ("Skiping conditional access policy " + $ConditionalAccessPolicy.displayName + ", since user " + $UserUPN + " is part of Excluded Users")
                    }
                    elseif ($UserID -in $ConditionalAccessPolicy.conditions.users.includeUsers) {
                        $userIncluded = $true
                    }
                    else {
                        $userIncluded = $false
                    }
                }

                #All Users in Conditional Access Policy will show as a 'All' in the includeUsers.
                if ("All" -in $ConditionalAccessPolicy.conditions.users.includeUsers) {
                    $GroupEntry = New-Object -TypeName PSObject -Property @{
                        GroupID          = "All"
                        GroupDisplayName = "All Users"
                    }
                    $AssignedToGroup.Add($GroupEntry) | Out-Null
                    $userIncluded = $true
                }
                elseif ((($ConditionalAccessPolicy.conditions.users.includeUsers).count -gt 0) -and "None" -notin $ConditionalAccessPolicy.conditions.users.includeUsers) {
                    $AssignedToUserCount = ($ConditionalAccessPolicy.conditions.users.includeUsers).count
                    if (!$UserUPN) {
                        $userIncluded = $true
                    }
                }
                foreach ($includedGroup in $ConditionalAccessPolicy.conditions.users.includeGroups) {
                    $GroupDisplayName = $includedGroup
                    if ($Groups.ContainsKey($includedGroup)) {
                        $GroupDisplayName = $Groups.Item($includedGroup)
                    }
                    else {
                        try {
                            $GroupInfo = Invoke-MgGraphRequest -Uri ($GraphURI_Groups + "/" + $includedGroup + "/?`$select=id,displayname") -Method GET
                            $Groups.Add($GroupInfo.id, $GroupInfo.displayname)
                            $GroupDisplayName = $GroupInfo.displayname
                        }
                        catch {
                        }
                    }
                    $GroupEntry = New-Object -TypeName PSObject -Property @{
                        GroupID          = $includedGroup
                        GroupDisplayName = $GroupDisplayName
                    }
                    #We only need to add if we didn't specify a UPN or if the user is part of the group that has the CA assigned.
                    if (!$UserUPN) {
                        $AssignedToGroup.Add($GroupEntry) | Out-Null
                    }
                    if ($includedGroup -in $UserGroups) {
                        $userIncluded = $true
                        $AssignedToGroup.Add($GroupEntry) | Out-Null
                    }
                }

            
                foreach ($excludedGroup in $ConditionalAccessPolicy.conditions.users.excludeGroups) {
                    $GroupDisplayName = $excludedGroup
                    if ($Groups.ContainsKey($excludedGroup)) {
                        $GroupDisplayName = $Groups.Item($excludedGroup)
                    }
                    else {
                        try {
                            $GroupInfo = Invoke-MgGraphRequest -Uri ($GraphURI_Groups + "/" + $excludedGroup + "/?`$select=id,displayname") -Method GET
                            $Groups.Add($GroupInfo.id, $GroupInfo.displayname)
                            $GroupDisplayName = $GroupInfo.displayname
                        }
                        catch { }
                    }
                    $GroupEntry = New-Object -TypeName PSObject -Property @{
                        GroupID          = $excludedGroup
                        GroupDisplayName = $GroupDisplayName
                    }
                    $ExcludedFromGroup.Add($GroupEntry) | Out-Null                
                    if ($excludedGroup -in $UserGroups) {
                        $userExcluded = $true
                        Write-Warning ("Skiping conditional access policy " + $ConditionalAccessPolicy.displayName + ", since user " + $UserUPN + " is part of an Excluded Group: " + $GroupEntry.GroupDisplayName)
                    }
                }
                $ExcludedFromUserCount = ($ConditionalAccessPolicy.conditions.users.excludeUsers).count

                if ("GuestsOrExternalUsers" -in $ConditionalAccessPolicy.conditions.users.excludeUsers) {
                    $ExcludedFromUserCount--
                }

                #If only assigned/excluded from a group we will show the group display name, otherwise the number of groups assigned/excluded.
                if (($AssignedToGroup.count -gt 0) -and ($AssignedToUserCount -gt 0)) {
                    $outAssignedToGroup = "$AssignedToUserCount user(s)," + $AssignedToGroup.count + " group(s)"
                }
                elseif (($AssignedToGroup.count -eq 0) -and ($AssignedToUserCount -gt 0)) {
                    $outAssignedToGroup = "$AssignedToUserCount user(s)"
                }
                elseif (($AssignedToGroup.count -gt 0) -and ($AssignedToUserCount -eq 0)) {
                    if ($AssignedToGroup.count -eq 1) {
                        $outAssignedToGroup = $AssignedToGroup[0].GroupDisplayName
                    }
                    else {
                        $outAssignedToGroup = "" + $AssignedToGroup.count + " group(s)"
                    }
                }
                else {
                    $outAssignedToGroup = "None"
                }

                if (($ExcludedFromGroup.count -gt 0) -and ($ExcludedFromUserCount -gt 0)) {
                    $outExcludedFromGroup = "$ExcludedFromUserCount user(s), " + $ExcludedFromGroup.count + " group(s)"
                }
                elseif (($ExcludedFromGroup.count -eq 0) -and ($ExcludedFromUserCount -gt 0)) {
                    $outExcludedFromGroup = "$ExcludedFromUserCount user(s)"
                }
                elseif (($ExcludedFromGroup.count -gt 0) -and ($ExcludedFromUserCount -eq 0)) {
                    if ($ExcludedFromGroup.count -eq 1) {
                        $outExcludedFromGroup = $ExcludedFromGroup[0].GroupDisplayName
                    }
                    else {
                        $outExcludedFromGroup = "" + $ExcludedFromGroup.count + " group(s)"
                    }
                }
                else {
                    $outExcludedFromGroup = "None"
                }

                $PolicyState = $ConditionalAccessPolicy.State
                if ($PolicyState -eq "enabledForReportingButNotEnforced") {
                    $PolicyState = "ReportOnly"
                }
                
                #region 2: Assignment > Cloud apps or actions > Cloud Apps
                #Exchange 00000002-0000-0ff1-ce00-000000000000
                #SharePoint 00000003-0000-0ff1-ce00-000000000000
                #Teams cc15fd57-2c6c-4117-a88c-83b1d56b4bbe
                $ID = 2
                $Setting = "CloudApps"
                $SettingDescription = "Assignment > Cloud apps or actions > Cloud Apps"
                $Comment = ""
                $hasExchange = $false
                $hasSharePoint = $false
                $hasTeams = $false
                $hasOffice365 = $false
                $SettingValue = ""
                foreach ($Application in $ConditionalAccessPolicy.Conditions.Applications.IncludeApplications) {
                    $appDisplayName = ($ServicePrincipals |  Where-Object -Property AppId -eq -Value $Application).DisplayName
                    switch ($Application) {
                        "All" { $hasOffice365 = $true; $SettingValue = "All" }
                        "Office365" { $hasOffice365 = $true; $SettingValue = "Office 365" }
                        "00000002-0000-0ff1-ce00-000000000000" { $hasExchange = $true; $SettingValue += $appDisplayName + "; " }
                        "00000003-0000-0ff1-ce00-000000000000" { $hasSharePoint = $true; $SettingValue += $appDisplayName + "; " }
                        "cc15fd57-2c6c-4117-a88c-83b1d56b4bbe" { $hasTeams = $true; $SettingValue += $appDisplayName + "; " }
                        default { $SettingValue += $appDisplayName + "; " }
                    }
                }
                if ($SettingValue.EndsWith("; ")) {
                    $SettingValue = $SettingValue.Substring(0, $SettingValue.Length - 2)
                }

                if (((($AssignedToGroup.count -gt 0) -and ($hasOffice365 -or $hasTeams) -and ($PolicyState -NE "disabled")) -and (!$userExcluded) -and $userIncluded) -or $all) {

                    if (($hasExchange -and $hasSharePoint -and $hasTeams) -or ($hasOffice365)) {
                        $Status = "Supported"
                    }
                    else {
                        $Status = "Unsupported"
                        $Comment = "Teams Devices needs to access: Office 365 or Exchange Online, SharePoint Online, and Microsoft Teams"
                        $PolicyErrors++
                    }

                    $SettingPSObj = [PSCustomObject]@{
                        PolicyName            = $ConditionalAccessPolicy.displayName
                        PolicyState           = $PolicyState
                        Setting               = $Setting 
                        Value                 = $SettingValue
                        TeamsDevicesStatus    = $Status 
                        Comment               = $Comment
                        SettingDescription    = $SettingDescription 
                        AssignedToGroup       = $outAssignedToGroup
                        ExcludedFromGroup     = $outExcludedFromGroup 
                        AssignedToGroupList   = $AssignedToGroup
                        ExcludedFromGroupList = $ExcludedFromGroup
                        PolicyID              = $ConditionalAccessPolicy.id
                        ID                    = $ID
                    }
                    $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed')
                    [void]$output.Add($SettingPSObj)
                    #endregion

                    #region 6: Assignment > Conditions > Locations
                    $ID = 6.1
                    $Setting = "includeLocations"
                    $SettingDescription = "Assignment > Conditions > Locations"
                    $Comment = ""
                    $Status = "Supported"
                    if ($ConditionalAccessPolicy.conditions.locations.includeLocations) {
                        $SettingValue = $ConditionalAccessPolicy.conditions.locations.includeLocations
                    }
                    else {
                        $SettingValue = "Not Configured"
                    }
                    $SettingPSObj = [PSCustomObject]@{
                        PolicyName            = $ConditionalAccessPolicy.displayName
                        PolicyState           = $PolicyState
                        Setting               = $Setting 
                        Value                 = $SettingValue
                        TeamsDevicesStatus    = $Status 
                        Comment               = $Comment
                        SettingDescription    = $SettingDescription 
                        AssignedToGroup       = $outAssignedToGroup
                        ExcludedFromGroup     = $outExcludedFromGroup 
                        AssignedToGroupList   = $AssignedToGroup
                        ExcludedFromGroupList = $ExcludedFromGroup
                        PolicyID              = $ConditionalAccessPolicy.id
                        ID                    = $ID
                    }
                    $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed')
                    [void]$output.Add($SettingPSObj)

                    $ID = 6.2
                    $Setting = "excludeLocations"
                    $SettingDescription = "Assignment > Conditions > Locations"
                    $Comment = ""
                    $Status = "Supported"
                    if ($ConditionalAccessPolicy.conditions.locations.excludeLocations) {
                        $SettingValue = $ConditionalAccessPolicy.conditions.locations.excludeLocations
                    }
                    else {
                        $SettingValue = "Not Configured"
                    }
                    $SettingPSObj = [PSCustomObject]@{
                        PolicyName            = $ConditionalAccessPolicy.displayName
                        PolicyState           = $PolicyState
                        Setting               = $Setting 
                        Value                 = $SettingValue
                        TeamsDevicesStatus    = $Status 
                        Comment               = $Comment
                        SettingDescription    = $SettingDescription 
                        AssignedToGroup       = $outAssignedToGroup
                        ExcludedFromGroup     = $outExcludedFromGroup 
                        AssignedToGroupList   = $AssignedToGroup
                        ExcludedFromGroupList = $ExcludedFromGroup
                        PolicyID              = $ConditionalAccessPolicy.id
                        ID                    = $ID
                    }
                    $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed')
                    [void]$output.Add($SettingPSObj)
                    #endregion

                    #region 7: Assignment > Conditions > Client apps
                    $ID = 7
                    $Setting = "ClientAppTypes"
                    $SettingDescription = "Assignment > Conditions > Client apps"
                    $SettingValue = ""
                    $Comment = ""
                    foreach ($ClientAppType in $ConditionalAccessPolicy.Conditions.ClientAppTypes) {
                        if ($ClientAppType -eq "All") {
                            $Status = "Supported"
                            $SettingValue = $ClientAppType
                            $Comment = ""
                        }
                        else {
                            $Status = "Unsupported"
                            $SettingValue += $ClientAppType + ";"
                            $Comment = $URLTeamsDevicesCA
                            $PolicyErrors++
                        }
                    
                    }
                    $SettingPSObj = [PSCustomObject]@{
                        PolicyName            = $ConditionalAccessPolicy.displayName
                        PolicyState           = $PolicyState
                        Setting               = $Setting 
                        Value                 = $SettingValue
                        TeamsDevicesStatus    = $Status 
                        Comment               = $Comment
                        SettingDescription    = $SettingDescription 
                        AssignedToGroup       = $outAssignedToGroup
                        ExcludedFromGroup     = $outExcludedFromGroup 
                        AssignedToGroupList   = $AssignedToGroup
                        ExcludedFromGroupList = $ExcludedFromGroup
                        PolicyID              = $ConditionalAccessPolicy.id
                        ID                    = $ID
                    }
                    $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed')
                    [void]$output.Add($SettingPSObj)
                    #endregion

                    #region 8: Assignment > Conditions > Filter for devices
                    $ID = 8
                    $Setting = "deviceFilter"
                    $SettingDescription = "Assignment > Conditions > Filter for devices"
                    $Comment = ""
                    if ($ConditionalAccessPolicy.conditions.devices.deviceFilter.mode -eq "exclude") {
                        $Status = "Supported"
                        $SettingValue = $ConditionalAccessPolicy.conditions.devices.deviceFilter.mode + ": " + $ConditionalAccessPolicy.conditions.devices.deviceFilter.rule
                    }
                    else {
                        $SettingValue = "Not Configured"
                        $Status = "Warning"
                        $Comment = "https://learn.microsoft.com/microsoftteams/troubleshoot/teams-rooms-and-devices/teams-android-devices-conditional-access-issues"
                    }
                    $SettingPSObj = [PSCustomObject]@{
                        PolicyName            = $ConditionalAccessPolicy.displayName
                        PolicyState           = $PolicyState
                        Setting               = $Setting 
                        Value                 = $SettingValue
                        TeamsDevicesStatus    = $Status 
                        Comment               = $Comment
                        SettingDescription    = $SettingDescription 
                        AssignedToGroup       = $outAssignedToGroup
                        ExcludedFromGroup     = $outExcludedFromGroup 
                        AssignedToGroupList   = $AssignedToGroup
                        ExcludedFromGroupList = $ExcludedFromGroup
                        PolicyID              = $ConditionalAccessPolicy.id
                        ID                    = $ID
                    }
                    $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed')
                    [void]$output.Add($SettingPSObj)
                    #endregion

                    #20240924 - Added check authentication flows
                    #region 9: Assignment > Conditions > Authentication flows
                    $ID = 9
                    $Setting = "authenticationFlows"
                    $SettingDescription = "Assignment > Conditions > Authentication flows"
                    $Comment = ""
                    if ($ConditionalAccessPolicy.conditions.authenticationFlows.transferMethods -like "*deviceCodeFlow*") {
                        $SettingValue = $ConditionalAccessPolicy.conditions.authenticationFlows.transferMethods
                        if($ConditionalAccessPolicy.GrantControls.BuiltInControls -contains "block"){
                             $Status = "Not Supported"
                             $Comment = "Authentication flows with block will prevent Teams Devices Remote Sign in" 
                             $PolicyErrors++
                        }else{
                            $Status = "Supported"
                        }
                    }
                    else {
                        $SettingValue = "Not Configured"
                        $Status = "Supported"
                    }
                    $SettingPSObj = [PSCustomObject]@{
                        PolicyName            = $ConditionalAccessPolicy.displayName
                        PolicyState           = $PolicyState
                        Setting               = $Setting 
                        Value                 = $SettingValue
                        TeamsDevicesStatus    = $Status 
                        Comment               = $Comment
                        SettingDescription    = $SettingDescription 
                        AssignedToGroup       = $outAssignedToGroup
                        ExcludedFromGroup     = $outExcludedFromGroup 
                        AssignedToGroupList   = $AssignedToGroup
                        ExcludedFromGroupList = $ExcludedFromGroup
                        PolicyID              = $ConditionalAccessPolicy.id
                        ID                    = $ID
                    }
                    $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed')
                    [void]$output.Add($SettingPSObj)
                    #endregion

                    #region 10: Access controls > Grant
                    $Setting = "GrantControls"
                    foreach ($BuiltInControl in $ConditionalAccessPolicy.GrantControls.BuiltInControls) {
                        $Comment = "" 
                        $SettingValue = "Enabled"
                        switch ($BuiltInControl) {
                            "mfa" { 
                                $ID = 11
                                $Status = "Warning"
                                $SettingDescription = "Access controls > Grant > Require multi-factor authentication"
                                $PolicyWarnings++
                                $Comment = "Require multi-factor authentication only supported for Teams Phones and Displays." 
                            }
                            "compliantDevice" {
                                $ID = 12
                                $Status = "Supported"
                                $SettingDescription = "Access controls > Grant > Require device to be marked as compliant"
                            }
                            "DomainJoinedDevice" { 
                                $ID = 13
                                $Status = "Unsupported"
                                $SettingDescription = "Access controls > Grant > Require Hybrid Azure AD joined device"
                                $PolicyErrors++
                            }
                            "ApprovedApplication" { 
                                $ID = 14
                                $Status = "Unsupported"
                                $SettingDescription = "Access controls > Grant > Require approved client app"
                                $Comment = $URLTeamsDevicesCA
                                $PolicyErrors++
                            }
                            "CompliantApplication" { 
                                $ID = 15
                                $Status = "Unsupported"
                                $SettingDescription = "Access controls > Grant > Require app protection policy"
                                $Comment = $URLTeamsDevicesCA
                                $PolicyErrors++
                            }
                            "PasswordChange" { 
                                $ID = 16
                                $Status = "Unsupported"
                                $SettingDescription = "Access controls > Grant > Require password change"
                                $Comment = $URLTeamsDevicesCA 
                                $PolicyErrors++
                            }

                            default { 
                                $ID = 10
                                $SettingDescription = "Access controls > Grant > " + $BuiltInControl
                                $Status = "Supported"
                            }
                        }
                        $SettingPSObj = [PSCustomObject]@{
                            PolicyName            = $ConditionalAccessPolicy.displayName
                            PolicyState           = $PolicyState
                            Setting               = $Setting 
                            Value                 = $SettingValue
                            TeamsDevicesStatus    = $Status 
                            Comment               = $Comment
                            SettingDescription    = $SettingDescription 
                            AssignedToGroup       = $outAssignedToGroup
                            ExcludedFromGroup     = $outExcludedFromGroup 
                            AssignedToGroupList   = $AssignedToGroup
                            ExcludedFromGroupList = $ExcludedFromGroup
                            PolicyID              = $ConditionalAccessPolicy.id
                            ID                    = $ID
                        }
                        $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed')
                        [void]$output.Add($SettingPSObj) 
                    }
                    #endregion
                
                    #region 17: Access controls > Grant > Custom Authentication Factors
                    $ID = 17
                    $Setting = "CustomAuthenticationFactors"
                    $SettingDescription = "Access controls > Grant > Custom Authentication Factors"
                    if ($ConditionalAccessPolicy.GrantControls.CustomAuthenticationFactors) {
                        $Status = "Unsupported"
                        $SettingValue = "Enabled"
                        $PolicyErrors++
                        $Comment = $URLTeamsDevicesCA
                    }
                    else {
                        $Status = "Supported"
                        $SettingValue = "Disabled"
                    }
                    $SettingPSObj = [PSCustomObject]@{
                        PolicyName            = $ConditionalAccessPolicy.displayName
                        PolicyState           = $PolicyState
                        Setting               = $Setting 
                        Value                 = $SettingValue
                        TeamsDevicesStatus    = $Status 
                        Comment               = $Comment
                        SettingDescription    = $SettingDescription 
                        AssignedToGroup       = $outAssignedToGroup
                        ExcludedFromGroup     = $outExcludedFromGroup 
                        AssignedToGroupList   = $AssignedToGroup
                        ExcludedFromGroupList = $ExcludedFromGroup
                        PolicyID              = $ConditionalAccessPolicy.id
                        ID                    = $ID
                    }
                    $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed')
                    [void]$output.Add($SettingPSObj)
                    #endregion

                    #region 18: Access controls > Grant > Terms of Use
                    $ID = 18
                    $Setting = "TermsOfUse"
                    $SettingDescription = "Access controls > Grant > Terms of Use"
                    $Comment = "" 
                    if ($ConditionalAccessPolicy.GrantControls.TermsOfUse) {
                        $Status = "Warning"
                        $SettingValue = "Enabled"
                        $Comment = $URLTeamsDevicesKnownIssues
                        $PolicyWarnings++
                    }
                    else {
                        $Status = "Supported"
                        $SettingValue = "Disabled"
                    }
                    $SettingPSObj = [PSCustomObject]@{
                        PolicyName            = $ConditionalAccessPolicy.displayName
                        PolicyState           = $PolicyState
                        Setting               = $Setting 
                        Value                 = $SettingValue
                        TeamsDevicesStatus    = $Status 
                        Comment               = $Comment
                        SettingDescription    = $SettingDescription 
                        AssignedToGroup       = $outAssignedToGroup
                        ExcludedFromGroup     = $outExcludedFromGroup 
                        AssignedToGroupList   = $AssignedToGroup
                        ExcludedFromGroupList = $ExcludedFromGroup
                        PolicyID              = $ConditionalAccessPolicy.id
                        ID                    = $ID
                    }
                    $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed')
                    [void]$output.Add($SettingPSObj)
                    #endregion

                    #region 19: Access controls > Session > Use app enforced restrictions
                    $ID = 19
                    $Setting = "ApplicationEnforcedRestrictions"
                    $SettingDescription = "Access controls > Session > Use app enforced restrictions"
                    $Comment = "" 
                    if ($ConditionalAccessPolicy.SessionControls.ApplicationEnforcedRestrictions) {
                        $Status = "Unsupported"
                        $SettingValue = "Enabled"
                        $PolicyErrors++
                    }
                    else {
                        $Status = "Supported"
                        $SettingValue = "Disabled"
                    }
                    $SettingPSObj = [PSCustomObject]@{
                        PolicyName            = $ConditionalAccessPolicy.displayName
                        PolicyState           = $PolicyState
                        Setting               = $Setting 
                        Value                 = $SettingValue
                        TeamsDevicesStatus    = $Status 
                        Comment               = $Comment
                        SettingDescription    = $SettingDescription 
                        AssignedToGroup       = $outAssignedToGroup
                        ExcludedFromGroup     = $outExcludedFromGroup 
                        AssignedToGroupList   = $AssignedToGroup
                        ExcludedFromGroupList = $ExcludedFromGroup
                        PolicyID              = $ConditionalAccessPolicy.id
                        ID                    = $ID
                    }
                    $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed')
                    [void]$output.Add($SettingPSObj)
                    #endregion
                
                    #region 19: Access controls > Session > Use Conditional Access App Control
                    $ID = 19
                    $Setting = "CloudAppSecurity"
                    $SettingDescription = "Access controls > Session > Use Conditional Access App Control"
                    $Comment = "" 
                    if ($ConditionalAccessPolicy.SessionControls.CloudAppSecurity) {
                        $Status = "Unsupported"
                        $SettingValue = $ConditionalAccessPolicy.SessionControls.CloudAppSecurity.cloudAppSecurityType
                        $PolicyErrors++
                    }
                    else {
                        $Status = "Supported"
                        $SettingValue = "Not Configured"
                    }
                    $SettingPSObj = [PSCustomObject]@{
                        PolicyName            = $ConditionalAccessPolicy.displayName
                        PolicyState           = $PolicyState
                        Setting               = $Setting 
                        Value                 = $SettingValue
                        TeamsDevicesStatus    = $Status 
                        Comment               = $Comment
                        SettingDescription    = $SettingDescription 
                        AssignedToGroup       = $outAssignedToGroup
                        ExcludedFromGroup     = $outExcludedFromGroup 
                        AssignedToGroupList   = $AssignedToGroup
                        ExcludedFromGroupList = $ExcludedFromGroup
                        PolicyID              = $ConditionalAccessPolicy.id
                        ID                    = $ID
                    }
                    $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed')
                    [void]$output.Add($SettingPSObj)
                    #endregion

                    #region 20: Access controls > Session > Sign-in frequency
                    $ID = 20
                    $Setting = "SignInFrequency"
                    $SettingDescription = "Access controls > Session > Sign-in frequency"
                    $Comment = "" 
                    if ($ConditionalAccessPolicy.SessionControls.SignInFrequency.isEnabled -eq "true") {
                        $Status = "Warning"
                        $SettingValue = "" + $ConditionalAccessPolicy.SessionControls.SignInFrequency.Value + " " + $ConditionalAccessPolicy.SessionControls.SignInFrequency.Type
                        $Comment = "Users will be signout from Teams Device every " + $ConditionalAccessPolicy.SessionControls.SignInFrequency.Value + " " + $ConditionalAccessPolicy.SessionControls.SignInFrequency.Type
                        $PolicyWarnings++
                    }
                    else {
                        $Status = "Supported"
                        $SettingValue = "Not Configured"
                    }
                    $SettingPSObj = [PSCustomObject]@{
                        PolicyName            = $ConditionalAccessPolicy.displayName
                        PolicyState           = $PolicyState
                        Setting               = $Setting 
                        Value                 = $SettingValue
                        TeamsDevicesStatus    = $Status 
                        Comment               = $Comment
                        SettingDescription    = $SettingDescription 
                        AssignedToGroup       = $outAssignedToGroup
                        ExcludedFromGroup     = $outExcludedFromGroup 
                        AssignedToGroupList   = $AssignedToGroup
                        ExcludedFromGroupList = $ExcludedFromGroup
                        PolicyID              = $ConditionalAccessPolicy.id
                        ID                    = $ID
                    }
                    $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed')
                    [void]$output.Add($SettingPSObj)
                    #endregion

                    #region 21: Access controls > Session > Persistent browser session
                    $ID = 21
                    $Setting = "PersistentBrowser"
                    $SettingDescription = "Access controls > Session > Persistent browser session"
                    $Comment = "" 
                    if ($ConditionalAccessPolicy.SessionControls.PersistentBrowser.isEnabled -eq "true") {
                        $Status = "Unsupported"
                        $SettingValue = $ConditionalAccessPolicy.SessionControls.persistentBrowser.mode
                        $PolicyErrors++
                    }
                    else {
                        $Status = "Supported"
                        $SettingValue = "Not Configured"
                    }
                
                    $SettingPSObj = [PSCustomObject]@{
                        PolicyName            = $ConditionalAccessPolicy.displayName
                        PolicyState           = $PolicyState
                        Setting               = $Setting 
                        Value                 = $SettingValue
                        TeamsDevicesStatus    = $Status 
                        Comment               = $Comment
                        SettingDescription    = $SettingDescription 
                        AssignedToGroup       = $outAssignedToGroup
                        ExcludedFromGroup     = $outExcludedFromGroup 
                        AssignedToGroupList   = $AssignedToGroup
                        ExcludedFromGroupList = $ExcludedFromGroup
                        PolicyID              = $ConditionalAccessPolicy.id
                        ID                    = $ID
                    }
                    $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicyDetailed')
                    [void]$output.Add($SettingPSObj)
                    #endregion

                    if ($PolicyErrors -gt 0) {
                        $StatusSum = "Has " + $PolicyErrors + " unsupported settings."
                        $displayWarning = $true
                    }
                    elseif ($PolicyWarnings -gt 0) {
                        $StatusSum = "Has " + $PolicyWarnings + " settings that may impact users."
                        $displayWarning = $true
                    }
                    else {
                        $StatusSum = "All settings supported."
                    }
                    $PolicySum = [PSCustomObject]@{
                        PolicyID              = $ConditionalAccessPolicy.id
                        PolicyName            = $ConditionalAccessPolicy.DisplayName
                        PolicyState           = $PolicyState
                        AssignedToGroup       = $outAssignedToGroup
                        AssignedToGroupList   = $AssignedToGroup
                        ExcludedFromGroup     = $outExcludedFromGroup 
                        ExcludedFromGroupList = $ExcludedFromGroup
                        TeamsDevicesStatus    = $StatusSum
                    }
                    $PolicySum.PSObject.TypeNames.Insert(0, 'TeamsDeviceConditionalAccessPolicy')
                    [void]$outputSum.Add($PolicySum)
                }
                else {
                    $skippedCAPolicies++
                }
            }
            if ($totalCAPolicies -eq 0) {
                if ($UserUPN) {
                    Write-Warning ("The user " + $UserUPN + " doesn't have any Compliance Policies assigned.")
                }
                else {
                    Write-Warning "No Conditional Access Policies assigned to All Users, All Devices or group found. Please use Test-UcTeamsDevicesConditionalAccessPolicy -IgnoreAssigment to check all policies."
                }
            }
        
            if ($IncludeSupported -and $Detailed) {
                if ($ExportCSV) {
                    $output | Sort-Object PolicyName, ID | Select-Object PolicyName, PolicyID, PolicyState, AssignedToGroup, ExcludedFromGroup, TeamsDevicesStatus, Setting, SettingDescription, Value, Comment | Export-Csv -path $OutputFullPath -NoTypeInformation
                    Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan
                    return
                }
                else {
                    $output | Sort-Object PolicyName, ID
                }
            }
            elseif ($Detailed) {
                if ((( $output | Where-Object -Property TeamsDevicesStatus -NE -Value "Supported").count -eq 0) -and !$IncludeSupported) {
                    Write-Warning "No unsupported settings found, please use Test-UcTeamsDevicesConditionalAccessPolicy -IncludeSupported, to output all settings."
                }
                else {
                    if ($ExportCSV) {
                        $output | Where-Object -Property TeamsDevicesStatus -NE -Value "Supported" | Sort-Object PolicyName, ID | Select-Object PolicyName, PolicyID, PolicyState, AssignedToGroup, ExcludedFromGroup, TeamsDevicesStatus, Setting, SettingDescription, Value, Comment | Export-Csv -path $OutputFullPath -NoTypeInformation
                        Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan
                    }
                    else {    
                        $output | Where-Object -Property TeamsDevicesStatus -NE -Value "Supported" | Sort-Object PolicyName, ID
                    }
                }
            }
            else {
                if (($skippedCAPolicies -gt 0) -and !$All) {
                    Write-Warning ("Skipping $skippedCAPolicies conditional access policies since they will not be applied to Teams Devices")
                    Write-Warning ("Please use the All switch to check all policies: Test-UcTeamsDevicesConditionalAccessPolicy -All")
                }
                if ($displayWarning) {
                    Write-Warning "One or more policies contain unsupported settings, please use Test-UcTeamsDevicesConditionalAccessPolicy -Detailed to identify the unsupported settings."
                }
                $outputSum | Sort-Object PolicyName
            }
        }
    }
}
#EndRegion '.\Public\Test-UcTeamsDevicesConditionalAccessPolicy.ps1' 846
#Region '.\Public\Test-UcTeamsDevicesEnrollmentPolicy.ps1' -1

function Test-UcTeamsDevicesEnrollmentPolicy {
    param(
        [string]$UserUPN,
        [switch]$Detailed,
        [switch]$ExportCSV,
        [string]$OutputPath
    )    
    <#
        .SYNOPSIS
        Validate Intune Enrollment Policies that are supported by Microsoft Teams Android Devices
 
        .DESCRIPTION
        This function will validate each setting in a Conditional Access Policy to make sure they are in line with the supported settings:
 
        Contributors: David Paulino, Gonçalo Sepulveda
 
        Requirements: Microsoft Graph PowerShell Module (Install-Module Microsoft.Graph)
 
        .PARAMETER UserUPN
        Specifies a UserUPN that we want to check for a user enrollment policies.
 
        .PARAMETER Detailed
        Displays test results for unsupported settings in each Intune Enrollment Policy.
 
        .PARAMETER ExportCSV
        When present will export the detailed results to a CSV file. By defautl will save the file under the current user downloads, unless we specify the OutputPath.
 
        .PARAMETER OutputPath
        Allows to specify the path where we want to save the results.
 
        .EXAMPLE
        PS> Test-UcTeamsDevicesEnrollmentPolicy
 
        .EXAMPLE
        PS> Test-UcTeamsDevicesEnrollmentPolicy -UserUPN
    #>


    $GraphURI_Users = "https://graph.microsoft.com/v1.0/users"
    $GraphURI_EnrollmentPolicies = "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations"
    $GraphURI_AOSPEnrollmentProfiles = "https://graph.microsoft.com/beta/deviceManagement/androidDeviceOwnerEnrollmentProfiles?`$select=displayName,tokenExpirationDateTime&`$filter=enrollmentMode eq 'corporateOwnedAOSPUserAssociatedDevice' and isTeamsDeviceProfile eq true" 
    $output = [System.Collections.ArrayList]::new()

    if (Test-UcMgGraphConnection -Scopes "DeviceManagementServiceConfig.Read.All", "DeviceManagementConfiguration.Read.All", "Directory.Read.All") {
        Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null
        $outFileName = "TeamsDevices_EnrollmentPolicy_Report_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv"
        if ($OutputPath) {
            if (!(Test-Path $OutputPath -PathType Container)) {
                Write-Host ("Error: Invalid folder " + $OutputPath) -ForegroundColor Red
                return
            } 
            $OutputFullPath = [System.IO.Path]::Combine($OutputPath, $outFileName)
        }
        else {                
            $OutputFullPath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads", $outFileName)
        }
        try {
            Write-Progress -Activity "Test-UcTeamsDevicesEnrollmentPolicy" -Status "Getting Intune Enrollment Policies"
            $EnrollmentPolicies = (Invoke-MgGraphRequest -Uri $GraphURI_EnrollmentPolicies -Method GET).value
            Write-Progress -Activity "Test-UcTeamsDevicesEnrollmentPolicy" -Status "Getting AOSP Enrollment Profiles"
            $AOSPEnrollmentProfiles = (Invoke-MgGraphRequest -Uri $GraphURI_AOSPEnrollmentProfiles -Method GET).value
            $connectedMSGraph = $true
        }
        catch [System.Net.Http.HttpRequestException] {
            if ($PSItem.Exception.Response.StatusCode -eq "Unauthorized") {
                Write-Error "Access Denied, please make sure the user connecing to MS Graph is part of one of the following Global Reader/Intune Service Administrator/Global Administrator roles"
            }
            else {
                Write-Error $PSItem.Exception.Message
            }
        }
        catch {
            Write-Error 'Please connect to MS Graph with Connect-MgGraph -Scopes "DeviceManagementServiceConfig.Read.All","Directory.Read.All" before running this script'
        }
        if ($connectedMSGraph) {
            if ($UserUPN) {
                try {
                    $UserGroups = (Invoke-MgGraphRequest -Uri ($GraphURI_Users + "/" + $userUPN + "/transitiveMemberOf?`$select=id") -Method GET).value.id
                }
                catch [System.Net.Http.HttpRequestException] {
                    if ($PSItem.Exception.Response.StatusCode -eq "NotFound") {
                        Write-warning -Message ("User Not Found: " + $UserUPN)
                    }
                    return
                }
            }
            #We need to cycle this in order to get the Group IDs that are assigned to the enrollment policies
            $graphRequests = [System.Collections.ArrayList]::new()
            foreach ($EnrollmentPolicy in $EnrollmentPolicies) {
                #Only Android Policies
                if (($EnrollmentPolicy."@odata.type" -eq "#microsoft.graph.deviceEnrollmentPlatformRestrictionConfiguration") -and ($EnrollmentPolicy.platformType -eq "android") ) {
                    $gRequestTmp = New-Object -TypeName PSObject -Property @{
                        id     = $EnrollmentPolicy.id
                        method = "GET"
                        url    = "/deviceManagement/deviceEnrollmentConfigurations/" + $EnrollmentPolicy.id + "/assignments?`$select=target"
                    }
                    [void]$graphRequests.Add($gRequestTmp)
                }
            }

            $PolicyGroupAssigment = (Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Test-UcTeamsDevicesEnrollmentPolicy, fetching enrollment policies assignment" -IncludeBody)
        
            $graphRequests = [System.Collections.ArrayList]::new()
            foreach ($Group in $PolicyGroupAssigment.body.value.target.GroupID) {
                if (!($UserUPN) -or ($PolicyGroupAssigment.body.value.target.GroupID -in $UserGroups)) {
                    $gRequestTmp = New-Object -TypeName PSObject -Property @{
                        id     = $Group
                        method = "GET"
                        url    = "/groups/" + $Group + "?`$select=id,displayName"
                    }
                    [void]$graphRequests.Add($gRequestTmp)
                }
            }
            if ($graphRequests.Count -gt 0) {
                $Groups = (Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Test-UcTeamsDevicesEnrollmentPolicy, getting group information") | Select-Object Id, displayName
            }
            
            foreach ($EnrollmentPolicy in $EnrollmentPolicies) {
                $Status = "Not Supported"
                #This is the default enrollment policy that is applied to all users/devices
                if ($EnrollmentPolicy."@odata.type" -eq "#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration") {
                    if (!($EnrollmentPolicy.androidRestriction.platformBlocked) -and !($EnrollmentPolicy.androidRestriction.personalDeviceEnrollmentBlocked)) {
                        $Status = "Supported"
                    }
                    $SettingPSObj = [PSCustomObject]@{
                        PID                             = 9999
                        PolicyName                      = $EnrollmentPolicy.displayName
                        PolicyPriority                  = "Default"
                        PlatformType                    = "Android device administrator"
                        AssignedToGroup                 = "All devices"
                        TeamsDevicesStatus              = $Status
                        PlatformBlocked                 = $EnrollmentPolicy.androidRestriction.platformBlocked
                        PersonalDeviceEnrollmentBlocked = $EnrollmentPolicy.androidRestriction.personalDeviceEnrollmentBlocked
                        osMinimumVersion                = $EnrollmentPolicy.androidRestriction.osMinimumVersion
                        osMaximumVersion                = $EnrollmentPolicy.androidRestriction.osMaximumVersion
                        blockedManufacturers            = $EnrollmentPolicy.androidRestriction.blockedManufacturers
                        TokenExpirationDate             = ""
                    }
                    $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceEnrollmentPolicy')
                    [void]$output.Add($SettingPSObj)
                }

                $Status = "Not Supported"
                if (($EnrollmentPolicy."@odata.type" -eq "#microsoft.graph.deviceEnrollmentPlatformRestrictionConfiguration") -and ($EnrollmentPolicy.platformType -eq "android") ) {
                    $AssignedToGroup = [System.Collections.ArrayList]::new()
                    $AssignedGroupsTemp = ($PolicyGroupAssigment | Where-Object -Property "id" -Value $EnrollmentPolicy.id -EQ).body.value.target.GroupID

                    foreach ($AssignedGroup in $AssignedGroupsTemp) {
                        if (!($UserUPN) -or ($AssignedGroup -in $UserGroups)) {
                            $GroupDisplayName = ($Groups | Where-Object -Property "id" -Value $AssignedGroup -EQ).DisplayName
                            $GroupEntry = New-Object -TypeName PSObject -Property @{
                                GroupID          = $AssignedGroup
                                GroupDisplayName = $GroupDisplayName
                            }
                            [void]$AssignedToGroup.Add($GroupEntry)
                        }
                    }
                    if ($AssignedToGroup.Count -gt 0) {
                        $outAssignedToGroup = "None"
                        if ($AssignedToGroup.count -eq 1) {
                            $outAssignedToGroup = $AssignedToGroup[0].GroupDisplayName
                        }
                        elseif ($AssignedToGroup.count -gt 1) {
                            $outAssignedToGroup = "" + $AssignedToGroup.count + " group(s)"
                        }
                        if (!($EnrollmentPolicy.platformRestriction.platformBlocked) -and !($EnrollmentPolicy.platformRestriction.personalDeviceEnrollmentBlocked)) {
                            $Status = "Supported"
                        }

                        $SettingPSObj = [PSCustomObject]@{
                            PID                             = $EnrollmentPolicy.priority
                            PolicyName                      = $EnrollmentPolicy.displayName
                            PolicyPriority                  = $EnrollmentPolicy.priority
                            PlatformType                    = "Android device administrator"
                            AssignedToGroup                 = $outAssignedToGroup
                            AssignedToGroupList             = $AssignedToGroup
                            TeamsDevicesStatus              = $Status 
                            PlatformBlocked                 = $EnrollmentPolicy.platformRestriction.platformBlocked
                            PersonalDeviceEnrollmentBlocked = $EnrollmentPolicy.platformRestriction.personalDeviceEnrollmentBlocked
                            osMinimumVersion                = $EnrollmentPolicy.platformRestriction.osMinimumVersion
                            osMaximumVersion                = $EnrollmentPolicy.platformRestriction.osMaximumVersion
                            blockedManufacturers            = $EnrollmentPolicy.platformRestriction.blockedManufacturers
                            TokenExpirationDate             = ""
                        }
                        $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceEnrollmentPolicy')
                        [void]$output.Add($SettingPSObj)
                    }
                }
            }

            #region 20240912 - Support for Android AOSP Enrollment
            #AOSP Enrollment Profiles are not assigned to users.
            if (!$UserUPN) {
                $PolicyName = ""
                $TokenExpirationDate = ""
                $CurrentDate = Get-Date
                if ($AOSPEnrollmentProfiles.Length -ge 1) {
                    #In some cases we can have multiple AOSP profiles enabled for Teams Devices, currently we only support one valid at a time.
                    $ValidAOSPEnrollmentProfiles = $AOSPEnrollmentProfiles | Where-Object { $_.tokenExpirationDateTime -gt $CurrentDate }
                    if ($ValidAOSPEnrollmentProfiles.Length -eq 1) {
                        $PolicyName = $ValidAOSPEnrollmentProfiles.displayName
                        $TokenExpirationDate = $ValidAOSPEnrollmentProfiles.tokenExpirationDateTime
                        $TeamsDevicesStatus = "Supported - Token valid for " + ($TokenExpirationDate - $CurrentDate).days + " day(s)"
                    }
                    elseif ($ValidAOSPEnrollmentProfiles.Length -eq 0) {
                        $ExpiredAOSPEnrollmentProfiles = $AOSPEnrollmentProfiles | Sort-Object -Property tokenExpirationDateTime -Descending
                        $TeamsDevicesStatus = "Not Supported - Token Expired on " + ($ExpiredAOSPEnrollmentProfiles[0].tokenExpirationDateTime.ToString([cultureinfo]::CurrentCulture.DateTimeFormat.ShortDatePattern))
                    }
                    else {
                        $TeamsDevicesStatus = "Not Supported - Multiple AOSP Enrollment Profile enabled for Teams Devices"
                    }
                }
                else {
                    $TeamsDevicesStatus = "Not Supported - Missing AOSP Enrollment"
                }
                $SettingPSObj = [PSCustomObject]@{
                    PID                             = "10000"
                    PolicyName                      = $PolicyName
                    PolicyPriority                  = "ASOP"
                    PlatformType                    = "Android Open Source Project (AOSP)"
                    TeamsDevicesStatus              = $TeamsDevicesStatus
                    PlatformBlocked                 = ""
                    PersonalDeviceEnrollmentBlocked = ""
                    osMinimumVersion                = ""
                    osMaximumVersion                = ""
                    blockedManufacturers            = ""
                    TokenExpirationDate             = $TokenExpirationDate
                }
                $SettingPSObj.PSObject.TypeNames.Insert(0, 'TeamsDeviceEnrollmentPolicy')
                [void]$output.Add($SettingPSObj)
            }
            #endregion
            if ($Detailed) {
                if ($ExportCSV) {
                    $output | Sort-Object PID | Select-Object PolicyName, PolicyPriority, PlatformType, AssignedToGroup, TeamsDevicesStatus, PlatformBlocked, PersonalDeviceEnrollmentBlocked, osMinimumVersion, osMaximumVersion, blockedManufacturers | Export-Csv -path $OutputFullPath -NoTypeInformation
                    Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan
                    return
                }
                #20231116 - Fix for empty output.
                else {
                    $output | Sort-Object PID | Format-List
                }
            }
            else {
                $output | Sort-Object PID | Format-Table
            }
        }
    }
}
#EndRegion '.\Public\Test-UcTeamsDevicesEnrollmentPolicy.ps1' 249
#Region '.\Public\Test-UcTeamsOnlyDNSRequirements.ps1' -1

function Test-UcTeamsOnlyDNSRequirements {
    param(
        [Parameter(Mandatory = $false)]
        [string]$Domain,
        [switch]$All
    )    
    <#
        .SYNOPSIS
        Check if the DNS records are OK for a TeamsOnly Tenant.
 
        .DESCRIPTION
        This function will check if the DNS entries that were previously required.
 
        .PARAMETER Domain
        Specifies a domain registered with Microsoft 365
 
        .EXAMPLE
        PS> Test-UcTeamsOnlyDNSRequirements
 
        .EXAMPLE
        PS> Test-UcTeamsOnlyDNSRequirements -Domain uclobby.com
    #>

    
    $outDNSRecords = [System.Collections.ArrayList]::new()
    if ($Domain) {
        $365Domains = Get-UcM365Domains -Domain $Domain | Where-Object { $_.Name -notlike "*.onmicrosoft.com" }
    }
    else {
        try {
            Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null
            #We only need to validate the Enable domains and exclude *.onmicrosoft.com
            $365Domains = Get-CsOnlineSipDomain | Where-Object { $_.Status -eq "Enabled" -and $_.Name -notlike "*.onmicrosoft.com" }
        }
        catch {
            Write-Host "Error: Please connect to Microsoft Teams PowerShell before running this cmdlet with Connect-MicrosoftTeams" -ForegroundColor Red
            return
        }
    }
    $DomainCount = ($365Domains.Count)
    $i = 1
    foreach ($365Domain in $365Domains) {
        $tmpDomain = $365Domain.Name
        Write-Progress -Activity "Teams Only DNS Requirements" -Status "Checking: $tmpDomain - $i of $DomainCount"
        $i++
        $DiscoverFQDN = "lyncdiscover." + $365Domain.Name
        $SIPFQDN = "sip." + $365Domain.Name
        $SIPTLSFQDN = "_sip._tls." + $365Domain.Name
        $FederationFQDN = "_sipfederationtls._tcp." + $365Domain.Name
        $DNSResultDiscover = (Resolve-DnsName $DiscoverFQDN -Type CNAME -ErrorAction Ignore).NameHost
        $DNSResultSIP = (Resolve-DnsName $SIPFQDN -Type CNAME -ErrorAction Ignore).NameHost
        $DNSResultSIPTLS = (Resolve-DnsName $SIPTLSFQDN -Type SRV -ErrorAction Ignore).NameTarget
        $DNSResultFederation = (Resolve-DnsName $FederationFQDN -Type SRV -ErrorAction Ignore)

        $DNSDiscover = ""
        if ($DNSResultDiscover -and ($All -or $DNSResultDiscover.contains("online.lync.com") -or $DNSResultDiscover.contains("online.gov.skypeforbusiness.us"))) {
            $DNSDiscover = $DNSResultDiscover
        }

        $DNSSIP = ""
        if ($DNSResultSIP -and ($All -or $DNSResultSIP.contains("online.lync.com") -or $DNSResultSIP.contains("online.gov.skypeforbusiness.us"))) {
            $DNSSIP = $DNSResultSIP
        }
        
        $DNSSIPTLS = ""
        if ($DNSResultSIPTLS -and ($All -or $DNSResultSIPTLS.contains("online.lync.com") -or $DNSResultSIPTLS.contains("online.gov.skypeforbusiness.us"))) {
            $DNSSIPTLS = $DNSResultSIPTLS
        }

        $DNSFederation = ""
        if ([string]::IsNullOrEmpty($DNSResultFederation.NameTarget)) {
            $DNSFederation = "Not configured"
        }
        elseif (($DNSResultFederation.NameTarget.equals("sipfed.online.lync.com") -or $DNSResultFederation.NameTarget.equals("sipfed.online.gov.skypeforbusiness.us")) -and $DNSResultFederation.Port -eq 5061) {
            $DNSFederation = "OK"
        }
        else {
            $DNSFederation = "NOK - " + $DNSResultFederation.NameTarget + ":" + $DNSResultFederation.Port 
        }

        if ($DNSDiscover -or $DNSSIP -or $DNSSIPTLS -or $DNSFederation) {
            $tmpDNSRecord = New-Object -TypeName PSObject -Property @{
                Domain           = $365Domain.Name
                DiscoverRecord   = $DNSDiscover
                SIPRecord        = $DNSSIP
                SIPTLSRecord     = $DNSSIPTLS
                FederationRecord = $DNSFederation
            }
            $tmpDNSRecord.PSObject.TypeNames.Insert(0, 'TeamsOnlyDNSRequirements')
            [void]$outDNSRecords.Add($tmpDNSRecord)
        }
    }
    return $outDNSRecords | Sort-Object Domain
}
#EndRegion '.\Public\Test-UcTeamsOnlyDNSRequirements.ps1' 94
#Region '.\Public\Update-UcTeamsDevice.ps1' -1

function Update-UcTeamsDevice {
    [cmdletbinding(SupportsShouldProcess)]
    param(
        [ValidateSet("Firmware", "TeamsClient", "All")]
        [string]$UpdateType = "All",
        [ValidateSet("Phone", "MTRA", "Display", "Panel")]
        [string]$DeviceType,
        [string]$DeviceID,
        [string]$SoftwareVersion,
        [string]$InputCSV,
        [string]$Subnet,
        [string]$OutputPath,
        [switch]$ReportOnly
    )
    <#
        .SYNOPSIS
        Update Microsoft Teams Devices
 
        .DESCRIPTION
        This function will send update commands to Teams Android Devices using MS Graph.
 
        Contributors: Eileen Beato, David Paulino and Bryan Kendrick
 
        Requirements: Microsoft Graph PowerShell Module (Install-Module Microsoft.Graph)
                        Microsoft Graph Scopes:
                            "TeamworkDevice.ReadWrite.All","User.Read.All"
 
        .PARAMETER DeviceID
        Specify the Teams Admin Center Device ID that we want to update.
 
        .PARAMETER DeviceType
        Specifies a filter, valid options:
            Phone - Teams Native Phones
            MTRA - Microsoft Teams Room Running Android
            Display - Microsoft Teams Displays
            Panel - Microsoft Teams Panels
 
        .PARAMETER UpdateType
        Allow to specify which time of update we want to do:
            Firmware
            TeamsClient
            All
 
        .PARAMETER SoftwareVersion
        Allow to specify which version we want to update.
 
        .PARAMETER InputCSV
        When present will use this file as Input, we only need a column with Device Id. It supports files exported from Teams Admin Center (TAC).
 
        .PARAMETER Subnet
        Only available when using InputCSV and requires a “IP Address” column, it allows to only send updates to Teams Android devices within a subnet.
        Format examples:
            10.0.0.0/8
            192.168.0.0/24
 
        .PARAMETER OutputPath
        Allows to specify the path where we want to save the results. By default will save on current user Download.
 
        .PARAMETER ReportOnly
        Will read Teams Device Android versions info and generate a report
 
        .EXAMPLE
        PS> Update-UcTeamsDevice
 
        .EXAMPLE
        PS> Update-UcTeamsDevice -ReportOnly
 
        .EXAMPLE
        PS> Update-UcTeamsDevice -InputCSV C:\Temp\DevicesList_2023-04-20_15-19-00-UTC.csv
    #>


    $regExIPAddressSubnet = "^((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9]))\/(3[0-2]|[1-2]{1}[0-9]{1}|[1-9])$"
    #20241023: If report Only then we dont need write permission on TeamworkDevice
    $GraphConnected = $false
    if($ReportOnly){
        $GraphConnected = Test-UcMgGraphConnection -Scopes "TeamworkDevice.Read.All","User.Read.All" -AltScopes "TeamworkDevice.Read.All","User.ReadBasic.All"
    } else {
        $GraphConnected = Test-UcMgGraphConnection -Scopes "TeamworkDevice.ReadWrite.All","User.Read.All" -AltScopes "TeamworkDevice.ReadWrite.All","User.ReadBasic.All"
    }

    if ($GraphConnected) {
        $outTeamsDevices = [System.Collections.ArrayList]::new()
        Test-UcPowerShellModule -ModuleName UcLobbyTeams | Out-Null
        #Checking if the Subnet is valid
        if ($Subnet) {
            if (!($Subnet -match $regExIPAddressSubnet)) {
                Write-Host ("Error: Subnet " + $Subnet + " is invalid, please make sure the subnet is valid and in this format 10.0.0.0/8, 192.168.0.0/24") -ForegroundColor Red
                return
            } 
        }

        if ($ReportOnly) {
            $outFileName = "UpdateTeamsDevices_ReportOnly_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv"
            $StatusType = "offline", "critical", "nonUrgent", "healthy"
        }
        else {
            $outFileName = "UpdateTeamsDevices_" + ( get-date ).ToString('yyyyMMdd-HHmmss') + ".csv"
            $StatusType = "critical", "nonUrgent"
        }
        #Verify if the Output Path exists
        if ($OutputPath) {
            if (!(Test-Path $OutputPath -PathType Container)) {
                Write-Host ("Error: Invalid folder " + $OutputPath) -ForegroundColor Red
                return
            }
            else {
                $OutputFullPath = [System.IO.Path]::Combine($OutputPath, $outFileName)
            }
        }
        else {                
            $OutputFullPath = [System.IO.Path]::Combine($env:USERPROFILE, "Downloads", $outFileName)
        }

        $graphRequests = [System.Collections.ArrayList]::new()
        if ($DeviceID) {
            $gRequestTmp = New-Object -TypeName PSObject -Property @{
                id     = $DeviceID
                method = "GET"
                url    = "/teamwork/devices/" + $DeviceID
            }
            [void]$graphRequests.Add($gRequestTmp)
            $GraphResponse = Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Update-UcTeamsDevices, getting device info" -IncludeBody
            
            if ($GraphResponse.status -eq 200) {
                $TeamsDeviceList = $GraphResponse.body
            }
            elseif ($GraphResponse.status -eq 404) {
                Write-Host ("Error: Device ID $DeviceID not found.") -ForegroundColor Red
                return
            }
        }
        elseif ($InputCSV) {
            if (Test-Path $InputCSV) {
                try {
                    $TeamsDeviceInput = Import-Csv -Path $InputCSV
                }
                catch {
                    Write-Host ("Invalid CSV input file: " + $InputCSV) -ForegroundColor Red
                    return
                }
                foreach ($TeamsDevice in $TeamsDeviceInput) {
                    $includeDevice = $true
                    if ($Subnet) {
                        $includeDevice = Test-UcIPaddressInSubnet -IPAddress $TeamsDevice.'IP Address' -Subnet $Subnet
                    }

                    if ($includeDevice) {
                        $gRequestTmp = New-Object -TypeName PSObject -Property @{
                            id     = $TeamsDevice.'Device Id'
                            method = "GET"
                            url    = "/teamwork/devices/" + $TeamsDevice.'Device Id'
                        }
                        [void]$graphRequests.Add($gRequestTmp)
                    }
                }
                if ($graphRequests.Count -gt 0) {
                    $TeamsDeviceList = (Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Update-UcTeamsDevices, getting device info" )
                } 
            }
            else {
                Write-Host ("Error: File not found " + $InputCSV) -ForegroundColor Red
                return
            }
        }
        else {
            #Currently only Android based Teams devices are supported.
            switch ($DeviceType) {
                "Phone" { 
                    $gRequestTmp = New-Object -TypeName PSObject -Property @{
                        id     = "ipPhone"
                        method = "GET"
                        url    = "/teamwork/devices/?`$filter=deviceType eq 'ipPhone'"
                    }
                    [void]$graphRequests.Add($gRequestTmp)
                    $gRequestTmp = New-Object -TypeName PSObject -Property @{
                        id     = "lowCostPhone"
                        method = "GET"
                        url    = "/teamwork/devices/?`$filter=deviceType eq 'lowCostPhone'"
                    }
                    [void]$graphRequests.Add($gRequestTmp)
                }
                "MTRA" {            
                    $gRequestTmp = New-Object -TypeName PSObject -Property @{
                        id     = "collaborationBar"
                        method = "GET"
                        url    = "/teamwork/devices/?`$filter=deviceType eq 'collaborationBar'"
                    }
                    [void]$graphRequests.Add($gRequestTmp)
                    $gRequestTmp = New-Object -TypeName PSObject -Property @{
                        id     = "touchConsole"
                        method = "GET"
                        url    = "/teamwork/devices/?`$filter=deviceType eq 'touchConsole'"
                    }
                    [void]$graphRequests.Add($gRequestTmp)
                }
                "Display" {
                    $gRequestTmp = New-Object -TypeName PSObject -Property @{
                        id     = "teamsDisplay"
                        method = "GET"
                        url    = "/teamwork/devices/?`$filter=deviceType eq 'teamsDisplay'"
                    }
                    [void]$graphRequests.Add($gRequestTmp)
                }
                "Panel" {
                    $gRequestTmp = New-Object -TypeName PSObject -Property @{
                        id     = "teamsPanel"
                        method = "GET"
                        url    = "/teamwork/devices/?`$filter=deviceType eq 'teamsPanel'"
                    }
                    [void]$graphRequests.Add($gRequestTmp)
                }
                Default {
                    #This is the only way to exclude MTRW and SurfaceHub by creating a request per device type.
                    $gRequestTmp = New-Object -TypeName PSObject -Property @{
                        id     = "ipPhone"
                        method = "GET"
                        url    = "/teamwork/devices/?`$filter=deviceType eq 'ipPhone'"
                    }
                    [void]$graphRequests.Add($gRequestTmp) 
                    $gRequestTmp = New-Object -TypeName PSObject -Property @{
                        id     = "lowCostPhone"
                        method = "GET"
                        url    = "/teamwork/devices/?`$filter=deviceType eq 'lowCostPhone'"
                    }
                    [void]$graphRequests.Add($gRequestTmp)
                    $gRequestTmp = New-Object -TypeName PSObject -Property @{
                        id     = "collaborationBar"
                        method = "GET"
                        url    = "/teamwork/devices/?`$filter=deviceType eq 'collaborationBar'"
                    }
                    [void]$graphRequests.Add($gRequestTmp)
                    $gRequestTmp = New-Object -TypeName PSObject -Property @{
                        id     = "touchConsole"
                        method = "GET"
                        url    = "/teamwork/devices/?`$filter=deviceType eq 'touchConsole'"
                    }
                    [void]$graphRequests.Add($gRequestTmp) 
                    $gRequestTmp = New-Object -TypeName PSObject -Property @{
                        id     = "teamsDisplay"
                        method = "GET"
                        url    = "/teamwork/devices/?`$filter=deviceType eq 'teamsDisplay'"
                    }
                    [void]$graphRequests.Add($gRequestTmp)
                    $gRequestTmp = New-Object -TypeName PSObject -Property @{
                        id     = "teamsPanel"
                        method = "GET"
                        url    = "/teamwork/devices/?`$filter=deviceType eq 'teamsPanel'"
                    }
                    [void]$graphRequests.Add($gRequestTmp)
                }
            }
            #Using new cmdlet to get a list of devices
            $TeamsDeviceList = (Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Update-UcTeamsDevices, getting device info").value
        }

        $devicesWithUpdatePending = 0
        $graphRequests = [System.Collections.ArrayList]::new()
        foreach ($TeamsDevice in $TeamsDeviceList) {

            if (($graphRequests.id -notcontains $TeamsDevice.currentuser.id) -and !([string]::IsNullOrEmpty($TeamsDevice.currentuser.id))) {
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = $TeamsDevice.currentuser.id
                    method = "GET"
                    url    = "/users/" + $TeamsDevice.currentuser.id
                }
                [void]$graphRequests.Add($gRequestTmp)
            }

            if ($TeamsDevice.healthStatus -in $StatusType -or $SoftwareVersion) {
                $devicesWithUpdatePending++
                $gRequestTmp = New-Object -TypeName PSObject -Property @{
                    id     = $TeamsDevice.id + "-health"
                    method = "GET"
                    url    = "/teamwork/devices/" + $TeamsDevice.id + "/health"
                }
                [void]$graphRequests.Add($gRequestTmp)
            }

            $gRequestTmp = New-Object -TypeName PSObject -Property @{
                id     = $TeamsDevice.id + "-operations"
                method = "GET"
                url    = "/teamwork/devices/" + $TeamsDevice.id + "/operations"
            }
            [void]$graphRequests.Add($gRequestTmp) 

        }
        if ($graphRequests.Count -gt 0) {
            $graphResponseExtra = Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Update-UcTeamsDevices, getting device health info" -IncludeBody
        }

        #In case we detect more than 5 devices with updates pending we will request confirmation that we can continue.
        if (($devicesWithUpdatePending -ge 5) -and !$ReportOnly) {
            if ($ConfirmPreference) {
                $title = 'Confirm'
                $question = "There are " + $devicesWithUpdatePending + " Teams Devices pending update. Are you sure that you want to continue?"
                $choices = '&Yes', '&No'
                $decision = $Host.UI.PromptForChoice($title, $question, $choices, 1)
            }
            else {
                $decision = 0
            }
            if ($decision -ne 0) {
                return
            }
        }
        $graphRequests = [System.Collections.ArrayList]::new()
        foreach ($TeamsDevice in $TeamsDeviceList) {
            if ($TeamsDevice.healthStatus -in $StatusType -or $SoftwareVersion) {
                $TeamsDeviceHealth = ($graphResponseExtra | Where-Object { $_.id -eq ($TeamsDevice.id + "-health") }).body
                
                #Valid types are: adminAgent, operatingSystem, teamsClient, firmware, partnerAgent, companyPortal.
                #Currently we only consider Firmware and TeamsApp(teamsClient)
                
                #Firmware
                if (($TeamsDeviceHealth.softwareUpdateHealth.firmwareSoftwareUpdateStatus.softwareFreshness.Equals("updateAvailable") -or $SoftwareVersion) -and ($UpdateType -in ("All", "Firmware"))) {
                    if (!($ReportOnly)) {

                        $requestHeader = New-Object 'System.Collections.Generic.Dictionary[string, string]'
                        $requestHeader.Add("Content-Type", "application/json")
                        $requestBody = New-Object 'System.Collections.Generic.Dictionary[string, string]'
                        $requestBody.Add("softwareType", "firmware")
                        
                        if ($SoftwareVersion) {
                            $requestBody.Add("softwareVersion", $SoftwareVersion)
                        }
                        else {
                            $requestBody.Add("softwareVersion", $TeamsDeviceHealth.softwareUpdateHealth.firmwareSoftwareUpdateStatus.availableVersion)
                        }

                        $gRequestTmp = New-Object -TypeName PSObject -Property @{
                            id      = $TeamsDevice.id + "-updateFirmware"
                            method  = "POST"
                            url     = "/teamwork/devices/" + $TeamsDevice.id + "/updateSoftware"
                            body    = $requestBody
                            headers = $requestHeader
                        }
                        [void]$graphRequests.Add($gRequestTmp)
                    }
                }
                
                #TeamsApp
                if (($TeamsDeviceHealth.softwareUpdateHealth.teamsClientSoftwareUpdateStatus.softwareFreshness.Equals("updateAvailable") -or $SoftwareVersion) -and ($UpdateType -in ("All", "TeamsClient"))) {
                    if (!($ReportOnly)) {
                        $requestHeader = New-Object 'System.Collections.Generic.Dictionary[string, string]'
                        $requestHeader.Add("Content-Type", "application/json")

                        $requestBody = New-Object 'System.Collections.Generic.Dictionary[string, string]'
                        $requestBody.Add("softwareType", "teamsClient")
                        if ($SoftwareVersion) {
                            $requestBody.Add("softwareVersion", $SoftwareVersion)
                        }
                        else {
                            $requestBody.Add("softwareVersion", $TeamsDeviceHealth.softwareUpdateHealth.teamsClientSoftwareUpdateStatus.availableVersion)
                        }
                        $gRequestTmp = New-Object -TypeName PSObject -Property @{
                            id      = $TeamsDevice.id + "-updateTeamsClient"
                            method  = "POST"
                            url     = "/teamwork/devices/" + $TeamsDevice.id + "/updateSoftware"
                            body    = $requestBody
                            headers = $requestHeader
                        }
                        [void]$graphRequests.Add($gRequestTmp)
                    }
                }
            }
        }
        if ($graphRequests.Count -gt 0) {
            $updateGraphResponse = Invoke-UcMgGraphBatch -Requests $graphRequests -MgProfile beta -Activity "Update-UcTeamsDevices, sending update commands" -IncludeBody
        }
        foreach ($TeamsDevice in $TeamsDeviceList) {
            if ($TeamsDevice.healthStatus -in $StatusType -or $SoftwareVersion) {
                $TeamsDeviceHealth = ($graphResponseExtra | Where-Object { $_.id -eq ($TeamsDevice.id + "-health") }).body
                if ($ReportOnly) {
                    $UpdateStatus = "Report Only:"
                    $pendingUpdate = $false
                    if ($TeamsDeviceHealth.softwareUpdateHealth.firmwareSoftwareUpdateStatus.softwareFreshness.Equals("updateAvailable") -and ($UpdateType -in ("All", "Firmware"))) {
                        $UpdateStatus += " Firmware Update Pending;"
                        $pendingUpdate = $true
                    }
                    if ($TeamsDeviceHealth.softwareUpdateHealth.teamsClientSoftwareUpdateStatus.softwareFreshness.Equals("updateAvailable") -and ($UpdateType -in ("All", "TeamsClient"))) {
                        $UpdateStatus += " Teams App Update Pending;"
                        $pendingUpdate = $true
                    }
                    if (!$pendingUpdate) {
                        $UpdateStatus = "Report Only: No firmware or Teams App updates pending."
                    }
                }
                else {
                    $tmpUpdateStatus = ($updateGraphResponse | Where-Object { $_.id -eq ($TeamsDevice.id + "-updateFirmware") })
                    $tmpUpdateRequest = $graphRequests | Where-Object { $_.id -eq ($TeamsDevice.id + "-updateFirmware") }
                    if ($tmpUpdateStatus.status -eq 202) {
                        $UpdateStatus = "Firmware update request to version " + $tmpUpdateRequest.body.softwareVersion + " was queued."
                    }
                    elseif ($tmpUpdateStatus.status -eq 404) {
                        $UpdateStatus = "Unknown/Invalid firmware version: " + $tmpUpdateRequest.body.softwareVersion
                    }
                    elseif ($tmpUpdateStatus.status -eq 409) {
                        $UpdateStatus = "There is a firmware update pending, please check the update status."
                    }
                    $tmpUpdateStatus = ($updateGraphResponse | Where-Object { $_.id -eq ($TeamsDevice.id + "-updateTeamsClient") })
                    $tmpUpdateRequest = $graphRequests | Where-Object { $_.id -eq ($TeamsDevice.id + "-updateTeamsClient") }
                    if ($tmpUpdateStatus.status -eq 202) {
                        $UpdateStatus = "Teams App update request to version " + $tmpUpdateRequest.body.softwareVersion + " was queued."
                    }
                    elseif ($tmpUpdateStatus.status -eq 404) {
                        $UpdateStatus = "Unknown/Invalid Teams App version: " + $tmpUpdateRequest.body.softwareVersion
                    }
                    elseif ($tmpUpdateStatus.status -eq 409) {
                        $UpdateStatus = "There is a Teams App update pending, please check the update status."
                    }
                }
                $userUPN = ($graphResponseExtra | Where-Object { $_.id -eq $TeamsDevice.currentuser.id }).body.userPrincipalName

                $TeamsDeviceOperations = ($graphResponseExtra | Where-Object { $_.id -eq ($TeamsDevice.id + "-operations") }).body.value

                $LastUpdateStatus = ""
                $LastUpdateInitiatedBy = ""
                $LastUpdateModifiedDate = ""

                #In this case we only need the last time we tried to udpdate.
                foreach ($TeamsDeviceOperation in $TeamsDeviceOperations) {
                    if ($TeamsDeviceOperation.operationType -eq 'softwareUpdate') {
                        $LastUpdateStatus = $TeamsDeviceOperation.status
                        $LastUpdateInitiatedBy = $TeamsDeviceOperation.createdBy.user.displayName
                        $LastUpdateModifiedDate = $TeamsDeviceOperation.lastActionDateTime
                        break;
                    }
                }

                $TDObj = New-Object -TypeName PSObject -Property @{
                    TACDeviceID                     = $TeamsDevice.id
                    UserDisplayName                 = $TeamsDevice.currentUser.displayName
                    UserUPN                         = $userUPN
                    DeviceType                      = Convert-UcTeamsDeviceType $TeamsDevice.deviceType
                    Manufacturer                    = $TeamsDevice.hardwaredetail.manufacturer
                    Model                           = $TeamsDevice.hardwaredetail.model
                    HealthStatus                    = $TeamsDevice.healthStatus
                    TeamsAdminAgentCurrentVersion   = $TeamsDeviceHealth.softwareUpdateHealth.adminAgentSoftwareUpdateStatus.currentVersion
                    TeamsAdminAgentAvailableVersion = $TeamsDeviceHealth.softwareUpdateHealth.adminAgentSoftwareUpdateStatus.availableVersion
                    FirmwareCurrentVersion          = $TeamsDeviceHealth.softwareUpdateHealth.firmwareSoftwareUpdateStatus.currentVersion
                    FirmwareAvailableVersion        = $TeamsDeviceHealth.softwareUpdateHealth.firmwareSoftwareUpdateStatus.availableVersion
                    CompanyPortalCurrentVersion     = $TeamsDeviceHealth.softwareUpdateHealth.companyPortalSoftwareUpdateStatus.currentVersion
                    CompanyPortalAvailableVersion   = $TeamsDeviceHealth.softwareUpdateHealth.companyPortalSoftwareUpdateStatus.availableVersion
                    OEMAgentAppCurrentVersion       = $TeamsDeviceHealth.softwareUpdateHealth.partnerAgentSoftwareUpdateStatus.currentVersion
                    OEMAgentAppAvailableVersion     = $TeamsDeviceHealth.softwareUpdateHealth.partnerAgentSoftwareUpdateStatus.availableVersion
                    TeamsAppCurrentVersion          = $TeamsDeviceHealth.softwareUpdateHealth.teamsClientSoftwareUpdateStatus.currentVersion
                    TeamsAppAvailableVersion        = $TeamsDeviceHealth.softwareUpdateHealth.teamsClientSoftwareUpdateStatus.availableVersion
                    UpdateStatus                    = $UpdateStatus
                    
                    #PreviousUpdate
                    PreviousUpdateStatus            = $LastUpdateStatus
                    PreviousUpdateInitiatedBy       = $LastUpdateInitiatedBy
                    PreviousUpdateModifiedDate      = $LastUpdateModifiedDate
                }
                $TDObj.PSObject.TypeNames.Insert(0, 'UpdateTeamsDevice')
                [void]$outTeamsDevices.Add($TDObj)
            }
        }

        if ( $outTeamsDevices.Count -eq 1) {
            return $outTeamsDevices
        }
        elseif ( $outTeamsDevices.Count -gt 1) {
            $outTeamsDevices | Sort-Object DeviceType, Manufacturer, Model | Select-Object TACDeviceID, UserDisplayName, UserUPN, DeviceType, Manufacturer, Model, HealthStatus, TeamsAdminAgentCurrentVersion, TeamsAdminAgentAvailableVersion, FirmwareCurrentVersion, FirmwareAvailableVersion, CompanyPortalCurrentVersion, CompanyPortalAvailableVersion, OEMAgentAppCurrentVersion, OEMAgentAppAvailableVersion, TeamsAppCurrentVersion, TeamsAppAvailableVersion, PreviousUpdateStatus, PreviousUpdateInitiatedBy, PreviousUpdateModifiedDate, UpdateStatus | Export-Csv -path $OutputFullPath -NoTypeInformation
            Write-Host ("Results available in: " + $OutputFullPath) -ForegroundColor Cyan
        }
        else {
            Write-Host ("No Teams Device(s) found that have pending update.") -ForegroundColor Cyan
        }
    }
}
#EndRegion '.\Public\Update-UcTeamsDevice.ps1' 472