Comment modifier les paramètres d’économiseur d’écran avec PowerShell

Le contrôle des stratégies d’économiseur d’écran est un élément essentiel de la gestion des terminaux, en particulier dans les environnements soumis à des exigences strictes en matière de conformité ou d’image de marque. Qu’il s’agisse de renforcer la sécurité de l’écran de verrouillage ou de configurer un économiseur d’écran spécifique pour tous les profils d’utilisateurs, l’automatisation centralisée peut faire gagner beaucoup de temps et d’efforts aux professionnels de l’informatique et aux fournisseurs de services gérés (MSP). Cet article explore un puissant script PowerShell conçu pour modifier les paramètres de l’économiseur d’écran sur Windows avec PowerShell, offrant un contrôle fin à grande échelle.

Contexte

Pour les fournisseurs de services gérés et les administrateurs informatiques, s’assurer que les paramètres de l’économiseur d’écran sont conformes aux politiques de sécurité n’est pas qu’une question d’apparence. Les cadres réglementaires tels que HIPAA et PCI-DSS exigent souvent que les sessions inactives soient verrouillées après une durée déterminée. L’application manuelle de ces configurations à tous les profils est à la fois source d’erreurs et inefficace. La stratégie de groupe peut être utile, mais elle n’est pas toujours disponible ou souhaitée, en particulier dans les environnements mixtes ou lors de la gestion de terminaux non reliés à un domaine. Ce script PowerShell comble cette lacune en modifiant directement le registre Windows pour chaque profil d’utilisateur, fournissant un script PowerShell pour modifier les paramètres d’économiseur d’écran pour tous les utilisateurs d’une machine.

Le script

#Requires -Version 5.1

<#
.SYNOPSIS
    Change the screensaver settings for all user profiles on a Windows machine.
.DESCRIPTION
    Change the screensaver settings for all user profiles on a Windows machine.
By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use.
    Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. 
    Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. 
    Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. 
    Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. 
    Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. 
    Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. 
    EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).

.PARAMETER SelectScreensaver "Bubbles"
    Select the screensaver to apply. If 'None' is selected, the screensaver will be disabled. Options include: None, 3D Text, Blank, Bubbles, Photos, Mystify, Ribbons

.PARAMETER WaitTime
    Specify the wait time in minutes before the screensaver activates. Valid values range from 1 to 9999 minutes. If no value is provided, the default wait time of 15 minutes will be used

.PARAMETER OnResumeEnterPassword
    Choose whether to require the user to log in on resume.

.PARAMETER PhotosDirectoryPath
    Enter the path to the directory containing the images for the 'Photos' screensaver. This must be a local path on the device. If 'Photos' is selected but this is not filled out, the default of each user's Pictures folder will be used.

.PARAMETER TextFor3DText
    Enter the text for the 3D Text screensaver. Only required if 3D Text is selected. Maximum length is 20 characters.

.PARAMETER ForceReboot
    Reboot the device after applying the screensaver settings. This may be needed for all changes to take effect. If not selected, the user may need to log off or restart their computer.
    
.EXAMPLE
    -SelectScreensaver "Ribbons" -WaitTime 10 -OnResumeEnterPassword:$False
    [Info] Working on 'tuser1' profile...

    [Info] Setting screensaver to 'Ribbons'.
    Registry::HKEY_USERS\S-1-5-21-21640437-1735840450-3283185940-1104\Control Panel\Desktop\SCRNSAVE.EXE changed from C:\Windows\System32\Mystify.scr to C:\Windows\System32\Ribbons.scr

    [Info] Setting screensaver timeout to 10 minute(s).
    Registry::HKEY_USERS\S-1-5-21-21640437-1735840450-3283185940-1104\Control Panel\Desktop\ScreenSaveTimeOut changed from 900 to 600

    [Info] Setting screensaver password requirement to False.
    Registry::HKEY_USERS\S-1-5-21-21640437-1735840450-3283185940-1104\Control Panel\Desktop\ScreenSaverIsSecure changed from 1 to 0

    [Info] Finished working on 'tuser1' profile.

.EXAMPLE
    -SelectScreensaver "Mystify" -WaitTime 1 -OnResumeEnterPassword:$True
    [Info] Working on 'tuser1' profile...

    [Info] Setting screensaver to 'Mystify'.
    Registry::HKEY_USERS\S-1-5-21-21640437-1735840450-3283185940-1104\Control Panel\Desktop\SCRNSAVE.EXE changed from C:\Windows\System32\Ribbons.scr to C:\Windows\System32\Mystify.scr

    [Info] Setting screensaver timeout to 1 minute(s).
    Registry::HKEY_USERS\S-1-5-21-21640437-1735840450-3283185940-1104\Control Panel\Desktop\ScreenSaveTimeOut changed from 900 to 60

    [Info] Setting screensaver password requirement to True.
    Registry::HKEY_USERS\S-1-5-21-21640437-1735840450-3283185940-1104\Control Panel\Desktop\ScreenSaverIsSecure changed from 0 to 1

    [Info] Finished working on 'tuser1' profile.

.EXAMPLE
    -SelectScreensaver "3D Text" -WaitTime 5 -OnResumeEnterPassword:$False -TextFor3DText "Hello World"
    [Info] Working on 'tuser1' profile...

    [Info] Setting screensaver to '3D Text'.
    Registry::HKEY_USERS\S-1-5-21-21640437-1735840450-3283185940-1104\Control Panel\Desktop\SCRNSAVE.EXE changed from C:\Windows\System32\Mystify.scr to C:\Windows\System32\ssText3d.scr

    [Info] Setting screensaver timeout to 5 minute(s).
    Registry::HKEY_USERS\S-1-5-21-21640437-1735840450-3283185940-1104\Control Panel\Desktop\ScreenSaveTimeOut changed from 60 to 300

    [Info] Setting screensaver password requirement to False.
    Registry::HKEY_USERS\S-1-5-21-21640437-1735840450-3283185940-1104\Control Panel\Desktop\ScreenSaverIsSecure changed from 1 to 0

    [Info] Setting 3D Text to 'Hello World'.
    Registry::HKEY_USERS\S-1-5-21-21640437-1735840450-3283185940-1104\Software\Microsoft\Windows\CurrentVersion\Screensavers\ssText3d\DisplayString changed from newvalue23 to Hello World

    [Info] Finished working on 'tuser1' profile.

.NOTES
    Minimum OS Architecture Supported: Windows 10, Windows Server 2016
    Release Notes: Initial Release
#>

[CmdletBinding()]
param (
    [Parameter()]
    [string]$SelectScreensaver,

    [Parameter()]
    [int]$WaitTime,

    [Parameter()]
    [switch]$OnResumeEnterPassword = [System.Convert]::ToBoolean($env:onResumeEnterPassword),

    [Parameter()]
    [string]$TextFor3DText,

    [Parameter()]
    [string]$PhotosDirectoryPath,
    
    [Parameter()]
    [switch]$ForceReboot = [System.Convert]::ToBoolean($env:forceReboot)
)

begin {
    function Test-IsElevated {
        [CmdletBinding()]
        param ()
        
        # Get the current Windows identity of the user running the script
        $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
        
        # Create a WindowsPrincipal object based on the current identity
        $p = New-Object System.Security.Principal.WindowsPrincipal($id)
        
        # Check if the current user is in the Administrator role
        # The function returns $True if the user has administrative privileges, $False otherwise
        # 544 is the value for the Built In Administrators role
        # Reference: https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsbuiltinrole
        $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]'544')
    }

    function Set-RegKey {
        [CmdletBinding()]
        param (
            [Parameter()]
            [String]$Path,
            [Parameter()]
            [String]$Name,
            [Parameter()]
            $Value,
            [Parameter()]
            [ValidateSet("DWord", "QWord", "String", "ExpandedString", "Binary", "MultiString", "Unknown")]
            [String]$PropertyType = "DWord"
        )
    
        if ([String]::IsNullOrWhiteSpace($Path)) {
            throw (New-Object System.ArgumentNullException("You must provide a valid path to a registry key."))
        }
    
        if ($Path -notmatch "^(Registry::|HKCR:\\|HKCU:\\|HKLM:\\|HKU:\\|HKCC:\\|TestRegistry:\\)") {
            throw (New-Object System.ArgumentException("The path provided '$Path' is invalid as it does not start with 'Registry::', 'HKCR:\', 'HKCU:\', 'HKLM:\', 'HKU:\' or 'HKCC:\'."))
        }
    
        if ($Path -match "^Registry::" -and $Path -notmatch "^Registry::(HKCR\\|HKEY_CLASSES_ROOT\\|HKCU\\|HKEY_CURRENT_USER\\|HKLM\\|HKEY_LOCAL_MACHINE|HKU\\|HKEY_USERS|HKCC\\|HKEY_CURRENT_CONFIG)") {
            throw (New-Object System.ArgumentException("The path provided '$Path' is invalid as it does not start with a valid registry root such as 'HKLM\' or 'HKEY_LOCAL_MACHINE\'."))
        }
    
        if ([String]::IsNullOrWhiteSpace($Name)) {
            throw (New-Object System.ArgumentNullException("You must provide a valid name for the registry property."))
        }
    
        # Check if the specified registry path exists
        if (!(Test-Path -Path $Path)) {
            try {
                # If the path does not exist, create it
                New-Item -Path $Path -Force -ErrorAction Stop | Out-Null
            }
            catch {
                throw $_
            }
        }
    
        # Check if the registry key already exists at the specified path
        if (Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue) {
            # Retrieve the current value of the registry key
            $CurrentValue = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name
            if ($CurrentValue -eq $Value) {
                Write-Host "$Path\$Name is already the value '$Value'."
            }
            else {
                try {
                    # Update the registry key with the new value
                    Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -Confirm:$false -ErrorAction Stop | Out-Null
                }
                catch {
                    throw $_
                }
    
                # Output the change made to the registry key
                Write-Host "$Path\$Name changed from $CurrentValue to $((Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name)"
            }
    
            return
        }
    
        try {
            # If the registry key does not exist, create it with the specified value and property type
            New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $PropertyType -Force -Confirm:$false -ErrorAction Stop | Out-Null
        }
        catch {
            throw $_
        }
    
        # Output the creation of the new registry key
        Write-Host "Set $Path\$Name to $((Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name)"
    }

    function Get-UserHives {
        [CmdletBinding()]
        param (
            [Parameter()]
            [ValidateSet('AzureAD', 'DomainAndLocal', 'All')]
            [String]$Type = "All",
            [Parameter()]
            [String[]]$ExcludedUsers,
            [Parameter()]
            [switch]$IncludeDefault
        )
    
        # Define the SID patterns to match based on the selected user type
        $Patterns = switch ($Type) {
            "AzureAD" { "S-1-12-1-(\d+-?){4}$" }
            "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" }
            "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" }
        }
    
        # Retrieve user profile information based on the defined patterns
        $UserProfiles = Foreach ($Pattern in $Patterns) { 
            Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" |
                Where-Object { $_.PSChildName -match $Pattern } | 
                Select-Object @{Name = "SID"; Expression = { $_.PSChildName } },
                @{Name = "Username"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, 
                @{Name = "Domain"; Expression = { if ($_.PSChildName -match "S-1-12-1-(\d+-?){4}$") { "AzureAD" }else { $Null } } }, 
                @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, 
                @{Name = "Path"; Expression = { $_.ProfileImagePath } }
        }
    
        # If the IncludeDefault switch is set, add the Default profile to the results
        switch ($IncludeDefault) {
            $True {
                $DefaultProfile = "" | Select-Object Username, SID, UserHive, Path
                $DefaultProfile.Username = "Default"
                $DefaultProfile.Domain = $env:COMPUTERNAME
                $DefaultProfile.SID = "DefaultProfile"
                $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT"
                $DefaultProfile.Path = "C:\Users\Default"
    
                # Exclude users specified in the ExcludedUsers list
                $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.Username }
            }
        }
    
        if ($PSVersionTable.PSVersion.Major -lt 3) {
            $AllAccounts = Get-WmiObject -Class "win32_UserAccount"
        }
        else {
            $AllAccounts = Get-CimInstance -ClassName "win32_UserAccount"
        }
    
        $CompleteUserProfiles = $UserProfiles | ForEach-Object {
            $SID = $_.SID
            $Win32Object = $AllAccounts | Where-Object { $_.SID -like $SID }
    
            if ($Win32Object) {
                $Win32Object | Add-Member -NotePropertyName UserHive -NotePropertyValue $_.UserHive 
                $Win32Object | Add-Member -NotePropertyName Path -NotePropertyValue $_.Path
                $Win32Object
            }
            else {
                [PSCustomObject]@{
                    Name     = $_.Username
                    Domain   = $_.Domain
                    SID      = $_.SID
                    UserHive = $_.UserHive
                    Path     = $_.Path
                }
            }
        }
    
        # Return the list of user profiles, excluding any specified in the ExcludedUsers list
        $CompleteUserProfiles | Where-Object { $ExcludedUsers -notcontains $_.Name }
    }

    # Import the needed functions for working with PIDLs (Pointer to an Item Identifier List) into the Shell32 class
    Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;

public class Shell32 {
    // Converts a file or folder path into a PIDL (Pointer to an Item Identifier List)
    [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
    public static extern int SHParseDisplayName(string pszName, IntPtr pbc, out IntPtr ppidl, uint sfgaoIn, out uint psfgaoOut);

    // Converts a PIDL into a file or folder path
    [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
    public static extern int SHGetPathFromIDList(IntPtr pidl, char[] pszPath);

    // Retrieves the size of of an item ID list
    [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
    public static extern int ILGetSize(IntPtr pidl);

    // Used to free memory
    [DllImport("ole32.dll")]
    public static extern void CoTaskMemFree(IntPtr pv);
}
"@ -Language CSharp

    # Function to convert a path to a PIDL
    function Convert-PathToPIDL {
        param (
            [string]$Path
        )

        # Initialize the PIDL to zero
        $returnPIDL = [IntPtr]::Zero

        try {
            # Call SHParseDisplayName to get the PIDL from the provided path
            # https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shparsedisplayname
            $result = [Shell32]::SHParseDisplayName($Path, [IntPtr]::Zero, [ref]$returnPIDL, 0, [ref][uint32]0)

            # Check if the call was successful
            if ($result -ne 0) {
                throw "Failed to retrieve PIDL for path: $Path (Error Code: $result)"
            }

            # Return the PIDL
            return $returnPIDL
        }
        catch {
            throw $_
        }
    }

    # Function to get the size of a PIDL
    function Get-PIDLSize {
        param (
            [IntPtr]$PIDL
        )

        try {
            # Call the Shell32.dll ILGetSize function
            # https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-ilgetsize
            $Size = [Shell32]::ILGetSize($PIDL)
            return $Size
        }
        catch {
            throw $_
        }
    }

    # Function to encode a path into a Base64 encoded PIDL
    # This is used to set the photo screensaver folder via the registry
    function Get-EncryptedPIDLStringFromPath {
        param (
            [string]$Path
        )

        try {
            # Get the PIDL from the path
            $PIDL = Convert-PathToPIDL $Path

            # Get the size of the PIDL structure
            $SizeOfPIDL = Get-PIDLSize $PIDL

            # Create a new byte array the size of the PIDL
            $ByteArray = New-Object byte[] $SizeOfPIDL
            
            # Copy the PIDL data into the byte array
            # https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.copy?view=net-9.0#system-runtime-interopservices-marshal-copy(system-intptr-system-byte()-system-int32-system-int32)
            [System.Runtime.InteropServices.Marshal]::Copy($PIDL, $ByteArray, 0, $SizeOfPIDL)

            # Encode the byte array to a Base64 string
            $Base64EncodedString = [System.Convert]::ToBase64String($ByteArray)

            return $Base64EncodedString
        }
        catch {
            throw $_
        }
        finally {
            # Free the unmanaged PIDL memory
            if ($PIDL -ne [IntPtr]::Zero -and $null -ne $PIDL) {
                [Shell32]::CoTaskMemFree($PIDL)
            }
        }
    }

    # Function to convert a Base64 encoded PIDL back to a path
    function Convert-PIDLToPath {
        param (
            [string]$EncodedPIDL
        )
    
        # Decode the Base64 string to a byte array
        $DecodedBytes = [System.Convert]::FromBase64String($EncodedPIDL)
    
        try {
            # Allocate memory for the PIDL
            $DecodedPIDLPointer = [System.Runtime.InteropServices.Marshal]::AllocCoTaskMem($DecodedBytes.Length)
    
            # Copy the decoded bytes to the allocated memory
            # https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.copy?view=net-9.0#system-runtime-interopservices-marshal-copy(system-byte()-system-int32-system-intptr-system-int32)
            [System.Runtime.InteropServices.Marshal]::Copy($DecodedBytes, 0, $DecodedPIDLPointer, $DecodedBytes.Length)
            
            # Create buffer to hold the path
            # The buffer size is 261 characters (Windows default max path length of 260 + null terminator)
            $PathBuffer = New-Object char[] 261
    
            # Call SHGetPathFromIDList to get the path from the PIDL
            # The first parameter is a point to where the data is, the second is where the path will be stored 
            # The function returns 0 on success
            $Result = [Shell32]::SHGetPathFromIDList($DecodedPIDLPointer, $PathBuffer)
            
            # Throw if error occurred
            if ($Result -eq 0) {
                throw "Failed to retrieve path from PIDL."
            }
    
            # Return the decoded path as a string
            return -join $PathBuffer
        }
        catch {
            throw $_
        }
        finally {
            # Free the allocated memory
            if ($DecodedPIDLPointer -ne [IntPtr]::Zero) {
                [System.Runtime.InteropServices.Marshal]::FreeCoTaskMem($DecodedPIDLPointer)
            }
        }
    }
}
process {
    try {
        $IsElevated = Test-IsElevated -ErrorAction Stop
    }
    catch {
        Write-Host -Object "[Error] $($_.Exception.Message)"
        Write-Host -Object "[Error] Unable to determine if the account '$env:Username' is running with Administrator privileges."
        exit 1
    }
    
    if (!$IsElevated) {
        Write-Host -Object "[Error] Access Denied: Please run with Administrator privileges."
        exit 1
    }

    # Import script variables
    if ($env:selectScreensaver) {$selectScreensaver = $env:selectScreensaver}
    if ($env:waitTime) {
        if ([int]::TryParse($env:waitTime, [ref]$null)) {
            $waitTime = [int]$env:waitTime
        }
        else {
            Write-Host "[Error] The wait time must be a valid integer between 1 and 9999 minutes. Please enter a valid wait time."
            exit 1
        }
    }
    if (-not [string]::IsNullOrWhiteSpace($env:textFor3DText)) {$textFor3DText = $env:textFor3DText.Trim()}
    if (-not [string]::IsNullOrWhiteSpace($env:photosDirectoryPath)) {$photosDirectoryPath = $env:photosDirectoryPath.Trim()}

    # Map screensaver dropdown value to SCR file name
    $screensaverMap = @{
        "None"    = ""
        "3D Text" = "ssText3d.scr"
        "Blank"   = "scrnsave.scr"
        "Bubbles" = "Bubbles.scr"
        "Photos"  = "PhotoScreensaver.scr"
        "Mystify" = "Mystify.scr"
        "Ribbons" = "Ribbons.scr"
    }

    # Validate the selected screensaver
    if (-not [string]::IsNullOrWhiteSpace($SelectScreensaver) -and -not $screensaverMap.ContainsKey($SelectScreensaver)) {
        Write-Host "[Error] Invalid screen saver selected. Valid options are: $($screensaverMap.Keys -join ', ')"
        exit 1
    }

    # Validate 3D Text is provided
    if ($SelectScreensaver -eq "3D Text" -and [string]::IsNullOrWhiteSpace($textFor3DText)) {
        Write-Host "[Error] 3D Text screensaver requires text to be provided."
        exit 1
    }

    # Warn if 3D Text is not selected but text is provided
    if ($SelectScreensaver -ne "3D Text" -and -not [string]::IsNullOrWhiteSpace($TextFor3DText)) {
        Write-Host "[Warning] 3D Text was not selected as the screensaver. Ignoring provided Text for 3D Text input.`n"
        $textFor3DText = $null
    }

    # Validate character length of 3D Text
    if ($SelectScreensaver -eq "3D Text" -and $TextFor3DText.Length -gt 20) {
        Write-Host "[Error] 3D Text exceeds the maximum length of 20 characters."
        exit 1
    }

    # Warn if Photos is not selected but path is provided
    if ($SelectScreensaver -ne "Photos" -and -not [string]::IsNullOrWhiteSpace($PhotosDirectoryPath)) {
        Write-Host "[Warning] Photos was not selected as the screensaver. Ignoring provided Photos Directory Path input.`n"
        $PhotosDirectoryPath = $null
    }

    # Error if the provided photos path is a UNC path
    if ($SelectScreensaver -eq "Photos" -and $PhotosDirectoryPath -and $PhotosDirectoryPath -match "^\\\\") {
        Write-Host "[Error] UNC paths cannot be used for the Photos screensaver. Please provide a local path."
        exit 1
    }

    # Error if photos path is not a local path
    if ($SelectScreensaver -eq "Photos" -and $PhotosDirectoryPath -and $PhotosDirectoryPath -notmatch "^[a-zA-Z]:\\") {
        Write-Host "[Error] The provided photo directory path '$PhotosDirectoryPath' is not a valid local path."
        exit 1
    }

    # Validate character length of the photos directory path
    if ($SelectScreensaver -eq "Photos" -and $PhotosDirectoryPath -and $PhotosDirectoryPath.Length -gt 260) {
        Write-Host "[Error] The provided photo directory path '$PhotosDirectoryPath' exceeds the maximum length of 260 characters."
        exit 1
    }

    # Set default photos path if photos is selected but no path provided
    if ($SelectScreensaver -eq "Photos" -and [string]::IsNullOrWhiteSpace($PhotosDirectoryPath)) {
        Write-Host "[Warning] No path chosen for Photos directory. Defaulting to each user's Pictures folder."
        $defaultPhotosPath = $true
    }
    elseif ($SelectScreensaver -eq "Photos") {
        # Otherwise, validate the provided path
        # Error if the provided path doesn't exist
        if (-not (Test-Path -Path $PhotosDirectoryPath)) {
            Write-Host "[Error] The provided photo directory path '$PhotosDirectoryPath' does not exist."
            exit 1
        }

        # Error if the provided path is not a directory
        if (-not (Test-Path -Path $PhotosDirectoryPath -PathType Container)) {
            Write-Host "[Error] The provided photo directory path '$PhotosDirectoryPath' is not a directory."
            exit 1
        }
    }

    # Skip all other options if 'None' is selected as the screensaver
    # Only warn the user if they specified another option
    if ($SelectScreensaver -eq "None") {
        if ($WaitTime -or $OnResumeEnterPassword -or $TextFor3DText -or $PhotosDirectoryPath) {
            Write-Host "[Warning] When 'None' is selected as the screensaver, all other options will be ignored.`n"
        }
        $SkipAllSettings = $true
    }
    else {
        # Check for wait time, if not provided fall back to 15 mins
        # If it is provided, check if it is a valid integer
        if ($waitTime -eq 0) {
            Write-Host "[Warning] Wait time not provided. Defaulting to 15 minutes.`n"
            $waitTime = 15
        }
        elseif ($waitTime -lt 0 -or $waitTime -gt 9999) {
            Write-Host "[Error] The minimum wait time is 1 minute, the maximum is 9999 minutes. Please enter a valid wait time."
            exit 1
        }
    }

    # Warn if force reboot is not selected and a setting is being changed
    if (-not $ForceReboot) {
        Write-Host "[Warning] Force reboot not selected. Changes to screensaver settings may not take effect until the user logs off or restarts their computer.`n"
    }

    $ExitCode = 0

    # Get all user profiles on the machine
    $UserProfiles = Get-UserHives -Type "All"

    # Create list to hold loaded profiles
    # This is used to unload the profiles after the script has run
    $ProfileWasLoaded = New-Object System.Collections.Generic.List[object]
    
    # Loop through each profile on the machine
    foreach ($UserProfile in $UserProfiles) {
        # Store current user SID in variable
        $currentUserSID = $UserProfile.SID
        
        # Load User ntuser.dat if it's not already loaded
        if (!(Test-Path -Path Registry::HKEY_USERS\$currentUserSID -ErrorAction SilentlyContinue)) {
            try {
                Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe LOAD HKU\$currentUserSID `"$($UserProfile.UserHive)`"" -Wait -WindowStyle Hidden
                $ProfileWasLoaded.Add($UserProfile)
            }
            catch {
                Write-Host "[Error] Error loading the profile for '$($UserProfile.Name)'."
                Write-Host "[Error] $($_.Exception.Message)"
                $ExitCode = 1
                continue
            }
        }

        Write-Host "[Info] Working on '$($UserProfile.Name)' profile...`n"

        # Set registry base path for the current user
        $regPath = "Registry::HKEY_USERS\$currentUserSID\Control Panel\Desktop"

        # Get current values for later comparison
        $currentValues = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue
        $currentScreensaver = $currentValues."SCRNSAVE.EXE"

        # If a screensaver has been selected, set or remove it
        if (-not [string]::IsNullOrWhiteSpace($SelectScreensaver)){
            if ($SelectScreensaver -eq "None") {
                $currentScreenSaverIsSecure = $currentValues."ScreenSaverIsSecure"
                $currentScreenSaverTimeout = $currentValues."ScreenSaveTimeout"

                # Create array of values to check if screensaver is active
                $combined = @($currentScreensaver, $currentScreenSaverIsSecure, $currentScreenSaverTimeout)

                if ([string]::IsNullOrWhiteSpace($combined)) {
                    Write-Host "[Info] Screensaver is already disabled."
                }
                else {
                    try {
                        Write-Host "[Info] Disabling screensaver."

                        # Remove the screensaver registry key
                        if ($currentScreensaver) {
                            Write-Host "[Info] Removing $regPath\SCRNSAVE.EXE (current value: $currentScreensaver)"
                            Remove-ItemProperty -Path $regPath -Name "SCRNSAVE.EXE" -ErrorAction Stop
                        }

                        # Remove the password requirement registry key
                        if ($currentScreenSaverIsSecure) {
                            Write-Host "[Info] Removing $regPath\ScreenSaverIsSecure (current value: $currentScreenSaverIsSecure)"
                            Remove-ItemProperty -Path $regPath -Name "ScreenSaverIsSecure" -ErrorAction Stop
                        }

                        # Remove the screensaver timeout key
                        if ($currentScreenSaverTimeout) {
                            Write-Host "[Info] Removing $regPath\ScreenSaveTimeout (current value: $currentScreenSaverTimeout)"
                            Remove-ItemProperty -Path $regPath -Name "ScreenSaveTimeout" -ErrorAction Stop
                        }

                        Write-Host "[Info] Successfully disabled screensaver."
                    }
                    catch {
                        Write-Host "[Error] Failed to disable screensaver."
                        Write-Host "$($_.Exception.Message)"
                        $ExitCode = 1
                    }
                }
            }
            else {
                $screensaverFileName = $screensaverMap[$SelectScreensaver]

                if ($currentScreensaver -eq "$env:SystemRoot\System32\$screensaverFileName") {
                    Write-Host "[Info] Screensaver is already set to '$SelectScreensaver'."
                }
                else {
                    try {
                        Write-Host "[Info] Setting screensaver to '$SelectScreensaver'."
                        Set-RegKey -Path $regPath -Name "SCRNSAVE.EXE" -Value "$env:SystemRoot\System32\$screensaverFileName" -PropertyType "String"
                    }
                    catch {
                        Write-Host "[Error] Failed to set screensaver."
                        Write-Host "$($_.Exception.Message)"
                        $ExitCode = 1
                    }
                }
            }
            Write-Host ""
        }

        # If set to skip all settings, continue to the next profile
        # This will be true if the user selected 'None' as the screensaver
        if ($SkipAllSettings) {
            Write-Host "[Info] Finished working on '$($UserProfile.Name)' profile.`n"
            continue
        }

        # Set screensaver timeout
        if ($waitTime) {
            # Get the current value
            $currentValue = (Get-ItemProperty -Path $regPath -Name ScreenSaveTimeOut -ErrorAction SilentlyContinue).ScreenSaveTimeOut

            # Check if it is already set, if not then set it
            if ($currentValue -eq ($waitTime * 60)) {
                Write-Host "[Info] Screensaver timeout is already set to $waitTime minute(s)."
            }
            else {
                try {
                    Write-Host "[Info] Setting screensaver timeout to $waitTime minute(s)."
                    Set-RegKey -Path $regPath -Name ScreenSaveTimeOut -Value ($waitTime * 60) -PropertyType "String"
                }
                catch {
                    Write-Host "[Error] Failed to set screensaver timeout."
                    Write-Host "$($_.Exception.Message)"
                    $ExitCode = 1
                }
            }
            Write-Host ""
        }

        # Get current value for password required on resume
        $currentValue = (Get-ItemProperty -Path $regPath -Name ScreenSaverIsSecure -ErrorAction SilentlyContinue).ScreenSaverIsSecure

        # Check if password required on resume is set, if not set it
        if ($OnResumeEnterPassword) {
            if ($currentValue -eq 1) {
                Write-Host "[Info] Screensaver password requirement is already enabled."
            }
            else {
                try {
                    Write-Host "[Info] Enabling screensaver password requirement."
                    Set-RegKey -Path $regPath -Name ScreenSaverIsSecure -Value 1 -PropertyType "String"
                }
                catch {
                    Write-Host "[Error] Failed to enable screensaver password requirement."
                    Write-Host "$($_.Exception.Message)"
                    $ExitCode = 1
                }
            }
        }
        else {
            if ($currentValue -eq 0) {
                Write-Host "[Info] Screensaver password requirement is already disabled."
            }
            else {
                try {
                    Write-Host "[Info] Disabling screensaver password requirement."
                    Set-RegKey -Path $regPath -Name ScreenSaverIsSecure -Value 0 -PropertyType "String"
                }
                catch {
                    Write-Host "[Error] Failed to disable screensaver password requirement."
                    Write-Host "$($_.Exception.Message)"
                    $ExitCode = 1
                }
            }
        }

        # If using the default photos directory path, set the photos directory path to the user's Pictures folder
        if ($defaultPhotosPath){
            $PhotosDirectoryPath = "$($UserProfile.Path)\Pictures"
        }

        # Set photos screensaver directory path
        if ($SelectScreensaver -eq "Photos" -and $PhotosDirectoryPath){
            Write-Host ""
            # Photos screensaver uses a different registry key
            $photosRegPath = "Registry::HKEY_USERS\$currentUserSID\SOFTWARE\Microsoft\Windows Photo Viewer\Slideshow\Screensaver"

            # Convert the photos directory path to an EncryptedPIDL, which is what the registry expects
            try {
                $EncryptedPIDL = Get-EncryptedPIDLStringFromPath $PhotosDirectoryPath
            }
            catch {
                Write-Host "[Error] Error converting '$PhotosDirectoryPath' to EncryptedPIDL string."
                Write-Host "[Error] $($_.Exception.Message)"
                $EncryptedPIDL = $null
                $ExitCode = 1
            }

            # Check if the registry path exists, create it if not
            if (-not (Test-Path -Path $photosRegPath)) {
                try {
                    New-Item -Path $photosRegPath -Force -ErrorAction Stop | Out-Null
                }
                catch {
                    Write-Host "[Error] Error creating the registry path '$photosRegPath' for photos screensaver."
                    Write-Host "$($_.Exception.Message)"
                    $EncryptedPIDL = $null
                    $ExitCode = 1
                }
            }

            if ($EncryptedPIDL){
                # Get the current value of the photos directory path
                $currentValue = (Get-ItemProperty -Path $photosRegPath -Name "EncryptedPIDL" -ErrorAction SilentlyContinue)."EncryptedPIDL"

                # If the current value is not null, try to convert it to a path for comparison
                if ($currentValue){
                    # Get the current value directory path
                    try {
                        # Trim the whitespace and remove null characters
                        $currentValueDirectoryPath = (Convert-PIDLToPath $currentValue).Trim() -replace "\u0000"
                    }
                    catch {
                        Write-Host "[Error] Error converting current PIDL value to path."
                        Write-Host "$($_.Exception.Message)"
                        $currentValueDirectoryPath = $null
                        $ExitCode = 1
                    }
                }

                # Check if it is already set, if not then set it
                if ($currentValueDirectoryPath -eq $PhotosDirectoryPath) {
                    Write-Host "[Info] Photos screensaver is already using '$PhotosDirectoryPath'."
                }
                else {
                    if ($currentValueDirectoryPath) {
                        Write-Host "[Info] Photos screensaver directory is currently using '$currentValueDirectoryPath'."
                    }

                    # Set the photos screensaver directory path
                    try {
                        Write-Host "[Info] Setting photos screensaver to use '$PhotosDirectoryPath'."
                        Set-ItemProperty -Path $photosRegPath -Name "EncryptedPIDL" -Value $EncryptedPIDL -Type "String" -Force -Confirm:$false -ErrorAction Stop
                        Write-Host "[Info] Successfully set photos screensaver to use '$PhotosDirectoryPath'."
                    }
                    catch {
                        Write-Host "[Error] Failed to set photos screensaver directory."
                        Write-Host "$($_.Exception.Message)"
                        $currentValue = $null
                        $ExitCode = 1
                    }
                }
            }
        }

        # Set 3D Text content
        if ($SelectScreensaver -eq "3D Text" -and $textFor3DText) {
            Write-Host ""

            # 3D Text has a different registry path than the rest of the settings
            $3DTextRegPath = "Registry::HKEY_USERS\$currentUserSID\Software\Microsoft\Windows\CurrentVersion\Screensavers\ssText3d"

            # Get the current value of the text
            $currentValue = (Get-ItemProperty -Path $regPath -Name DisplayString -ErrorAction SilentlyContinue).DisplayString

            # Check if it is already set, if not then set it
            if ($currentValue -eq $textFor3DText) {
                Write-Host "[Info] 3D Text is already set to '$textFor3DText'."
            }
            else {
                try {
                    Write-Host "[Info] Setting 3D Text to '$textFor3DText'."
                    Set-RegKey -Path $3DTextRegPath -Name DisplayString -Value $textFor3DText -PropertyType "String"
                }
                catch {
                    Write-Host "[Error] Failed to set 3D Text."
                    Write-Host "$($_.Exception.Message)"
                    $ExitCode = 1
                }
            }
        }
        Write-Host "`n[Info] Finished working on '$($UserProfile.Name)' profile.`n"
    }

    # If user profiles were loaded, unload the profiles
    if ($ProfileWasLoaded.Count -gt 0) {
        foreach ($UserProfile in $ProfileWasLoaded) {
            # Unload NTuser.dat
            [gc]::Collect()
            Start-Sleep 1
            try {
                Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe UNLOAD HKU\$currentUserSID" -Wait -WindowStyle Hidden -ErrorAction Stop | Out-Null
            }
            catch {
                Write-Host "[Error] Error unloading the profile for '$($UserProfile.Name)'."
                Write-Host "[Error] $($_.Exception.Message)"
                $ExitCode = 1
            }
        }
    }

     # if force reboot was selected, schedule a reboot in 1 minute and notify the user
     if ($ForceReboot) {
        Write-Host "[Info] Rebooting computer in 60 seconds..."
        shutdown.exe -r -t 60
    }
    
    exit $ExitCode
}
end {
    
    
    
}

 

Description détaillée

Voici comment ce script accomplit sa tâche :

  1. Contrôle des privilèges
    Le script vérifie d’abord s’il est exécuté avec des droits d’administrateur. Étant donné que les modifications du registre pour d’autres profils d’utilisateurs requièrent des privilèges élevés, cette vérification permet d’éviter une exécution non autorisée.
  2. Paramètres d’entrée
    • SelectScreensaver : Choisissez parmi les options intégrées telles que “Bubbles,” “Mystify,” ou “3D Text”.
    • WaitTime : Définir la durée (en minutes) avant que l’économiseur d’écran ne s’active.
    • OnResumeEnterPassword : Renforcer le mot de passe au réveil.
    • PhotosDirectoryPath : Chemin personnalisé pour l’économiseur d’écran Photos.
    • TextFor3DText : Définir un texte personnalisé pour l’économiseur d’écran 3D Text.
    • ForceReboot : Redémarrer éventuellement après les modifications.
  3. Validation et cartographie des écrans de veille
    Une table de hachage associe des noms d’économiseurs d’écran conviviaux à leurs fichiers exécutables (par exemple, Mystify → Mystify.scr).
  4. Modifications des clés de registre
    Il utilise la fonction d’aide Set-RegKey pour mettre à jour ou créer les clés de registre nécessaires sous HKEY_USERS pour chaque SID d’utilisateur.
  5. Traitement des profils d’utilisateurs
    Via Get-UserHives, il charge NTUSER.DAT pour chaque utilisateur s’il n’est pas déjà chargé, rendant leurs ruches (hives) de registre temporairement accessibles.
  6. Paramètres spécifiques en fonction de l’économiseur d’écran
    • None : Supprime les touches pertinentes pour désactiver l’économiseur d’écran.
    • Photos : Encode le chemin de la photo en une chaîne PIDL Base64.
    • 3D Text : Définit la valeur de la chaîne d’affichage de l’économiseur d’écran.
  7. Décharger les profils
    Les profils chargés au moment de l’exécution sont déchargés en toute sécurité afin d’éviter de laisser des ruches de registre montées.
  8. Redémarrage facultatif
    Si l’option -ForceReboot est utilisée, elle planifie un redémarrage du système au bout de 60 secondes.

Cas d’utilisation potentiels

Étude de cas : Application de la conformité d’entreprise

Une entreprise MSP gérant des appareils pour un client du secteur de la santé doit implémenter un verrouillage automatique de l’écran après 5 minutes d’inactivité avec une protection par mot de passe activée. À l’aide de ce script, l’administrateur peut mettre en place une stratégie avec :

powershell

CopyEdit

-SelectScreensaver « Blank » -WaitTime 5 -OnResumeEnterPassword:$true -ForceReboot

Cette commande unique permet de mettre à jour tous les profils d’utilisateurs sur un appareil donné sans avoir recours à la stratégie de groupe ou à des solutions basées sur le domaine.

Comparaisons

Stratégie de groupe et script PowerShell

FonctionStratégie de groupeCe script PowerShell
Domaine requis✅ Oui❌ Non
Cibler tous les utilisateurs✅ Oui✅ Oui
Chemins d’accès personnalisés/texte❌ Limitée✅ Contrôle total
Prise en charge des terminaux hors ligne❌ Non✅ Oui
Application en temps réel❌ Non (nécessite une actualisation)✅ Immédiate (avec redémarrage si nécessaire)

Pour les MSP qui gèrent des environnements hybrides ou BYOD, ce script offre une flexibilité que les stratégies de groupe (GPO) n’ont pas.

FAQ

Q : Puis-je utiliser ce script sur des machines n’appartenant pas à un domaine ?

Oui. Le script fonctionne indépendamment de l’appartenance à un domaine.

Q : Que se passe-t-il si l’utilisateur est déjà connecté ?

Les modifications s’appliquent, mais peuvent nécessiter une déconnexion ou un redémarrage pour être pleinement prises en compte, à moins que l’option -ForceReboot ne soit utilisée.

Q : Fonctionnera-t-il avec Windows 11 ?

Oui, à condition que le système d’exploitation prenne en charge les fichiers binaires et les chemins d’accès au registre de l’économiseur d’écran sélectionné.

Q : Que se passe-t-il si je fournis un chemin d’accès réseau pour les photos ?

Le script refuse les chemins UNC pour l’économiseur d’écran Photos, qui doit utiliser un répertoire local.

Implications

Une mauvaise configuration de l’économiseur d’écran peut rendre les terminaux vulnérables, soit en ne verrouillant pas les sessions inactives, soit en activant des effets visuels gênants. Ce script ne se contente pas d’assurer la conformité, il favorise également l’hygiène de sécurité de l’utilisateur. Il empêche l’exposition accidentelle de données sensibles et garantit un comportement cohérent entre les comptes d’utilisateurs, ce qui est essentiel dans les environnements d’appareils partagés.

Recommandations

  • Exécutez en tant qu’administrateur : Exécutez toujours le script avec des autorisations élevées.
  • Utilisez -ForceReboot dans les déploiements critiques : Assure l’application de tous les paramètres sans dépendre de la déconnexion des utilisateurs.
  • Pré-test sur une machine d’essai : Valider les chemins d’accès, en particulier pour les photos et les écrans de veille en 3D.
  • Documentez les paramètres dans les outils RMM : Lors du déploiement via des systèmes tels que NinjaOne, étiquetez clairement les paramètres en vue d’une maintenance ultérieure.

Conclusion

La gestion de l’expérience utilisateur et de la conformité sur les terminaux n’a pas besoin d’impliquer des stratégies de groupe (GPO) ou des étapes manuelles fastidieuses. Ce script PowerShell permettant de modifier les paramètres de l’économiseur d’écran simplifie l’application des stratégies et facilite la gestion de tous les profils d’utilisateurs sur un appareil Windows. Lorsqu’il est intégré dans un écosystème d’automatisation plus large comme NinjaOne vous pouvez déployer ce script de manière optimale sur des flottes de machines, suivre l’état d’exécution et l’intégrer à d’autres scripts afin de garder le contrôle total de votre environnement.

Next Steps

Building an efficient and effective IT team requires a centralized solution that acts as your core service delivery tool. NinjaOne enables IT teams to monitor, manage, secure, and support all their devices, wherever they are, without the need for complex on-premises infrastructure.

Learn more about NinjaOne Remote Script Deployment, check out a live tour, or start your free trial of the NinjaOne platform.

Catégories :

Vous pourriez aussi aimer