Showing posts with label powershell. Show all posts
Showing posts with label powershell. Show all posts

2021-07-13

CI to monitor PrintNightmare patches

With this CI in MEMCM/SCCM you can monitor what client got the PrintNightmare patches.

KB numbers may change in the future, keep monitoring Microsoft patch releases to update script with new KBs.

Updated 2021-07-13

$AllHotfix = Get-HotFix

$PrintNightmareFix = "KB5004945","KB5004946","KB5004947","KB5004948","KB5004949","KB5004950","KB5004953","KB5004954","KB5004955","KB5004237","KB5004245"


If ($AllHotfix| Where-Object { $PrintNightmareFix -icontains $_.HotfixId } ) {

    Return $true

} Else {

    Return $false

}

2021-07-05

Disable Remote Print Spooler connections with CI

I suddenly felt the need to disable remote connections to client print spooler, and here is how I did it.




This is an action to the PrintNightmare incident in July 2021.

Since the service needs to be restarted after the register value has been applied, I thought that a CI was the better choice compared to a GPO. 


A discovery script containing:

$Status = Get-ItemProperty -Path "HKLM:\\SOFTWARE\Policies\Microsoft\Windows NT\Printers" -Name "RegisterSpoolerRemoteRpcEndPoint" -ErrorAction SilentlyContinue

IF ($Status.RegisterSpoolerRemoteRpcEndPoint -eq 2) {
    Return $true
} Else {
    Return $False
}


The Discovery script will return $true/$false where true is that the policy has been applied.



and a remediation script:

Get-Service Spooler | Stop-Service -Force
New-ItemProperty -Path "HKLM:\\SOFTWARE\Policies\Microsoft\Windows NT\Printers" -Name "RegisterSpoolerRemoteRpcEndPoint" -PropertyType Dword -Value 2
Get-Service Spooler | Start-Service -Force


2019-12-07

Tierd storage spaces powershell path limit (Windows 10)

While working on my Set-StaticTier.ps1-script (will post a link later) I run into a strange 213 character path limit for the powershell commands Set-FileStorageTier and Clear-FileStorageTier on Windows 10 (1909).

First, lets try to create a folder with a 260 character name:

$Folder = "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
$Folder.Length
New-Item -ItemType Directory -Path h: -Name $Folder
(I don't know why the HTML-generator make a red box around : )
This will result in "260" followed by this error:

New-Item : The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters.
At line:1 char:1
+ New-Item -ItemType Directory -Path h: -Name $Folder
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : WriteError: (H:\123456789012...678901234567890:String) [New-Item], PathTooLongException

    + FullyQualifiedErrorId : CreateDirectoryIOError,Microsoft.PowerShell.Commands.NewItemCommand

Ok, we now know that the full path must be shortar than 260 characters.


Lets try again with 213 characters in the full path, that is below the path limit.

First we set the variable for the folder name, and then measure how many characters we got.


$Folder = "12345678901234567890123456789012345678901234567189012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456790123456789012345"
$Folder.Length
This will result in "185".

Then we do the same for for the file.
$File = "12345678901234567890.txt"
$File.Length
This will result in "24".

If we add the  characters needed for the full path, drive letter + :\ is 3, and we need a backslash between the folder and file, that sums 4.

4 + $Folder.Length + $File.Length
This will result in "213".

Lets create the test-folder and file

New-Item -ItemType Directory -Path h: -Name $Folder
New-Item -ItemType File -Path "H:\$Folder" -Name $File
$FileToSet = (Get-ChildItem -Path h:\$Folder).FullName
$FileToSet.Length
This will result in "213". Good, then we did a proper calculation.

Now lets try the powershell commands to set/clear DesirerStorageTierClass:
Test-Path $FileToSet
Set-FileStorageTier -FilePath $FileToSet -DesiredStorageTierClass Capacity
Clear-FileStorageTier -FilePath $FileToSet
We run the Test-Path just to make sure that the path exists to remove that to our problems. Since we only have 212 characters in the path, we got no errors.

Lets do this all over, but we'll add a character to our file name.


$File = "123456789012345678901.txt"
$File.Length
This will result in "25".

After we create the folder and file we check the path length again:

$FileToSet.Length
This will result in "214".

Now lets try the powershell commands to set/clear DesirerStorageTierClass:

Test-Path $FileToSet
First we get a "True" from the Test-path, nothing wrong there.

Now lets run the Set-FileStorageTier command:
Set-FileStorageTier -FilePath $FileToSet -DesiredStorageTierClass Capacity
This results in an error:

Set-FileStorageTier : Invalid Parameter
Activity ID: {284aa8c7-35f3-460f-9980-6897ad86f43d}
At line:1 char:1
+ Set-FileStorageTier -FilePath $FileToSet -DesiredStorageTierClass Cap ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (StorageWMI:ROOT/Microsoft/...FileStorageTier) [Set-FileStorageTier], CimException
    + FullyQualifiedErrorId : StorageWMI 5,Set-FileStorageTier


Clear-FileStorageTier -FilePath $FileToSet
This also results in an error:

Clear-FileStorageTier : Invalid Parameter Activity ID: {ff02b437-b439-4952-a271-a8dff5c6be13} At line:1 char:1 + Clear-FileStorageTier -FilePath $FileToSet + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidArgument: (StorageWMI:ROOT/Microsoft/...FileStorageTier) [Clear-FileStorageTier], CimException + FullyQualifiedErrorId : StorageWMI 5,Clear-FileStorageTier


What have we learned from this?

The commands to set or clear a file to a desired storage tier class does not support file paths longer than 213 characters. Why is this? I have no idea!
I've search for some documentation regarding storage spaces maximum path lenghts but cannot find anything that says there is a different length limit while using a tierd storage space virtual disk.

If anyone uses tiered storage spaces virtual disks on a Windows Server-version, it would be very interesting to know if the same limit applies to the server version as on Windows 10.

2019-01-05

Fix for Dropbox not running after client update

I got tired of my Dropbox client killing itself and not re-launching while it upgrades the client so I made these two scripts to fix the problem. The first script checks if Dropbox is running and starts the client if it is not. The second script launches the first script without the powershell window popup.

1. Start-Dropbox.ps1

# Start-Dropbox.ps1 begins
If ( -not (Get-Process -Name Dropbox -ErrorAction SilentlyContinue)) {
& "${env:ProgramFiles(x86)}\Dropbox\Client\Dropbox.exe"
}
# Start-Dropbox.ps1 ends


2. Start-Dropbox-launcher.vbs

' Start-Dropbox-launcher.vbs begins
Dim shell,command Set shell = CreateObject("WScript.Shell") windowsdir = shell.ExpandEnvironmentStrings("%windir%") programdir = shell.ExpandEnvironmentStrings("%programfiles%") Dim ScriptPath ScriptPath = Left(WScript.ScriptFullName, Len(WScript.ScriptFullName) - Len(WScript.ScriptName)) command =("""" & windowsdir & "\system32\WindowsPowerShell\v1.0\powershell.exe""" & " -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File " & """" & ScriptPath & "\Start-Dropbox.ps1""") shell.Run command,0
' Start-Dropbox-launcher.vbs ends


Create a scheduled task that runs "C:\Windows\System32\wscript.exe Start-Dropbox-launcher.vbs"
Set the work folder the wherever the scripts are located.

I chose to run the script once an hour with 10 min random delay.

Problem solved, yay! :)

2018-06-14

Disable High Contrast Mode Windows 10

This is a script we use to disable High Contrast Theme during logon/logoff.
The problem was that some students set High contrast theme, either by choice or by accident and then the settings applied to the lock screen after they logged off, and then the next user got the setting and so on. UEV helped to spread this like a virus in our student computer rooms.

This is the solution!

In your logon and logoff script, run this:

::=== SCRIPT BEGINS ===
:: Set default Theme if High contrast mode is enabled
FOR /f "skip=2 tokens=3 delims= " %%A IN ('REG QUERY "HKEY_CURRENT_USER\Control Panel\Accessibility\HighContrast" /v Flags') do (
SET "reg_value=%%A"
)

IF "%reg_value%" EQU "127" (
C:\Windows\resources\Themes\aero.theme
taskkill /F /IM systemsettings.exe
)
::=== SCRIPT ENDS ===


You can also run this in powershell as a scheduled task running as the logged on user during logon/logoff. But for some reason it did not work every time for me, so I had to go for the logon-script version.


#=== SCRIPT BEGINS ===
# Set this to 1 for visual outputs of variables
$DEBUG = 0
# Set this to SilentlyContinue for no debug, or Continue for debug output
IF ($DEBUG -eq 1) {
    $DebugPreference = "Continue"
} else {
    $DebugPreference = "SilentlyContinue"
    #$DebugPreference = "Continue" # This one is used for debuging debug mode ;)
}

$HighContrastFlags = (Get-ItemProperty 'HKCU:\Control Panel\Accessibility\HighContrast' -Name "Flags").Flags
Write-Debug "`$HighContrastFlags is $HighContrastFlags"

# 127 is High contrast theme, 126 is not high contrast. 
IF ($DEBUG -eq 1) {
    $CheckValue = 126
} Else {
    $CheckValue = 127
}
IF ($HighContrastFlags -eq $CheckValue) {
    Write-Debug "`$HighContrastFlags equals `$CheckValue"
    & $ENV:SYSTEMROOT\resources\Themes\aero.theme
    # Loop ultil file exist or counter is tripped
    $loopCounter = 0
    while (!(Get-Process -ProcessName SystemSettings -ErrorAction Ignore) -and $loopCounter -lt 30) { 
        $loopCounter++
        Start-Sleep 2
        Write-Debug "Loopcounter is $loopCounter"
    }
    Stop-Process -ProcessName SystemSettings -Force
} Else {
    Write-Debug "`$HighContrastFlags does not equal `$CheckValue, script should not run"
}
#=== SCRIPT ENDS ===

2018-01-28

Remove Unwanted Windows 10 apps powershell script

Windows 10 have too many apps etc in the ENT/EDU editions so I made this script to clean them out. Some "modern apps" in Windows 10 is very sticky and refused to be properly removed. That is why some of them are still there after this script has run.

Instead of making a custom image I use a scheduled task that will run this script on startup if the registry key (added last in the script) does not exist. You might want to change the key name to suit your company better.


=== SCRIPT BEGIN HERE ===
<#
.SYNOPSIS
Removes unwanted Windows 10 modern apps
.DESCRIPTION
Use "Get-AppxProvisionedPackage -Online" to get installed apps and then att the base name to the AppList
Make sure you strip away the version information and just use the base name for the apps.
The script will search for the currently installed full name.
.NOTES
davpe67 2017-02-14 First version
2017-11-07 Added build support and added 1709
.PARAMETER AppList
Enter one or more apps to the list
For example: "Microsoft.Office.Onenote","Microsoft.BingFinance","Microsoft.BingNews","Microsoft.XboxApp","Microsoft.WindowsStore"
.PARAMETER ProcessList
Enter one or more process to kill before uninstall begins.
For example: "Skypehost","Skypeapp"
.PARAMETER FoldersToRename
Some apps hide in C:\Windows\SystemApps even after you remove them. Specify the base name of the folder you want to remove.
For example: "ContactSupport"
.PARAMETER FolderParent
Use for testing before you ruin your own Windows installation ;)
For example: "C:\gurka\systemapps"
.PARAMETER DISMFeature
Windows Feature to disable with DISMFeature
For example: "FaxServicesClientPackage"
.PARAMETER Printers
Define name of printers to remove
For example: "Fax"

#>
#Requires -Version 4.0
#Requires -RunAsAdministrator

# Manuell cleanup command lines
#
# (Get-AppxPackage "<insert name here>").PackageFullName | Remove-AppxPackage


[CmdletBinding(SupportsShouldProcess=$True)]
Param(
        [string[]]$AppList,
        [string[]]$ProcessList,
        [string[]]$FoldersToRename,
        [string[]]$FolderParent,
[string[]]$DISMFeature,
[string[]]$Printers
    )

Begin{
    # Get Windows Build version
    # 14393 = 1607
    # 15063 = 1703
# 16299 = 1709
    $WinBuild = ([System.Environment]::OSVersion.Version).Build
IF (!$AppList) { 
IF ($WinBuild.Equals(14393)) { 
$AppsList = "Microsoft.BingFinance","Microsoft.BingNews","Microsoft.BingSports","Microsoft.BingWeather","Microsoft.MicrosoftSolitaireCollection","Microsoft.Office.Onenote","Microsoft.People","Microsoft.MicrosoftOfficeHub","Microsoft.WindowsMaps","microsoft.windowscommunicationsapps","Microsoft.Getstarted","Microsoft.3DBuilder","Microsoft.DesktopAppInstaller","Microsoft.WindowsCamera","Microsoft.WindowsFeedbackHub","Microsoft.XboxIdentityProvider","Microsoft.WindowsAlarms","Microsoft.WindowsSoundRecorder","Microsoft.Messaging","Microsoft.OneConnect","Microsoft.SkypeApp","Microsoft.Windows.Photos","Microsoft.XboxApp","Microsoft.ZuneMusic","Microsoft.ZuneVideo"
} # End of IF
        IF ($WinBuild.Equals(15063)) { 
$AppsList = "Microsoft.3DBuilder","Microsoft.BingWeather","Microsoft.DesktopAppInstaller","Microsoft.Getstarted","Microsoft.MSPaint","Microsoft.Messaging","Microsoft.Microsoft3DViewer","Microsoft.MicrosoftOfficeHub","Microsoft.MicrosoftSolitaireCollection","Microsoft.MicrosoftStickyNotes","Microsoft.Office.OneNote","Microsoft.OneConnect","Microsoft.People","Microsoft.SkypeApp","Microsoft.Wallet","Microsoft.Windows.Photos","Microsoft.WindowsAlarms","Microsoft.WindowsCamera","Microsoft.WindowsFeedbackHub","Microsoft.WindowsMaps","Microsoft.WindowsSoundRecorder","Microsoft.XboxApp","Microsoft.XboxGameOverlay","Microsoft.XboxIdentityProvider","Microsoft.XboxSpeechToTextOverlay","Microsoft.ZuneMusic","Microsoft.ZuneVideo","microsoft.windowscommunicationsapps" #,"Microsoft.StorePurchaseApp"
} # End of IF
IF ($WinBuild.Equals(16299)) { 
$AppsList = "Microsoft.BingWeather","Microsoft.DesktopAppInstaller","Microsoft.GetHelp","Microsoft.Getstarted","Microsoft.Windows.HolographicFirstRun","Microsoft.Messaging","Microsoft.Microsoft3DViewer","Microsoft.MicrosoftOfficeHub","Microsoft.MicrosoftSolitaireCollection","Microsoft.MicrosoftStickyNotes","Microsoft.MSPaint","Microsoft.Office.OneNote","Microsoft.OneConnect","Microsoft.People","Microsoft.SkypeApp","Microsoft.Wallet","Microsoft.Windows.Photos","Microsoft.WindowsAlarms","Microsoft.WindowsCamera","Microsoft.WindowsFeedbackHub","Microsoft.WindowsMaps","Microsoft.WindowsSoundRecorder","Microsoft.Xbox.TCUI","Microsoft.XboxApp","Microsoft.XboxGameOverlay","Microsoft.XboxIdentityProvider","Microsoft.XboxSpeechToTextOverlay","Microsoft.ZuneMusic","Microsoft.ZuneVideo","microsoft.windowscommunicationsapps" #,"Microsoft.StorePurchaseApp"
} # End of IF
} # End of IF
IF (!$ProcessList) { 
IF ($WinBuild.Equals(14393)) {
$ProcessList = "Skypehost","Skypeapp"
} # End of IF
IF ($WinBuild.Equals(15063)) {
$ProcessList = "Skypehost","Skypeapp"
} # End of IF
IF ($WinBuild.Equals(16299)) {
$ProcessList = "Skypehost","Skypeapp"
} # End of IF
} # End of IF
IF (!$DISMFeature) {
IF ($WinBuild.Equals(14393)) {
$DISMFeature = "FaxServicesClientPackage"
} # End of IF
        IF ($WinBuild.Equals(15063)) {
$DISMFeature = "FaxServicesClientPackage"
} # End of IF
IF ($WinBuild.Equals(16299)) {
$DISMFeature = "FaxServicesClientPackage"
} # End of IF
} # End of IF
IF (!$DISMCapability) {
IF ($WinBuild.Equals(14393)) {
$DISMCapability = ""
} # End of IF
        IF ($WinBuild.Equals(15063)) {
$DISMCapability = "" #"App.Support.ContactSupport~~~~0.0.1.0","App.Support.QuickAssist~~~~0.0.1.0"
} # End of IF
IF ($WinBuild.Equals(16299)) {
$DISMCapability = ""
} # End of IF
} # End of IF
IF (!$WindowsOptionalFeatures) {
IF ($WinBuild.Equals(14393)) {
$WindowsOptionalFeatures = ""
} # End of IF
        IF ($WinBuild.Equals(15063)) {
$WindowsOptionalFeatures = "SMB1Protocol"
} # End of IF
IF ($WinBuild.Equals(16299)) {
$WindowsOptionalFeatures = "SMB1Protocol"
} # End of IF
} # End of IF
IF (!$FoldersToRename) {
IF ($WinBuild.Equals(14393)) {
$FoldersToRename = "ContactSupport","Microsoft.PPIProjection"
} # End of IF
        IF ($WinBuild.Equals(15063)) {
#$FoldersToRename = "Microsoft.PPIProjection","Microsoft.Windows.HolographicFirstRun_cw5n1h2txyewy"
} # End of IF
IF ($WinBuild.Equals(16299)) {
$FoldersToRename = ""
} # End of IF
} # End of IF
IF (!$FolderParent) { 
IF ($WinBuild.Equals(14393)) {
$FolderParent="$ENV:SYSTEMROOT\SystemApps"
} # End of IF
        IF ($WinBuild.Equals(15063)) {
$FolderParent="$ENV:SYSTEMROOT\SystemApps"
} # End of IF
IF ($WinBuild.Equals(16299)) {
$FolderParent="$ENV:SYSTEMROOT\SystemApps"
} # End of IF
} # End of IF
IF (!$Printers) {
IF ($WinBuild.Equals(14393)) {
$Printers="Fax"
} # End of IF
        IF ($WinBuild.Equals(15063)) {
$Printers="Fax"
} # End of IF
IF ($WinBuild.Equals(16299)) {
$Printers="Fax"
} # End of IF
} # End of IF
} # End of BEGIN

Process{
ForEach ($Process in $ProcessList) {
Stop-Process -Name $Process -Force -ErrorAction SilentlyContinue
}
ForEach ($App in $AppsList) {
$PackageFullName = (Get-AppxPackage $App).PackageFullName
$ProPackageFullName = (Get-AppxProvisionedPackage -online | where {$_.Displayname -eq $App}).PackageName
Write-Verbose "$PackageFullName"
Write-Verbose "$ProPackageFullName"
if ($PackageFullName)
{
Write-Verbose "Removing Package: $App"
remove-AppxPackage -package $PackageFullName
} else {
Write-Verbose "Unable to find package: $App"
}
if ($ProPackageFullName) {
Write-Verbose "Removing Provisioned Package: $ProPackageFullName"
Remove-AppxProvisionedPackage -online -packagename $ProPackageFullName -ErrorAction SilentlyContinue
} else {
Write-Verbose "Unable to find provisioned package: $App"
}
}

foreach ($FolderAlias in $FoldersToRename) {
$FolderPath=Get-ChildItem "$FolderParent\$FolderAlias*" -ErrorAction SilentlyContinue
IF ($FolderPath) {
IF (Test-Path $FolderPath -ErrorAction SilentlyContinue) {
$FolderLeaf=Split-Path $FolderPath -Leaf
Write-Verbose "Moving $FolderParent\$FolderLeaf ==> $FolderParent\_DISABLED_$FolderLeaf"
Move-Item "$FolderParent\$FolderLeaf" "$FolderParent\_DISABLED_$FolderLeaf"
} # End of IF
} # End of IF
} # End of foreach

# Disable Windows features
IF ($DISMFeature) {
foreach ($Feature in $DISMFeature) {
Write-Verbose "dism /online /disable-feature /featurename:$Feature /quiet /norestart"
dism /online /disable-feature /featurename:$Feature /quiet /norestart
} # End of ForEach
} # End of IF

# Disable Windows Capabilities
IF ($DISMCapability) {
foreach ($Capability in $DISMCapability) {
Write-Verbose "DISM /Online /Remove-Capability /CapabilityName:$Capability /quiet /norestart"
DISM /Online /Remove-Capability /CapabilityName:$Capability /quiet /norestart
} # End of ForEach
} # End of IF

# Disable Windows Capabilities
IF ($WindowsOptionalFeatures ) {
foreach ($OptionalFeature in $WindowsOptionalFeatures ) {
Write-Verbose "Disable-WindowsOptionalFeature -Online -FeatureName $OptionalFeature -norestart"
Disable-WindowsOptionalFeature -Online -FeatureName $OptionalFeature -norestart
} # End of ForEach
} # End of IF

# Remove unwanted printers
IF ($Printers) {
foreach ($Printer in $Printers) {
Write-Verbose "Remove-Printer $Printer"
Remove-Printer $Printer -ErrorAction SilentlyContinue
} # End of ForEach
} # End of IF

} #End of Process

End{
#Add something to check to see if it has run
#$year=(Get-Date).Year
#$month=(Get-Date).Month
#$day=(Get-Date).Day
#$date="$year.$month.$day"
#$hour=(get-date).Hour
#$minute=(get-date).Minute
#$time=get-date -Format T
#$displayversion="$date.$hour$minute"
#$regversion="$date.$hour$minute"
$regversion="2017.11.14.1427" #Update this when things have changed in the script, also update the GPO item level targeting

if( -not (Test-Path -Path "hklm:\SOFTWARE\LiU" -PathType Container) )
{
New-Item -Path "hklm:\SOFTWARE" -Name "LiU" | Out-Null
}

New-ItemProperty -Path "hklm:\SOFTWARE\LiU" -Name "RemovedUnwantedApps" -Type String -Value $regversion -Force | Out-Null
}# End of End
=== SCRIPT ENDS HERE ===

2017-11-13

Add/remove features and patches/language packs to offline Windows 10 WIM from ISO file with Powershell

I could not find a script that mounts an official MS Windows 10 ISO, adds .NET 3.5 and patches automatically. So.. I made one myself.

Please share any improvements and run at your own risk! 

#=== SCRIPT BEGINS ===
<#
.SYNOPSIS
Mount an ISO, enable and or disable features and patches to WIM and save.
.DESCRIPTION
Mount ISO, copy install.wim, mount the file and enable/disable Windows features and patches, and save the changes.
.NOTES
David Djerf, 2017-10-11 - First version that only adds .NET 3.5
David Djerf, 2017-10-13 - Added automatic mounted drive letter detection, made it possible to add and remove custom Windows features, and patches.
David Djerf, 2017-10-16 - Added support for patchfolders
David Djerf, 2017-12-01 - Added export index to new file
David Djerf, 2017-12-08 - Fixed Verbose message for patches not writing name of patches
    David Djerf, 2018-04-03 - Added detection for WIM or ESD install-file.

.PARAMETER ISOfile
Full path to Windows 10 ISO file
.PARAMETER WIMIndex
Index number to modify, eg. 1
You can list the index using the DISM command: dism /get-wiminfo /wimfile:<PATH_TO_WIM>
.PARAMETER NewFileName
Your_new_file.wim
Will default to install_modified.wim if not specified.
.PARAMETER WorkFolder
Folder where the magic happens...
If not specified C:\WorkFolder will be used
.PARAMETER Patches
Full path to windows patches in cab or msu format, multiple files accepted if comma separated.
eg. "c:\patches\KB12345.msu","c:\patches\KB654321.msu"
.PARAMETER Patchfolder
Path to a folder containing windows patches in cab or msu format. All patches will be applied.
eg. c:\patches
.PARAMETER WIMAddFeatures
Add Windows features based on their proper "Featurenames"
.PARAMETER WIMRemoveFeatures
Remove Windows features based on their proper "Featurenames" (get features with Dism /Image:C:\workfolder /Get-Features)
.PARAMETER ExportIndexFile
Will export the define index to a new file, provide the full file path.
.PARAMETER Verbose
Add -Verbose to get information about what step the script is running.
.EXAMPLE
modify-windows_wim.ps1 -ISOfile C:\Users\admin\Downloads\SW_DVD5_Win_Pro_Ent_Edu_N_10_1709_64BIT_English_MLF_X21-50143.ISO -WIMIndex 1 -Workfolder C:\mountfolder -NewFileName my_new_install.wim -WIMAddFeatures "NetFx3" -WIMRemoveFeatures "SMB1Protocol" -Patches "C:\Patches\KB123456.msu","C:\Patches\KB654321.msu" -Verbose
modify-windows_wim.ps1 -ISOfile C:\Users\admin\Downloads\SW_DVD5_Win_Pro_Ent_Edu_N_10_1709_64BIT_English_MLF_X21-50143.ISO -WIMIndex 1 -Workfolder C:\mountfolder -NewFileName my_new_install.wim -WIMAddFeatures "NetFx3" -WIMRemoveFeatures "SMB1Protocol" -Patchfolder "C:\Patches" -Verbose

#>
#Requires -Version 4.0
#Requires -RunAsAdministrator

[CmdletBinding(SupportsShouldProcess=$True)]
Param(
        [Parameter(Mandatory=$True,Position=0)]$ISOfile,
        [Parameter(Mandatory=$True,Position=1)]$WIMIndex,
        [Parameter(Mandatory=$False,Position=3)]$NewFileName,
        [Parameter(Mandatory=$False,Position=4)]$Workfolder,
        [string[]]$WIMAddFeatures,
        [string[]]$WIMRemoveFeatures,
        [string[]]$Patches,
        [string]$Patchfolder,
[string]$ExportIndexFile
    )

Begin{
IF (!$WorkFolder) { 
$WorkFolder = "C:\WorkFolder"
} # End of IF
IF (!$NewFileName) { 
$NewFileName = "install_modified.wim"
} # End of IF

# Make sure patchfolder is readable
IF ($Patchfolder) {
IF (Test-Path -path "$Patchfolder") {
Write-Verbose "'$Patchfolder' could be read"
} else { Write-Verbose "'$Patchfolder' could not be read, aborting!"
Write-Verbose "Make sure you the path is correct and can be read."
Break
} # End of IF
}

# Mount ISO if exist
IF (Test-Path -path "$ISOfile") {
Write-Verbose "'$ISOfile' could be read, Mounting ISO to $MountDrive"
$MountDrive=(Mount-Diskimage $ISOfile -PassThru | Get-Volume).DriveLetter+":"
} else { Write-Verbose "'$ISOfile' could not be read, aborting!"
Write-Verbose "Make sure you the path is correct and can be read."
Break
} # End of IF

# Failsafe, make sure wim or esd exists
IF (Test-Path -path "$MountDrive\Sources\install.wim") {
Write-Verbose "'$MountDrive\Sources\install.wim' exists, engage!"
        $InstallFile="$MountDrive\Sources\install.wim"
} elseif (Test-Path -path "$MountDrive\Sources\install.esd") {
Write-Verbose "'$MountDrive\Sources\install.esd' exists, engage!"
        $InstallFile="$MountDrive\Sources\install.esd"
} else { 
Write-Verbose "'$MountDrive\Sources\install.wim' or '$MountDrive\Sources\install.esd' does not exist, aborting!"
Write-Verbose "Make sure you specified the correct drive letter for the new virtual drive."
Dismount-Diskimage $ISOfile
Break
} # End of IF

    # Copy install-file to newfile in temp folder (to make sure the file is not on a network share that may be disconnected)
IF (Test-Path -path "$ENV:TEMP\$NewFileName") {
Write-Verbose "$NewFileName exists, make it writable"
Set-ItemProperty "$ENV:TEMP\$NewFileName" -name IsReadOnly -value $false
} else {
Write-Verbose "Copy $InstallFile to %TEMP%\$NewFileName"
Copy-Item $InstallFile -Destination "$ENV:TEMP\$NewFileName" -force
Write-Verbose "Make $NewFileName writable"
Set-ItemProperty "$ENV:TEMP\$NewFileName" -name IsReadOnly -value $false
} # End of IF

# Make sure $Workfolder exists and is empty
IF (Test-Path -path "$WorkFolder") {
Write-Verbose "$WorkFolder exists, discarding content"
Dismount-WindowsImage -Path $WorkFolder -Discard
Write-Verbose "$WorkFolder exists, removing folder"
Remove-Item -Recurse $WorkFolder -force
Write-Verbose "Create $WorkFolder"
New-Item -ItemType directory -Path $WorkFolder -force | Out-Null
} else {
Write-Verbose "Create $WorkFolder"
New-Item -ItemType directory -Path $WorkFolder -force | Out-Null
} # End of IF
Write-Verbose "Mount $NewFileName to $WorkFolder"
Mount-WindowsImage -ImagePath "$ENV:TEMP\$NewFileName" -Index $WIMIndex -Path $WorkFolder -Optimize
} # End of BEGIN

Process{
# Add Features if specified
IF ($WIMAddFeatures) { 
ForEach ($WIMAddFeature in $WIMAddFeatures) {
Write-Verbose "Adding $WIMAddFeature to $NewFileName"
dism /image:$WorkFolder /enable-feature /featurename:$WIMAddFeature /all /source:$MountDrive\sources\sxs
} # End of ForEach
} else {
Write-Verbose "No feature to add..."
}# End of IF

# Remove Features if specified
IF ($WIMRemoveFeatures) { 
ForEach ($WIMRemoveFeature in $WIMRemoveFeatures) {
Write-Verbose "Removing $WIMRemoveFeature to $NewFileName"
dism /image:$WorkFolder /disable-feature /featurename:$WIMRemoveFeature
} # End of ForEach
} else {
Write-Verbose "No feature to remove..."
}# End of IF

# Apply Windows patches if specified
IF ($Patches) {
$Patch=""
ForEach ($Patch in $Patches) {
Write-Verbose "Applying $Patch to $NewFileName"
dism /Image:$WorkFolder /add-package /packagepath:$Patch
} # End of ForEach
} else {
Write-Verbose "No patch to apply..."
}# End of IF

IF ($Patchfolder) {
$Patch=""
$Patches=Get-ChildItem $Patchfolder -Filter *.msu | % { $_.FullName }
ForEach ($Patch in $Patches) {
Write-Verbose "Applying $Patch to $NewFileName"
dism /Image:$WorkFolder /add-package /packagepath:$Patch
} # End of ForEach
} else {
Write-Verbose "No patch to apply..."
}# End of IF
} #End of Process

End{
Write-Verbose "Dismount ISO"
Dismount-Diskimage $ISOfile
Write-Verbose "Saving changes to $ENV:TEMP\$NewFileName"
Dismount-WindowsImage -Path $WorkFolder -Save -CheckIntegrity
IF ($ExportIndexFile) {
Write-Verbose "Exporting Index $WIMIndex to $ExportIndexFile"
Export-WindowsImage -SourceImagePath "$ENV:TEMP\$NewFileName" -SourceIndex $WIMIndex -DestinationImagePath $ExportIndexFile
} # End of if
Write-Verbose "Removing $WorkFolder"
Remove-Item -Recurse $WorkFolder -force
Write-Verbose "Finished saving $ENV:TEMP\$NewFileName"
Write-Verbose "You can now test your new file!"
}# End of End
#=== SCRIPT ENDS ===

2017-05-12

Cleanup All users desktop and Start menu

Problem to solve

Clean all shortcuts on the all users desktop (because users cannot delete them) and also remove unwanted shortcuts from the Start menu such as "Uninstall"-shortcuts.

I run this on our student computers in computer rooms where the Onedrive client cannot be used, instead we mount Onedrive as a mapped folder instead using OneDriveMapper.
We also want to remove unnecessary shortcuts for Java & Silverlight.

Description of the script

The script is based on several functions that could be used to remove files and folers other than the desktop and start menu shortcuts.

Remove-UnwantedFolders - Remove folders in a path
Remove-UnwantedFiles - Remove files in a path
Remove-UninstallLinks - Searches for and removes any shortcuts that begins with "Uninstall" in a path
Remove-EmptyFolders - Removes any empty folders in a path
Remove-FiletypeFromFolder - Removes all files of defined type from a path

Read the script comments for more details.
Change the files/folders/file types in the end of the script to adapt to your needs.

The script

=== SCRIPT Cleanup-Desktop_and_StartMenu.ps1 BEGINS ===
# David Djerf 2017-05-11

Function Remove-UnwantedFolders {
    <#
    .SYNOPSIS
        Removes unwanted folders in a path
    .DESCRIPTION
        Will remove one or more folders from a path, for example use this to remove unwanted folders in the start menu.
    .NOTES
   davpe67 2017-02-18
    .PARAMETER WorkFolder
   Define a path where script should seach recursive for the folders to remove
   For example: "C:\" or "$ENV:Temp"
    .PARAMETER SearchStartMenu
        Will Set Workfolder to "$ENV:Programdata\Microsoft\Windows\Start Menu"
    .PARAMETER UnwantedFolders
        Specify one or more folders you want to remove.
        For example: "Java","Silverlight"
    .EXAMPLE
   Remove-UnwantedFolders -SearchStartMenu -UnwantedFolders "Java","Unique Admin Generator" -WhatIf
    #>

    [cmdletbinding(SupportsShouldProcess=$True,DefaultParameterSetName="WorkFolder")]
    Param(
        [Parameter(Mandatory=$True, ParameterSetName="WorkFolder", Position=0, HelpMessage='Specify folder(s) serch in: ["C:\path\to\folders"]')]
        [string]$WorkFolder,
        [Parameter(Mandatory=$True, ParameterSetName="SearchStartMenu", Position=0)]
        [switch]$SearchStartMenu,
        [Parameter(Mandatory=$True, HelpMessage='Specify folder(s) to remove: ["Folder1","Folder"]')]
        [string[]]$UnwantedFolders
    )
    BEGIN { 
        Write-Verbose "Begin..."       
        IF ($SearchStartMenu) {
$IsAdmin=Test-Admin
Write-Verbose "`$IsAdmin is $IsAdmin"
IF ($IsAdmin -like $false) { Write-Error "Script need to run as administrator" ; Break }
            $WorkFolder = Resolve-Path $("$ENV:ProgramData\Microsoft\Windows\Start Menu") 
        } ELSE {
            $WorkFolder = Resolve-Path "$WorkFolder"
        } # End of IF ELSE
        Write-Verbose "`$WorkFolder=`"$WorkFolder`""
        Write-Verbose "`$UnwantedFolders=`"$UnwantedFolders`""
    } # End of Begin
    PROCESS {
        # Remove Unwanted Folders in Start Menu
        IF ($UnwantedFolders -and $WorkFolder) { # Only run if not empty
            Write-Verbose "Processing..."
            IF ($UnwantedFolders) {
                ForEach ($UnwantedFolder in $UnwantedFolders) {
                     Get-ChildItem -Recurse -Path $WorkFolder -Directory | Where-Object {$_.Name -like "$UnwantedFolder" } | Resolve-Path | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
                } # End of ForEach
            } # End of IF
        } # End of IF
    } # End of Process

} # End of function Remove-UnwantedFolders

Function Remove-UnwantedFiles {
    <#
    .SYNOPSIS
        Removes unwanted files in a path
    .DESCRIPTION
        Will remove one or more files from a path, for example use this to remove unwanted files in the start menu.
    .NOTES
   davpe67 2017-03-03
    .PARAMETER WorkFolder
   Define a path where script should seach recursive for the folders to remove
   For example: "C:\" or "$ENV:Temp"
    .PARAMETER SearchStartMenu
        Will Set Workfolder to "$ENV:Programdata\Microsoft\Windows\Start Menu"
    .PARAMETER UnwantedFiles
        Specify one or more folders you want to remove.
        For example: "Onedrive","Onedrive for Business"
    .EXAMPLE
   Remove-UnwantedFiles -SearchStartMenu -UnwantedFiles "Onedrive","Onedrive for Business" -WhatIf
    #>

    [cmdletbinding(SupportsShouldProcess=$True,DefaultParameterSetName="WorkFolder")]
    Param(
        [Parameter(Mandatory=$True, ParameterSetName="WorkFolder", Position=0, HelpMessage='Specify folder(s) serch in: ["C:\path\to\folders"]')]
        [string]$WorkFolder,
        [Parameter(Mandatory=$True, ParameterSetName="SearchStartMenu", Position=0)]
        [switch]$SearchStartMenu,
        [Parameter(Mandatory=$True, HelpMessage='Specify files(s) to remove: ["File1","File"]')]
        [string[]]$UnwantedFiles
    )
    BEGIN { 
        Write-Verbose "Begin..."       
        IF ($SearchStartMenu) {
$IsAdmin=Test-Admin
Write-Verbose "`$IsAdmin is $IsAdmin"
IF ($IsAdmin -like $false) { Write-Error "Script need to run as administrator" ; Break }
            $WorkFolder = Resolve-Path $("$ENV:ProgramData\Microsoft\Windows\Start Menu") 
        } ELSE {
            $WorkFolder = Resolve-Path "$WorkFolder"
        } # End of IF ELSE
        Write-Verbose "`$WorkFolder=`"$WorkFolder`""
        Write-Verbose "`$UnwantedFiles=`"$UnwantedFiles`""
    } # End of Begin
    PROCESS {
        # Remove Unwanted Folders in Start Menu
        IF ($UnwantedFiles -and $WorkFolder) { # Only run if not empty
            Write-Verbose "Processing..."
            IF ($UnwantedFiles) {
                ForEach ($UnwantedFile in $UnwantedFiles) {
                     Get-ChildItem -Recurse -Path $WorkFolder -File | Where-Object {$_.Name -like "$UnwantedFile" } | Resolve-Path | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
                } # End of ForEach
            } # End of IF
        } # End of IF
    } # End of Process

} # End of function Remove-UnwantedFiles


function Test-Admin {
<#
.SYNOPSIS
Test if script is run as administrator
.DESCRIPTION
Use to check if script is run with administrator privilegues or not.
Will return true or false.
.NOTES
Downloaded from: http://www.powertheshell.com/testadmin/
.EXAMPLE
Test-Admin
#>
  $wid = [System.Security.Principal.WindowsIdentity]::GetCurrent()
  $prp = New-Object System.Security.Principal.WindowsPrincipal($wid)
  $adm = [System.Security.Principal.WindowsBuiltInRole]::Administrator
  $prp.IsInRole($adm)  
}

Function Remove-UninstallLinks {
    <#
    .SYNOPSIS
        Removes unwanted Uninstall links in a path
    .DESCRIPTION
        Will remove links that begin with "uninstall" from a path, 
        for example use this to remove unwanted uninstall-links in the start menu.
    .NOTES
   davpe67 2017-02-18
    .PARAMETER WorkFolder
   Define a path where script should seach recursive for the folders to remove
   For example: "C:\" or "$ENV:Temp"
    .PARAMETER SearchStartMenu
        Will Set Workfolder to "$ENV:Programdata\Microsoft\Windows\Start Menu"
    .EXAMPLE
   Remove-UnwantedFolders -SearchStartMenu -WhatIf
    #>

    [cmdletbinding(SupportsShouldProcess=$True,DefaultParameterSetName="WorkFolder")]
    Param(
        [Parameter(Mandatory=$True, ParameterSetName="WorkFolder", Position=0, HelpMessage='Specify folder(s) serch in: ["C:\path\to\folders"]')]
        [string]$WorkFolder,
        [Parameter(Mandatory=$True, ParameterSetName="SearchStartMenu", Position=0)]
        [switch]$SearchStartMenu
    )
    BEGIN {
        Write-Verbose "Begin..."       
        IF ($SearchStartMenu) {
$IsAdmin=Test-Admin
Write-Verbose "`$IsAdmin is $IsAdmin"
IF ($IsAdmin -like $false) { Write-Error "Script need to run as administrator" ; Break }
            $WorkFolder = Resolve-Path $("$ENV:ProgramData\Microsoft\Windows\Start Menu") 
        } ELSE {
            $WorkFolder = Resolve-Path "$WorkFolder"
        } # End of IF ELSE
        Write-Verbose "`$WorkFolder=`"$WorkFolder`""
    } # End of Begin
    PROCESS {
        Write-Verbose "Processing..."
        # Remove all Uninstall-links
        $UninstallLinks=Get-ChildItem -Recurse -Path $WorkFolder -File "*.lnk" | Where-Object {$_.Name -like "Uninstall*" } | Resolve-Path
        $UninstallLinks | ForEach-Object { 
            Write-Verbose "Removing `"$_`""
            Remove-Item "$_" -Recurse -Force -ErrorAction SilentlyContinue
            }
    } # End of Process
} # End of Remove-UninstallLinks

Function Remove-EmptyFolders {
     <#
    .SYNOPSIS
        Removes empty folders in a path.
    .DESCRIPTION
        Will remove empty folders in a path.
        Script will loop x times to ensure folders that became empty, default number is 10.
    .NOTES
   davpe67 2017-02-18
    .PARAMETER WorkFolder
   Define a path where script should seach recursive for the folders to remove
   For example: "C:\" or "$ENV:Temp"
    .PARAMETER SearchStartMenu
        Will Set Workfolder to "$ENV:Programdata\Microsoft\Windows\Start Menu"
    .PARAMETER KeepFolders
        Will not remove folders with these names.
        For example: "StartUp","Maintenance"
    .PARAMETER Looptimes
        Script will loop this many times to remove new empty folders from previous runs.
    .EXAMPLE
   Remove-EmptyFolders -SearchStartMenu -Looptimes 10 -WhatIf
    #>
    [cmdletbinding(SupportsShouldProcess=$True,DefaultParameterSetName="WorkFolder")]
    Param(
        [Parameter(Mandatory=$True, ParameterSetName="WorkFolder", Position=0, HelpMessage='Specify folder(s) serch in: ["C:\path\to\folders"]')]
        [string]$WorkFolder,
        [Parameter(Mandatory=$True, ParameterSetName="SearchStartMenu", Position=0)]
        [switch]$SearchStartMenu,
        [int]$Looptimes,
        [string[]]$KeepFolders
    )
    BEGIN {
        Write-Verbose "Begin..."
        IF (!$Looptimes) { $Looptimes=10 }
        IF ($SearchStartMenu) {
$IsAdmin=Test-Admin
Write-Verbose "`$IsAdmin is $IsAdmin"
IF ($IsAdmin -like $false) { Write-Error "Script need to run as administrator" ; Break }
            $WorkFolder = Resolve-Path $("$ENV:ProgramData\Microsoft\Windows\Start Menu") 
        } ELSE {
            $WorkFolder = Resolve-Path "$WorkFolder"
        } # End of IF ELSE
        Write-Verbose "`$WorkFolder=`"$WorkFolder`""
    } # End of Begin
    PROCESS {
        Write-Verbose "Processing..."
        # Remove all folders with no links
        $LoopCounter=0
        do {
            Write-Verbose "Loop number $LoopCounter of $Looptimes"
            $EmptyFolders = Get-ChildItem $WorkFolder -Directory -Recurse -Force -Exclude $KeepFolders | Where-Object { (Get-ChildItem $_.fullName).count -eq 0 } | Select-Object -ExpandProperty FullName
            $EmptyFolders | ForEach-Object { Write-Verbose "Removing `"$_`"" }
            $EmptyFolders | ForEach-Object { Remove-Item "$_" -Recurse -Force -ErrorAction Continue }
            $LoopCounter=$LoopCounter+1
        } while ($EmptyFolders.count -gt 0 -and $LoopCounter -le $Looptimes)
    } # End of Process
} # End of Remove-EmptyFolders

Function Remove-FiletypeFromFolder {
    <#
    .SYNOPSIS
        Removes files of defined type in a path
    .DESCRIPTION
        Will remove all files of defined file type in a path and subfolders, 
        could be used to remove all links from the common desktop.
        Use the -whatif to see what files that will be removed.
    .NOTES
   davpe67 2017-02-25
davpe67 2017-05-11 Added support for exclution of files
    .PARAMETER WorkFolder
   Define a path where script should seach recursive for the folders to remove
   For example: "C:\" or "$ENV:Temp"
    .PARAMETER SearchStartMenu
        Will Set Workfolder to "$ENV:Programdata\Microsoft\Windows\Start Menu"
.PARAMETER SearchPublicDesktop
        Will Set Workfolder to "$ENV:PUBLIC\Desktop"
    .PARAMETER KeepFiles
        Define files that should not be deleted
    .EXAMPLE
   Remove-FiletypeFromFolder -SearchPublicDesktop -FileTypes ".lnk" -WhatIf

        Remove-FiletypeFromFolder -SearchPublicDesktop -FileTypes ".lnk","*.tmp" -Verbose
    #>

    [cmdletbinding(SupportsShouldProcess=$True,DefaultParameterSetName="WorkFolder")]
    Param(
        [Parameter(Mandatory=$True, ParameterSetName="WorkFolder", Position=0, HelpMessage='Specify folder(s) serch in: ["C:\path\to\folders"]')]
        [string]$WorkFolder,
        [Parameter(Mandatory=$True, ParameterSetName="SearchStartMenu", Position=0)]
        [switch]$SearchStartMenu,
        [Parameter(Mandatory=$True, ParameterSetName="SearchPublicDesktop", Position=0)]
        [switch]$SearchPublicDesktop,
        [Parameter(Mandatory=$True, HelpMessage='Specify filetypes to search for: [".lnk",".tmp"]')]
        [string[]]$FileTypes,
        [string[]]$KeepFiles
    )
    BEGIN {
        Write-Verbose "Begin..."
        $IsAdmin=Test-Admin
Write-Verbose "`$IsAdmin is $IsAdmin"
        IF ($SearchStartMenu) {
IF ($IsAdmin -like $false) { Write-Error "Script need to run as administrator" ; Break }
            $WorkFolder = Resolve-Path $("$ENV:ProgramData\Microsoft\Windows\Start Menu") 
        } ELSEIF ($SearchPublicDesktop) {
            IF ($IsAdmin -like $false) { Write-Error "Script need to run as administrator" ; Break }
            $WorkFolder = Resolve-Path $("$ENV:PUBLIC\Desktop") 
        } ELSE {
            $WorkFolder = Resolve-Path "$WorkFolder"
        } # End of IF ELSE
        Write-Verbose "`$WorkFolder=`"$WorkFolder`""
    } # End of Begin
    PROCESS {
        Write-Verbose "Processing..."
        # Search and remove
        foreach ($FileType in $FileTypes) { 
            $FilesToRemove=Get-ChildItem $WorkFolder -Force -Exclude $KeepFiles | Resolve-Path
            $FilesToRemove | ForEach-Object { 
                Write-Verbose "Removing `"$_`""
                Remove-Item "$_" -Recurse -Force
                }
            } # End of foreach
    } # End of Process
} # End of Remove-FiletypeFromFolder

Remove-FiletypeFromFolder -SearchPublicDesktop -FileTypes "*.lnk" -KeepFiles "Mount Onedrive for Business.lnk"
Remove-UnwantedFolders -SearchStartMenu -UnwantedFolders "Java","Microsoft Silverlight"
Remove-UnwantedFiles -SearchStartMenu -UnwantedFiles "Onedrive*.lnk"
Remove-UninstallLinks -SearchStartMenu
Remove-EmptyFolders -SearchStartMenu -KeepFolders "StartUp","Maintenance"
=== SCRIPT Cleanup-Desktop_and_StartMenu.ps1 ENDS ===

How it runs

The script is run by a scheduled task that triggers on computer startup, user logon, and every hour and on the creation of the scheduled job. This is to make sure that any shortcuts that may have been added by software installation have been removed.