├── LICENSE ├── IntuneDriverUpdatePerModelSnippets.ps1 ├── Set-ConfigureChatAutoInstall.ps1 └── Install-MSTeams.ps1 /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sassan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /IntuneDriverUpdatePerModelSnippets.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Graph snippets for creating AAD dynamic groups, Driver update profiles and assignments for every unique device model in Intune. 3 | Required modules: Microsoft.Graph.Authentication, Microsoft.Graph.DeviceManagement, Microsoft.Graph.Groups and 4 | Microsoft.Graph.DeviceManagement.Actions (PowerShell Graph SDK v1) or Microsoft.Graph.Beta.DeviceManagement.Actions (PowerShell Graph SDK v2) 5 | 6 | Updated: 2023-07-16 7 | Author: Sassan Fanai @ Onevinn.se 8 | 9 | Version 0.0.0.2 - Works with PowerShell Graph SDK v2 (default). Set $GraphVersion to v1 or v2 depending on 10 | the version you are using. 11 | Stole some code from @jarwidmark to normalize Manufacturer/Make. 12 | Version 0.0.0.3 - Updated/fixed membership rule to use $Device.Manufacturer insted of normalized $Make. Thanks to BaconActual. 13 | #> 14 | 15 | # Set version of PowerShell Graph SDK that will be used, v1 or v2 16 | $GraphVersion = "v2" 17 | 18 | # Set naming scheme (prefix and suffix) for Groups and Driver Update profiles name. Can be omitted by setting them to "". 19 | # Example result: DriverUpdate - LENOVO ThinkPad L13 Gen 3 - Pilot 20 | $NamePrefix = "GraphV2 - " 21 | $NameSuffix = " - Test2" 22 | 23 | # Set Models to exclude creating groups and profiles for 24 | $ExcludedModels = @("Virtual Machine") 25 | 26 | # Set Approval type and number of deferral days in Driver Update profiles 27 | $approvalType = "manual" # manual or automatic 28 | $deploymentDeferralInDays = 7 # only used if approvalType is set to automatic 29 | 30 | # Lenovo convert MTM to friendly name, thanks to Damien Van Robaeys @ https://www.systanddeploy.com/2023/01/get-list-uptodate-of-all-lenovo-models.html 31 | function Get-LenovoFriendlyName { 32 | param ( 33 | $MTM 34 | ) 35 | $URL = "https://download.lenovo.com/bsco/public/allModels.json" 36 | $Get_Web_Content = Invoke-RestMethod -Uri $URL -Method GET 37 | $Current_Model = ($Get_Web_Content | where-object {($_ -like "*$MTM*") -and ($_ -notlike "*-UEFI Lenovo*") -and ($_ -notlike "*dTPM*") -and ($_ -notlike "*Asset*") -and ($_ -notlike "*fTPM*")})[0] 38 | $Get_FamilyName = ($Current_Model.name.split("("))[0].TrimEnd() 39 | $Get_FamilyName 40 | } 41 | 42 | # Connect to Graph 43 | Connect-MgGraph -Scopes "DeviceManagementManagedDevices.ReadWrite.All", "Group.ReadWrite.All", "DeviceManagementConfiguration.ReadWrite.All" 44 | 45 | if ($GraphVersion -eq "v1") { 46 | Select-MgProfile -Name beta # This cmdlet does not exist and thus not needed uf using Microsoft Graph PowerShell SDK v1 47 | } 48 | #Import-Module Microsoft.Graph.DeviceManagement.Actions 49 | 50 | # Get all unique Models in Intune with Windows as OS 51 | $IntuneDevices = Get-MgDeviceManagementManagedDevice -Filter "OperatingSystem eq 'Windows'" | Sort-Object -Property Model -Unique 52 | #$IntuneDevices | select Manufacturer, Model 53 | 54 | # Create AAD groups for each unique model 55 | foreach ($Device in $IntuneDevices) { 56 | Write-Host "Processing Make and Model: [$($Device.Manufacturer)] and [$($Device.Model)]" -ForegroundColor Cyan 57 | if ($Device.Model -notin $ExcludedModels) { 58 | $Make = $Device.Manufacturer 59 | $Model = $Device.Model 60 | 61 | switch -Wildcard ($Make) { 62 | "*Microsoft*" { 63 | $Make = "Microsoft" 64 | } 65 | "*HP*" { 66 | $Make = "HP" 67 | } 68 | "*Hewlett-Packard*" { 69 | $Make = "HP" 70 | } 71 | "*Dell*" { 72 | $Make = "Dell" 73 | } 74 | "*Lenovo*" { 75 | $Make = "Lenovo" 76 | $Model = Get-LenovoFriendlyName -MTM $Model.Substring(0,4) 77 | Write-Host "Manufacturer is [$Make]. Converting Model name from [$($Device.Model)] to [$Model] (for group/profile names)" -ForegroundColor Cyan 78 | } 79 | } 80 | 81 | $GroupName = "$NamePrefix$($Make) $($Model)$($NameSuffix)" 82 | 83 | if ($Model.StartsWith($Make)) { 84 | Write-Host "Model name [$Model] starts with Manufacturer name [$Make]. Omitting Manufacturer from Group Name" -ForegroundColor Yellow 85 | $GroupName = "$($NamePrefix)$($Model)$($NameSuffix)" 86 | } 87 | 88 | Write-Host "GroupName is [$GroupName]" -ForegroundColor Cyan 89 | $MR = '(device.deviceModel -eq "' + $($Device.Model) + '") and (device.deviceManufacturer -eq "' + $($Device.Manufacturer) + '")' 90 | Write-Host "Dynamic MembershipRule = $MR" -ForegroundColor Magenta 91 | 92 | $ExistingGroup = Get-MgGroup -Filter "DisplayName eq '$GroupName'" 93 | if (!$ExistingGroup) { 94 | Write-Host "Group [$GroupName] does not exist, creating" -ForegroundColor Green 95 | $GroupParam = @{ 96 | DisplayName = "$GroupName" 97 | Description = "Dynamic group for $($Model)" 98 | GroupTypes = @( 99 | 'DynamicMembership' 100 | ) 101 | SecurityEnabled = $true 102 | IsAssignableToRole = $false 103 | MailEnabled = $false 104 | membershipRuleProcessingState = 'On' 105 | MembershipRule = $($MR) 106 | MailNickname = (New-Guid).Guid.Substring(0,10) 107 | "Owners@odata.bind" = @( 108 | "https://graph.microsoft.com/v1.0/me" 109 | ) 110 | } 111 | $GroupResult = New-MgGroup -BodyParameter $GroupParam 112 | } 113 | else { 114 | Write-Host "Group [$GroupName] already exists, skipping" -ForegroundColor Yellow 115 | } 116 | } 117 | else { 118 | Write-Host "$($Device.Model) is in ExcludedList, skipping" -ForegroundColor Yellow 119 | } 120 | } 121 | 122 | # Get all Driver profiles 123 | $AllDriverProfiles = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/beta/deviceManagement/windowsDriverUpdateProfiles").value 124 | 125 | # Get all AAD groups with NamePrefix 126 | $AllDriverGroups = Get-MgGroup -Filter "startsWith(DisplayName,'$NamePrefix')" 127 | 128 | # Create Driver Update Profile for each AAD group 129 | foreach ($DriverGroup in $AllDriverGroups) { 130 | if ($DriverGroup.DisplayName -notin $AllDriverProfiles.displayName){ 131 | Write-Host "No Driver Update profile named [$($DriverGroup.DisplayName)] exists, creating" -ForegroundColor Green 132 | $ProfileBody = @{ 133 | '@odata.type' = "#microsoft.graph.windowsDriverUpdateProfile" 134 | displayName = "$($DriverGroup.DisplayName)" 135 | approvalType = "$approvalType" 136 | roleScopeTagIds = @() 137 | ContentType = "application/json" 138 | } 139 | if ($approvalType -eq "automatic"){ 140 | $ProfileBody.Add("deploymentDeferralInDays",$deploymentDeferralInDays) 141 | } 142 | $DriverProfile = Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/beta/deviceManagement/windowsDriverUpdateProfiles" -Body (ConvertTo-Json $ProfileBody) 143 | } 144 | else { 145 | Write-Host "Driver Update profile named [$($DriverGroup.DisplayName)] already exists, skipping" -ForegroundColor Yellow 146 | $DriverProfile = $AllDriverProfiles | Where-Object {$_.DisplayName -eq $DriverGroup.DisplayName} 147 | } 148 | 149 | # Create assignment for each Driver Update Profile to the AAD group with same name 150 | $AssignedGroups = (Invoke-MgGraphRequest -Method GET -Uri " https://graph.microsoft.com/beta/deviceManagement/windowsDriverUpdateProfiles/$($DriverProfile.Id)/assignments").value.target.groupid 151 | if ($DriverGroup.Id -notin $AssignedGroups){ 152 | Write-Host "Driver Udate Profile [$($DriverProfile.displayname)] is not assigned to AAD group [$($DriverGroup.DisplayName)], creating assignment" -ForegroundColor Green 153 | 154 | $AssignBody = @{ 155 | assignments = @( 156 | @{ 157 | target = @{ 158 | '@odata.type' = "#microsoft.graph.groupAssignmentTarget" 159 | groupId = "$($DriverGroup.Id)" 160 | } 161 | } 162 | ) 163 | } 164 | 165 | #Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/beta/deviceManagement/windowsDriverUpdateProfiles/$($DriverProfile.Id)/assignments" -Body (ConvertTo-Json $AssignBody) -ContentType "application/json" 166 | 167 | # If using Microsoft Graph PowerShell SDK v1 168 | if ($GraphVersion -eq "v1") { 169 | Set-MgDeviceManagementWindowDriverUpdateProfile -WindowsDriverUpdateProfileId $DriverProfile.Id -BodyParameter $AssignBody 170 | } 171 | 172 | # If using Microsoft Graph PowerShell SDK v2 173 | if ($GraphVersion -eq "v2") { 174 | Set-MgBetaDeviceManagementWindowsDriverUpdateProfile -WindowsDriverUpdateProfileId $DriverProfile.Id -BodyParameter $AssignBody 175 | } 176 | } 177 | else { 178 | Write-Host "Driver Udate Profile [$($DriverProfile.displayname)] is already assigned to AAD group [$($DriverGroup.DisplayName)], skipping" -ForegroundColor Yellow 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Set-ConfigureChatAutoInstall.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Sets the registry item ConfigureChatAutoInstall to 0 to disable Teams Consumer installation. 3 | It's the same registry value that is being set when using it in unattend.xml 4 | 5 | The registry key is owned by TrustedInstaller and System does not have permissions to write to it. 6 | This script sets System as owner on the registry key [HKLM:\Software\Microsoft\Windows\CurrentVersion\Communications] 7 | to be able to set ACL permission for System on it. It then sets ConfigureChatAutoInstall to 0 and restores both ACL and owner 8 | permissions. 9 | Script will also try to remove Consumer Teams if it exists. 10 | 11 | Sassan Fanai 12 | Version 1.0.0.1 - 2023-06-02 - Created 13 | Version 1.0.0.2 - 2023-07-05 - Remove MicrosotTeams (consumer) AppxPackage if installed 14 | Version 1.0.0.3 - 2023-09-14 - Move Start-transcript to the top 15 | Version 1.0.0.4 - 2023-11-07 - Added default log location and some more output 16 | #> 17 | 18 | $LogFileName = "BlockConsumerTeams.log" 19 | $LogPath = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\CCM\Logging\@Global" -Name "LogDirectory" 20 | if (!$LogPath) { 21 | $LogPath = $env:TEMP 22 | } 23 | $LogFile = Join-Path -Path $LogPath -ChildPath $LogFileName 24 | 25 | Start-Transcript -Path $LogFile 26 | 27 | 28 | function Set-RegACL { 29 | <# 30 | .SYNOPSIS 31 | Sets ACL permission on registry key. 32 | 33 | .DESCRIPTION 34 | Sets ACL on regsitry key based on specified RegistryRights Enum. 35 | Made primarly for setting ACL on registry keys where only TrustedInstaller 36 | have full control. This function does not take ownership of the registry key, 37 | should be used with Take-Ownership function. 38 | 39 | .PARAMETER Path (Required) 40 | The path to the object on which you wish to change ownership. It can be a file or a folder. 41 | 42 | .PARAMETER User (Required) 43 | The user whom you want to be the owner of the specified object. The user should be in the format 44 | \. Other user formats will not work. For system accounts, such as System, the user 45 | should be specified as "NT AUTHORITY\System". If the domain is missing, the local machine will be assumed. 46 | 47 | .PARAMETER Permission (switch) 48 | What permission to set on registry key. 49 | 50 | .NOTES 51 | Name: Set-RegACL 52 | Author: Sassan Fanai 53 | Date: 2023-06-02 54 | #> 55 | [CmdletBinding()] 56 | param ( 57 | $Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications", 58 | $User = "NT AUTHORITY\System", 59 | [ValidateSet("FullControl","ReadKey","SetValue","TakeOwnership","WriteKey","ChangePermissions","CreateSubKey","NeptDeleteune","QueryValues")] 60 | $Permission = "FullControl" 61 | ) 62 | 63 | $Item = Get-Item -Path $Path 64 | switch ($Item.Name.Split("\")[0]) { 65 | "HKEY_CLASSES_ROOT" { $rootKey=[Microsoft.Win32.Registry]::ClassesRoot; break } 66 | "HKEY_LOCAL_MACHINE" { $rootKey=[Microsoft.Win32.Registry]::LocalMachine; break } 67 | "HKEY_CURRENT_USER" { $rootKey=[Microsoft.Win32.Registry]::CurrentUser; break } 68 | "HKEY_USERS" { $rootKey=[Microsoft.Win32.Registry]::Users; break } 69 | "HKEY_CURRENT_CONFIG" { $rootKey=[Microsoft.Win32.Registry]::CurrentConfig; break } 70 | } 71 | Write-Verbose "Setting ACL permission [$Permission] for user [$User] @ [$Path]" 72 | $Key = $Item.Name.Replace(($Item.Name.Split("\")[0]+"\"),"") 73 | $Item = $rootKey.OpenSubKey($Key,[Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,[System.Security.AccessControl.RegistryRights]::ChangePermissions) 74 | $acl = $Item.GetAccessControl() 75 | $rule = New-Object System.Security.AccessControl.RegistryAccessRule ($User,$Permission,@("ObjectInherit","ContainerInherit"),"None","Allow") 76 | $acl.SetAccessRule($rule) 77 | $Item.SetAccessControl($acl) 78 | } 79 | 80 | function Take-Ownership { 81 | <# 82 | .SYNOPSIS 83 | Give ownership of a file or folder to the specified user. 84 | 85 | .DESCRIPTION 86 | Give the current process the SeTakeOwnershipPrivilege" and "SeRestorePrivilege" rights which allows it 87 | to reset ownership of an object. The script will then set the owner to be the specified user. 88 | 89 | .PARAMETER Path (Required) 90 | The path to the object on which you wish to change ownership. It can be a file or a folder. 91 | 92 | .PARAMETER User (Required) 93 | The user whom you want to be the owner of the specified object. The user should be in the format 94 | \. Other user formats will not work. For system accounts, such as System, the user 95 | should be specified as "NT AUTHORITY\System". If the domain is missing, the local machine will be assumed. 96 | 97 | .PARAMETER Recurse (switch) 98 | Causes the function to parse through the Path recursively. 99 | 100 | .INPUTS 101 | None. You cannot pipe objects to Take-Ownership 102 | 103 | .OUTPUTS 104 | None 105 | 106 | .NOTES 107 | Name: Take-Ownership.ps1 108 | Author: Jason Eberhardt 109 | Date: 2017-07-20 110 | #> 111 | [CmdletBinding(SupportsShouldProcess=$false)] 112 | Param([Parameter(Mandatory=$true, ValueFromPipeline=$false)] [ValidateNotNullOrEmpty()] [string]$Path, 113 | [Parameter(Mandatory=$true, ValueFromPipeline=$false)] [ValidateNotNullOrEmpty()] [string]$User, 114 | [Parameter(Mandatory=$false, ValueFromPipeline=$false)] [switch]$Recurse) 115 | 116 | Begin { 117 | $AdjustTokenPrivileges=@" 118 | using System; 119 | using System.Runtime.InteropServices; 120 | 121 | public class TokenManipulator { 122 | [DllImport("kernel32.dll", ExactSpelling = true)] 123 | internal static extern IntPtr GetCurrentProcess(); 124 | 125 | [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] 126 | internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen); 127 | [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] 128 | internal static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok); 129 | [DllImport("advapi32.dll", SetLastError = true)] 130 | internal static extern bool LookupPrivilegeValue(string host, string name, ref long pluid); 131 | 132 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 133 | internal struct TokPriv1Luid { 134 | public int Count; 135 | public long Luid; 136 | public int Attr; 137 | } 138 | 139 | internal const int SE_PRIVILEGE_DISABLED = 0x00000000; 140 | internal const int SE_PRIVILEGE_ENABLED = 0x00000002; 141 | internal const int TOKEN_QUERY = 0x00000008; 142 | internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; 143 | 144 | public static bool AddPrivilege(string privilege) { 145 | bool retVal; 146 | TokPriv1Luid tp; 147 | IntPtr hproc = GetCurrentProcess(); 148 | IntPtr htok = IntPtr.Zero; 149 | retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok); 150 | tp.Count = 1; 151 | tp.Luid = 0; 152 | tp.Attr = SE_PRIVILEGE_ENABLED; 153 | retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid); 154 | retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); 155 | return retVal; 156 | } 157 | 158 | public static bool RemovePrivilege(string privilege) { 159 | bool retVal; 160 | TokPriv1Luid tp; 161 | IntPtr hproc = GetCurrentProcess(); 162 | IntPtr htok = IntPtr.Zero; 163 | retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok); 164 | tp.Count = 1; 165 | tp.Luid = 0; 166 | tp.Attr = SE_PRIVILEGE_DISABLED; 167 | retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid); 168 | retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); 169 | return retVal; 170 | } 171 | } 172 | "@ 173 | } 174 | 175 | Process { 176 | $Item=Get-Item $Path 177 | Write-Verbose "Giving current process token ownership rights" 178 | Add-Type $AdjustTokenPrivileges -PassThru > $null 179 | [void][TokenManipulator]::AddPrivilege("SeTakeOwnershipPrivilege") 180 | [void][TokenManipulator]::AddPrivilege("SeRestorePrivilege") 181 | 182 | # Change ownership 183 | $Account=$User.Split("\") 184 | if ($Account.Count -eq 1) { $Account+=$Account[0]; $Account[0]=$env:COMPUTERNAME } 185 | $Owner=New-Object System.Security.Principal.NTAccount($Account[0],$Account[1]) 186 | Write-Verbose "Change ownership to '$($Account[0])\$($Account[1])'" 187 | 188 | $Provider=$Item.PSProvider.Name 189 | if ($Item.PSIsContainer) { 190 | switch ($Provider) { 191 | "FileSystem" { $ACL=[System.Security.AccessControl.DirectorySecurity]::new() } 192 | "Registry" { $ACL=[System.Security.AccessControl.RegistrySecurity]::new() 193 | # Get-Item doesn't open the registry in a way that we can write to it. 194 | switch ($Item.Name.Split("\")[0]) { 195 | "HKEY_CLASSES_ROOT" { $rootKey=[Microsoft.Win32.Registry]::ClassesRoot; break } 196 | "HKEY_LOCAL_MACHINE" { $rootKey=[Microsoft.Win32.Registry]::LocalMachine; break } 197 | "HKEY_CURRENT_USER" { $rootKey=[Microsoft.Win32.Registry]::CurrentUser; break } 198 | "HKEY_USERS" { $rootKey=[Microsoft.Win32.Registry]::Users; break } 199 | "HKEY_CURRENT_CONFIG" { $rootKey=[Microsoft.Win32.Registry]::CurrentConfig; break } 200 | } 201 | $Key=$Item.Name.Replace(($Item.Name.Split("\")[0]+"\"),"") 202 | $Item=$rootKey.OpenSubKey($Key,[Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,[System.Security.AccessControl.RegistryRights]::TakeOwnership) } 203 | default { throw "Unknown provider: $($Item.PSProvider.Name)" } 204 | } 205 | $ACL.SetOwner($Owner) 206 | Write-Verbose "Setting owner on $Path" 207 | $Item.SetAccessControl($ACL) 208 | if ($Provider -eq "Registry") { $Item.Close() } 209 | 210 | if ($Recurse.IsPresent) { 211 | # You can't set ownership on Registry Values 212 | if ($Provider -eq "Registry") { $Items=Get-ChildItem -Path $Path -Recurse -Force | Where-Object { $_.PSIsContainer } } 213 | else { $Items=Get-ChildItem -Path $Path -Recurse -Force } 214 | $Items=@($Items) 215 | for ($i=0; $i -lt $Items.Count; $i++) { 216 | switch ($Provider) { 217 | "FileSystem" { $Item=Get-Item $Items[$i].FullName 218 | if ($Item.PSIsContainer) { $ACL=[System.Security.AccessControl.DirectorySecurity]::new() } 219 | else { $ACL=[System.Security.AccessControl.FileSecurity]::new() } } 220 | "Registry" { $Item=Get-Item $Items[$i].PSPath 221 | $ACL=[System.Security.AccessControl.RegistrySecurity]::new() 222 | # Get-Item doesn't open the registry in a way that we can write to it. 223 | switch ($Item.Name.Split("\")[0]) { 224 | "HKEY_CLASSES_ROOT" { $rootKey=[Microsoft.Win32.Registry]::ClassesRoot; break } 225 | "HKEY_LOCAL_MACHINE" { $rootKey=[Microsoft.Win32.Registry]::LocalMachine; break } 226 | "HKEY_CURRENT_USER" { $rootKey=[Microsoft.Win32.Registry]::CurrentUser; break } 227 | "HKEY_USERS" { $rootKey=[Microsoft.Win32.Registry]::Users; break } 228 | "HKEY_CURRENT_CONFIG" { $rootKey=[Microsoft.Win32.Registry]::CurrentConfig; break } 229 | } 230 | $Key=$Item.Name.Replace(($Item.Name.Split("\")[0]+"\"),"") 231 | $Item=$rootKey.OpenSubKey($Key,[Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,[System.Security.AccessControl.RegistryRights]::TakeOwnership) } 232 | default { throw "Unknown provider: $($Item.PSProvider.Name)" } 233 | } 234 | $ACL.SetOwner($Owner) 235 | Write-Verbose "Setting owner on $($Item.Name)" 236 | $Item.SetAccessControl($ACL) 237 | if ($Provider -eq "Registry") { $Item.Close() } 238 | } 239 | } # Recursion 240 | } 241 | else { 242 | if ($Recurse.IsPresent) { Write-Warning "Object specified is neither a folder nor a registry key. Recursion is not possible." } 243 | switch ($Provider) { 244 | "FileSystem" { $ACL=[System.Security.AccessControl.FileSecurity]::new() } 245 | "Registry" { throw "You cannot set ownership on a registry value" } 246 | default { throw "Unknown provider: $($Item.PSProvider.Name)" } 247 | } 248 | $ACL.SetOwner($Owner) 249 | Write-Verbose "Setting owner on $Path" 250 | $Item.SetAccessControl($ACL) 251 | } 252 | } 253 | } 254 | 255 | 256 | if ($null -eq (Get-AppxPackage -Name MicrosoftTeams -AllUsers)) { 257 | Write-Output “Microsoft Teams Personal App not present” 258 | } 259 | else { 260 | try { 261 | Write-Output “Removing Microsoft Teams Personal App” 262 | Get-AppxPackage -Name MicrosoftTeams -AllUsers | Remove-AppPackage -AllUsers 263 | } 264 | catch { 265 | Write-Output “Error removing Microsoft Teams Personal App” 266 | } 267 | } 268 | 269 | Write-Output "Setting ownership of registry key to SYSTEM" 270 | Take-Ownership -Path "Registry::HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" -User "NT AUTHORITY\System" -Verbose 271 | 272 | Write-Output "Setting FullControl ACL on registry key for SYSTEM" 273 | Set-RegACL -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" -User "NT AUTHORITY\System" -Permission FullControl -Verbose 274 | 275 | Write-Output "Setting ConfigureChatAutoInstall to 0" 276 | New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" -Name "ConfigureChatAutoInstall" -Value "0" -PropertyType Dword -Force | Out-Null 277 | 278 | Write-Output "Restoring ReadKey ACL on registry key for SYSTEM" 279 | Set-RegACL -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" -User "NT AUTHORITY\System" -Permission ReadKey -Verbose 280 | 281 | Write-Output "Restoring ownership of registry key back to TrustedInstaller" 282 | Take-Ownership -Path "Registry::HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" -User "NT SERVICE\TrustedInstaller" -Verbose 283 | 284 | Stop-Transcript 285 | -------------------------------------------------------------------------------- /Install-MSTeams.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script installs/uninstalls Microsoft Teams (New) either offline or online using Teamsbootstrapper. 4 | 5 | .DESCRIPTION 6 | The script performs an installation/uninstallation of Microsoft Teams (New) by executing the Teamsbootstrapper.exe with the appropriate flags based on the invocation parameters. 7 | It supports offline installation using a local MSIX package or an online installation that downloads the necessary files. The process is logged in a specified log file. 8 | In an attempt to make the installation experience better and faster the -ForceInstall and -SetRunOnce parameters were made 9 | 10 | .PARAMETER EXE 11 | The name of the executable file for the MSTeams installation bootstrapper. Default is "Teamsbootstrapper.exe". 12 | 13 | .PARAMETER MSIX 14 | The name of the MSIX file for offline installation of MSTeams, only required if using -Offline. Default is "MSTeams-x64.msix". 15 | 16 | .PARAMETER LogFile 17 | The path to the log file where the install/uninstall process will be logged. Default is "$env:TEMP\Install-MSTeams.log". 18 | 19 | .PARAMETER Offline 20 | A switch parameter that, when present, will initiate an offline installation of MSTeams using the local MSIX file. 21 | 22 | .PARAMETER Uninstall 23 | A switch parameter that, when present, will deprovision MSTeams using the Teamsbootstrapper.exe and uninstall the MSTeams AppcPackage for AllUsers. 24 | Uninstall will delete the registry key: HKLM\Software\Wow6432Node\Microsoft\Office\Teams that can can block installations of MSTeams. 25 | Uninstall will attempt to remove InstallMSTeams RunOnce registry item for Default User and existing profiles that may have been set by SetRunOnce. 26 | 27 | .PARAMETER ForceInstall 28 | A switch parameter that, when present, will uninstall and deprovision MSTeams before attempting installation. It will also delete the registry key: 29 | HKLM\Software\Wow6432Node\Microsoft\Office\Teams that can can block the installation of MSTeams. 30 | 31 | .PARAMETER SetRunOnce 32 | A switch parameter that, when present, will configure RunOnce registry value for the Default User profile and all existing profiles to speed up installation of 33 | MSTeams after a user sign in. The RunOnce key will be deleted when uninstalling MSTeams using -Uninstall. 34 | If there is a active currently logged on user, a scheduled task will be be created that installs MSTeams as an AppxPackage to speed up the installation. 35 | 36 | .PARAMETER DownloadExe 37 | A switch parameter that, when present, will attempt to download Teamsbootstrapper.exe from Microsoft and verify its digital signature. 38 | Using this parameter removes the need to include a local Teamsbootstrapper.exe. Has to be specified for -Uninstall as well. 39 | 40 | .EXAMPLE 41 | .\Install-MSTeams.ps1 42 | Executes the script to install MSTeams "online" with default parameters. 43 | 44 | .EXAMPLE 45 | .\Install-MSTeams.ps1 -Offline 46 | Executes the script to install MSTeams offline using the specified MSIX file. 47 | 48 | .EXAMPLE 49 | .\Install-MSTeams.ps1 -Uninstall 50 | Executes the script to deprovision and uninstall MSTeams for all users. 51 | 52 | .EXAMPLE 53 | .\Install-MSTeams.ps1 -DownloadExe 54 | Executes the script to first download the Teamsbootstrapper.exe from Microsoft and then install MSTeams "online". 55 | 56 | .EXAMPLE 57 | .\Install-MSTeams.ps1 -ForceInstall -SetRunOnce 58 | Executes the script and attempts to force the installation by uninstalling MSTeams before attempting an installation. 59 | SetRunOnce will add a RunOnce registry entry and scheduled task to speed up the installation of MSTeams. 60 | These are the recommended parameters for installation. 61 | 62 | .NOTES 63 | Author: Sassan Fanai 64 | Date: 2025-06-17 65 | Version: 1.0.3.3 - Chnaged return code logic to workaround issue when output is not a clean JSON object. 66 | 67 | 1.0.3.2 - Added -DownloadExe parameter that attempts to download Teamsbootstrapper.exe from Microsoft, removing the need of any other local files. 68 | Functions used for download and verification were stolen with pride from @JankeSkanke and MSEndpointMgr @ https://github.com/MSEndpointMgr/M365Apps. Thank you! 69 | 70 | Install command example: %windir%\Sysnative\WindowsPowerShell\v1.0\PowerShell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File ".\Install-MSTeams.ps1" -Offline -ForceInstall 71 | Detection script example 1: if ("MSTeams" -in (Get-ProvisionedAppPackage -Online).DisplayName) { Write-Output "Installed" } 72 | Detection script example 2: $MinVersion = "23285.3604.2469.4152" 73 | $MSTeams = Get-ProvisionedAppPackage -Online | Where-Object {$PSitem.DisplayName -like "MSTeams"} 74 | if ($MSTeams.version -ge [version]$MinVersion ) { Write-Output "Installed" } 75 | #> 76 | [CmdletBinding()] 77 | param ( 78 | $EXE = "Teamsbootstrapper.exe", 79 | $MSIX = "MSTeams-x64.msix", 80 | $LogFile = "$env:TEMP\Install-MSTeams.log", 81 | [switch]$Offline, 82 | [switch]$Uninstall, 83 | [Alias("TryFix")] 84 | [switch]$ForceInstall, 85 | [switch]$SetRunOnce, 86 | [switch]$DownloadExe, 87 | $DownloadExeURL = "https://go.microsoft.com/fwlink/?linkid=2243204&clcid=0x409" # URL to Teamsbootstrapper.exe from https://learn.microsoft.com/en-us/microsoftteams/new-teams-bulk-install-client 88 | ) 89 | 90 | #region functions 91 | function CreateScheduledTask { 92 | param ( 93 | $TaskName = "InstallMSTeams", 94 | $PackageName 95 | ) 96 | Log "Running CreateScheduledTask function" 97 | $LoggedOnUsers = ((query user) -replace '\s{20,39}', ',,') -replace '\s{2,}', ',' | 98 | ConvertFrom-Csv | Select-Object USERNAME, ID, STATE, @{n='IdleTime';e='IDLE TIME'}, @{n='LogonTime';e='LOGON TIME'} 99 | $ActiveUser = $LoggedOnUsers | Where-Object {$_.State -eq "Active" } | Select-Object -ExpandProperty Username 100 | 101 | if (-not $ActiveUser) { 102 | Log "No active user currently logged on" 103 | return 104 | } 105 | else { 106 | $ActiveUser = $ActiveUser.Replace(">","") 107 | Log "Active user currently logged on is: $ActiveUser" 108 | } 109 | 110 | Log "Creating scheduled task that will run for the current logged on user to speed up the installation" 111 | $commands = "Add-AppxPackage -RegisterByFamilyName -MainPackage $PackageName -EA SilentlyContinue" 112 | Log "Creating scheduled task [$taskName] that will run [$commands] for the currently logged on user [$ActiveUser]" 113 | 114 | # Create the action to execute the PowerShell commands 115 | $action = New-ScheduledTaskAction -Execute "cmd" -Argument "/c start /min `"`" powershell.exe -EP Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -Command `"$commands`"" 116 | 117 | # Set the trigger for immediate execution 118 | $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(1) 119 | 120 | # Run in the user context 121 | $principal = New-ScheduledTaskPrincipal -UserId "$($ActiveUser)" -LogonType Interactive 122 | 123 | # Define task settings 124 | $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable 125 | 126 | # Register the task (hidden) 127 | $RegTask = Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force 128 | 129 | Log "Sleeping for a couple of seconds before removing the scheduled task [$taskName]" 130 | Start-Sleep -Seconds 5 131 | $UnregTask = Unregister-ScheduledTask -TaskName $taskName -Confirm:$false 132 | } 133 | 134 | function SetRunOnce { 135 | param ( 136 | $PackageName, 137 | $RunOnceRegName, 138 | [switch]$Delete 139 | ) 140 | Log "Running SetRunOnce function" 141 | Log "Loading NTUSER.DAT for Default User" 142 | $load = REG.EXE LOAD HKLM\Default C:\Users\Default\NTUSER.DAT 143 | 144 | $Value = "cmd /c start /min `"`" powershell.exe -EP Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -Command `"Add-AppxPackage -RegisterByFamilyName -MainPackage $PackageName -EA SilentlyContinue`"" 145 | if ($Delete) { 146 | Log "Delete parameter was specified for SetRunOnce function" 147 | $RunOnceReg = Get-ItemProperty -Path "HKLM:\Default\Software\Microsoft\Windows\CurrentVersion\RunOnce" -Name $RunOnceRegName -ErrorAction SilentlyContinue 148 | if ($RunOnceReg) { 149 | Log "Deleting $RunOnceRegName RunOnce entry from Default User profile" 150 | $reg = Remove-ItemProperty -Path "$($RunOnceReg.PSPath)" -Name $RunOnceRegName -Force 151 | } 152 | } 153 | else { 154 | Log "Creating RunOnce registry value: $value" 155 | $reg = New-ItemProperty -Path "HKLM:\Default\Software\Microsoft\Windows\CurrentVersion\RunOnce" -PropertyType "String" -Name "$($RunOnceRegName)" -Value $Value -Force 156 | } 157 | 158 | Log "Running garbage collect and unloading Default User profile" 159 | try { $reg.Handle.Close() } catch {} 160 | [GC]::Collect() 161 | $unload = REG.EXE UNLOAD HKLM\Default 162 | 163 | # Run for existing profiles 164 | $UserProfiles = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | 165 | Where-Object {$_.PSChildName -match "S-1-5-21-(\d+-?){4}$" } | 166 | Select-Object @{Name="SID"; Expression={$_.PSChildName}}, @{Name="UserHive";Expression={"$($_.ProfileImagePath)\NTuser.dat"}} 167 | 168 | foreach ($UserProfile in $UserProfiles) { 169 | # Load User NTUser.dat if it's not already loaded 170 | if (($ProfileWasLoaded = Test-Path Registry::HKEY_USERS\$($UserProfile.SID)) -eq $false) { 171 | Log "Loading NTUSER.DAT for profile: $($UserProfile.UserHive)" 172 | Start-Process -FilePath "CMD.EXE" -ArgumentList "/C REG.EXE LOAD HKU\$($UserProfile.SID) $($UserProfile.UserHive)" -Wait -WindowStyle Hidden 173 | } 174 | else { 175 | Log "Profile already loaded for: $($UserProfile.UserHive), no need to load NTUSER.DAT" 176 | } 177 | if ($Delete) { 178 | $RunOnceReg = Get-ItemProperty -Path "registry::HKEY_USERS\$($UserProfile.SID)\Software\Microsoft\Windows\CurrentVersion\RunOnce" -Name $RunOnceRegName -ErrorAction SilentlyContinue 179 | if ($RunOnceReg) { 180 | Log "Deleting $RunOnceRegName RunOnce entry from the user hive for: $($UserProfile.UserHive)" 181 | $reg = Remove-ItemProperty -Path "$($RunOnceReg.PSPath)" -Name $RunOnceRegName -Force 182 | } 183 | } 184 | else { 185 | Log "Creating RunOnce registry value for: $($UserProfile.UserHive) with SID $($UserProfile.SID)" 186 | $reg = New-ItemProperty "registry::HKEY_USERS\$($UserProfile.SID)\Software\Microsoft\Windows\CurrentVersion\RunOnce" -PropertyType "String" -Name "$($RunOnceRegName)" -Value $Value -Force 187 | } 188 | 189 | try { $reg.Handle.Close() } catch {} 190 | if ($ProfileWasLoaded -eq $false) { 191 | Log "Running garbage collector and unloading user profile: $($UserProfile.UserHive)" 192 | [GC]::Collect() 193 | Start-Sleep 1 194 | Start-Process -FilePath "CMD.EXE" -ArgumentList "/C REG.EXE UNLOAD HKU\$($UserProfile.SID)" -Wait -WindowStyle Hidden 195 | } 196 | } 197 | } 198 | 199 | function Install-MSTeams { 200 | param ( 201 | [switch]$Offline 202 | ) 203 | if ($Offline) { 204 | $Result = & "$EXEFolder\$EXE" -p -o "$MSIXFolder\$MSIX" 205 | } 206 | else { 207 | $Result = & "$EXEFolder\$EXE" -p 208 | } 209 | $ResultPSO = try { $Result | ConvertFrom-Json } catch {$null} 210 | if ($null -ne $ResultPSO) { 211 | return $ResultPSO 212 | } 213 | else { 214 | return $Result 215 | } 216 | } 217 | 218 | function Uninstall-MSTeams { 219 | Log "Running Uninstall-MSTeams function" -NoOutput 220 | $Appx = Get-AppxPackage -AllUsers | Where-Object {$PSItem.Name -eq "MSTeams"} 221 | if ($Appx) { 222 | Log "MSTeams $($Appx.Version) package is installed for these users: $($Appx.PackageUserInformation.UserSecurityId.UserName)" -NoOutput 223 | Log "Uninstalling AppxPackage for AllUsers" -NoOutput 224 | $Appx | Remove-AppxPackage -AllUsers 225 | } 226 | Log "Deprovisioning MSTeams using $EXE" -NoOutput 227 | $Result = & "$EXEFolder\$EXE" -x 228 | 229 | $ResultPSO = try { $Result | ConvertFrom-Json } catch {$null} 230 | if ($null -ne $ResultPSO) { 231 | return $ResultPSO 232 | } 233 | else { 234 | return $Result 235 | } 236 | } 237 | 238 | function IsAppInstalled { 239 | param ( 240 | $AppName = "MSTeams" 241 | ) 242 | $Appx = Get-AppxPackage -AllUsers | Where-Object {$PSItem. Name -eq $AppName} 243 | $ProvApp = Get-ProvisionedAppPackage -Online | Where-Object {$PSItem. DisplayName -eq $AppName} 244 | if ($Appx) { 245 | Log "$AppName AppxPackage ($Appx) is currently installed for these users: $($Appx.PackageUserInformation.UserSecurityId.UserName)" 246 | } 247 | else { 248 | Log "$AppName AppxPackage is currently NOT installed for any user" 249 | } 250 | if ($ProvApp) { 251 | Log "$AppName ProvisionedAppPackage ($($ProvApp.PackageName)) is currently installed" 252 | } 253 | else { 254 | Log "$AppName ProvisionedAppPackage is currently NOT installed" 255 | } 256 | } 257 | 258 | function Log { 259 | param ( 260 | $Text, 261 | $LogFile = $LogFile, 262 | [switch]$NoOutput, 263 | [switch]$NoLog 264 | ) 265 | 266 | $Now = "{0:yyyy-MM-dd HH:mm:ss}" -f [DateTime]::Now 267 | if (!$NoLog) { 268 | "$Now`: $($Text)" | Out-File -FilePath $LogFile -Append 269 | } 270 | if (!$NoOutput) { 271 | Write-Output "$Now`: $($Text)" 272 | } 273 | } 274 | 275 | function Start-DownloadFile { 276 | param( 277 | [parameter(Mandatory = $true)] 278 | [ValidateNotNullOrEmpty()] 279 | [string]$URL, 280 | 281 | [parameter(Mandatory = $true)] 282 | [ValidateNotNullOrEmpty()] 283 | [string]$Path, 284 | 285 | [parameter(Mandatory = $true)] 286 | [ValidateNotNullOrEmpty()] 287 | [string]$Name 288 | ) 289 | Begin { 290 | # Construct WebClient object 291 | $WebClient = New-Object -TypeName System.Net.WebClient 292 | } 293 | Process { 294 | # Create path if it doesn't exist 295 | if (-not(Test-Path -Path $Path)) { 296 | New-Item -Path $Path -ItemType Directory -Force | Out-Null 297 | } 298 | 299 | # Start download of file 300 | $WebClient.DownloadFile($URL, (Join-Path -Path $Path -ChildPath $Name)) 301 | } 302 | End { 303 | # Dispose of the WebClient object 304 | $WebClient.Dispose() 305 | } 306 | } 307 | 308 | function Invoke-FileCertVerification { 309 | param( 310 | [parameter(Mandatory = $true)] 311 | [ValidateNotNullOrEmpty()] 312 | [string]$FilePath 313 | ) 314 | # Get a X590Certificate2 certificate object for a file 315 | $Cert = (Get-AuthenticodeSignature -FilePath $FilePath).SignerCertificate 316 | $CertStatus = (Get-AuthenticodeSignature -FilePath $FilePath).Status 317 | if ($Cert){ 318 | #Verify signed by Microsoft and Validity 319 | if ($cert.Subject -match "O=Microsoft Corporation" -and $CertStatus -eq "Valid"){ 320 | #Verify Chain and check if Root is Microsoft 321 | $chain = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Chain 322 | $chain.Build($cert) | Out-Null 323 | $RootCert = $chain.ChainElements | ForEach-Object {$_.Certificate}| Where-Object {$PSItem.Subject -match "CN=Microsoft Root"} 324 | if (-not [string ]::IsNullOrEmpty($RootCert)){ 325 | #Verify root certificate exists in local Root Store 326 | $TrustedRoot = Get-ChildItem -Path "Cert:\LocalMachine\Root" -Recurse | Where-Object { $PSItem.Thumbprint -eq $RootCert.Thumbprint} 327 | if (-not [string]::IsNullOrEmpty($TrustedRoot)){ 328 | Log "Verified setupfile signed by : $($Cert.Issuer)" 329 | Return $True 330 | } 331 | else { 332 | Log "No trust found to root cert - aborting" 333 | Return $False 334 | } 335 | } 336 | else { 337 | Log "Certificate chain not verified to Microsoft - aborting" 338 | Return $False 339 | } 340 | } 341 | else { 342 | Log "Certificate not valid or not signed by Microsoft - aborting" 343 | Return $False 344 | } 345 | } 346 | else { 347 | Log "Setup file not signed - aborting" 348 | Return $False 349 | } 350 | } 351 | 352 | #endregion functions 353 | 354 | Log "### Starting Install-MSTeams execution ###" 355 | $EXEFolder = $PSScriptRoot 356 | $MSIXFolder = $PSScriptRoot 357 | 358 | if ($DownloadExe) { 359 | Log "Attempting to download Teamsbootstrapper.exe" 360 | Start-DownloadFile -URL $DownloadExeURL -Path $env:TEMP -Name "Teamsbootstrapper.exe" 361 | $FileCheck = Invoke-FileCertVerification -FilePath (Join-Path -Path $env:TEMP -ChildPath $EXE) 362 | if ($FileCheck) { 363 | Log "Verification of downloaded Teamsbootstrapper.exe was successful" 364 | $EXEFolder = $Env:TEMP 365 | } 366 | else { 367 | Log "Verification of downloaded Teamsbootstrapper.exe failed" 368 | } 369 | } 370 | 371 | 372 | 373 | if (-not(Test-Path -Path $EXEFolder\$EXE)) { 374 | Log "Failed to find $EXE" 375 | exit 2 376 | } 377 | 378 | $EXEinfo = Get-ChildItem -Path "$EXEFolder\$EXE" 379 | 380 | if ($Uninstall) { 381 | $LogFile = $LogFile.Replace("Install","Uninstall") 382 | Log "Attempting to uninstall MSTeams" 383 | IsAppInstalled "MSTeams" 384 | Log "$EXE version is $($EXEinfo.VersionInfo.ProductVersion)" 385 | 386 | $result = Uninstall-MSTeams 387 | $Appx = Get-AppxPackage -AllUsers | Where-Object {$PSItem. Name -eq "MSTeams"} 388 | $ProvApp = Get-ProvisionedAppPackage -Online | Where-Object {$PSItem. DisplayName -eq "MSTeams"} 389 | 390 | if (!$Appx -and !$ProvApp) { 391 | Log "Deleting registry key (if it exists): HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\Teams" 392 | Remove-Item HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\Teams -Force -ErrorAction SilentlyContinue 393 | Log "MSTeams is not installed as a ProvisionedAppPackage or AppxPackage for any user" 394 | SetRunOnce -RunOnceRegName "InstallMSTeams" -Delete 395 | exit 0 396 | } 397 | else { 398 | Log "Error uninstalling MSTeams: $Result" 399 | IsAppInstalled "MSTeams" 400 | exit 1 401 | } 402 | } 403 | 404 | if ($Offline) { 405 | if (-not(Test-Path -Path "$MSIXFolder\$MSIX")) { 406 | Log "Offline parameter specified but failed to find $MSIX" 407 | exit 2 408 | } 409 | Log "Attempting to install MSTeams offline with local MSIX" 410 | $MSIXinfo = Get-AppLockerFileInformation "$MSIXFolder\$MSIX" 411 | Log "$EXE version is $($EXEinfo.VersionInfo.ProductVersion)" 412 | Log "$MSIX version is $($MSIXinfo.Publisher.BinaryVersion.ToString())" 413 | 414 | if ($ForceInstall) { 415 | Log "ForceInstall parameter was specified, will attempt to uninstall and deprovision MSTeams before installing" 416 | IsAppInstalled "MSTeams" 417 | $result = Uninstall-MSTeams 418 | Log "Deleting registry key (if it exists): HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\Teams" 419 | Remove-Item HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\Teams -Force -ErrorAction SilentlyContinue 420 | $Appx = Get-AppxPackage -AllUsers | Where-Object {$PSItem. Name -eq "MSTeams"} 421 | $ProvApp = Get-ProvisionedAppPackage -Online | Where-Object {$PSItem. DisplayName -eq "MSTeams"} 422 | 423 | if (!$Appx -and !$ProvApp) { 424 | "MSTeams is not installed as a ProvisionedAppPackage or AppxPackage for any user" 425 | } 426 | else { 427 | Log "Error uninstalling MSTeams: $Result" 428 | IsAppInstalled "MSTeams" 429 | } 430 | } 431 | $result = Install-MSTeams -Offline 432 | if ($Result -match '"success": true') { 433 | Log "$EXE ($($EXEinfo.VersionInfo.ProductVersion)) successfully installed $MSIX ($($MSIXinfo.Publisher.BinaryVersion.ToString())) offline" 434 | if ($SetRunOnce) { 435 | $ProvApp = Get-ProvisionedAppPackage -Online | Where-Object {$PSItem. DisplayName -eq "MSTeams"} 436 | Log "SetRunOnce parameter specified, attempting to configure RunOnce registry key for Default User and existing profiles to speed up installation of MSTeams after sign in" 437 | SetRunOnce -PackageName "$($ProvApp.PackageName)" -RunOnceRegName "InstallMSTeams" 438 | CreateScheduledTask -PackageName "$($ProvApp.PackageName)" 439 | } 440 | Log "### Finished Install-MSTeams execution ###" 441 | exit 0 442 | } 443 | 444 | Log "Error installing MSTeams offline using $EXE ($($EXEinfo.VersionInfo.ProductVersion)) and $MSIX ($($MSIXinfo.Publisher.BinaryVersion.ToString()))" 445 | Log "$EXE returned errorCode = $($result.errorCode)" 446 | Log "Result: $result" 447 | Log "Installation will fail if the AppxPackage is already installed for any user. You can run the script with -ForceInstall to uninstall MSTeams prior to installation" 448 | IsAppInstalled "MSTeams" 449 | exit 1 450 | } 451 | else { 452 | Log "Attempting to install MSTeams online with $EXE" 453 | Log "$EXE version is $($EXEinfo.VersionInfo.ProductVersion)" 454 | if ($ForceInstall) { 455 | Log "ForceInstall parameter was specified, will attempt to uninstall and deprovision MSTeams before install" 456 | $result = Uninstall-MSTeams 457 | Log "Deleting registry key (if it exists): HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\Teams" 458 | Remove-Item HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\Teams -Force -ErrorAction SilentlyContinue 459 | $Appx = Get-AppxPackage -AllUsers | Where-Object {$PSItem. Name -eq "MSTeams"} 460 | $ProvApp = Get-ProvisionedAppPackage -Online | Where-Object {$PSItem. DisplayName -eq "MSTeams"} 461 | 462 | if (!$Appx -and !$ProvApp) { 463 | Log "MSTeams is not installed as a ProvisionedAppPackage or AppxPackage for any user" 464 | } 465 | else { 466 | Log "Error uninstalling MSTeams: $Result" 467 | IsAppInstalled "MSTeams" 468 | } 469 | } 470 | 471 | $result = Install-MSTeams 472 | if ($result.Success) { 473 | Log "$EXE ($($EXEinfo.VersionInfo.ProductVersion)) successfully downloaded and installed MSTeams" 474 | if ($SetRunOnce) { 475 | $ProvApp = Get-ProvisionedAppPackage -Online | Where-Object {$PSItem. DisplayName -eq "MSTeams"} 476 | Log "SetRunOnce parameter specified, attempting to configure RunOnce registry key for Default User and existing profiles to speed up installation of MSTeams after sign in" 477 | SetRunOnce -PackageName "$($ProvApp.PackageName)" -RunOnceRegName "InstallMSTeams" 478 | CreateScheduledTask -PackageName "$($ProvApp.PackageName)" 479 | } 480 | Log "### Finished Install-MSTeams execution ###" 481 | exit 0 482 | } 483 | 484 | Log "Error installing MSTeams online using $EXE ($($EXEinfo.VersionInfo.ProductVersion))" 485 | Log "$EXE returned errorCode = $($result.errorCode)" 486 | Log "Result: $result" 487 | Log "Installation will fail if the AppxPackage is already installed for any user. You can run the script with -ForceInstall to uninstall MSTeams prior to installation" 488 | IsAppInstalled "MSTeams" 489 | exit 1 490 | } 491 | --------------------------------------------------------------------------------