public/Set-RestrictiveACL.ps1
function Set-RestrictiveACL { <# .SYNOPSIS Overwrite all permissions on a file, folder, or registry key to allow access only to SYSTEM, Administrators, and TrustedInstaller accounts, which is the most restrictive ACL possible without risking system lockout. .DESCRIPTION Creates and applies a new ACL from scratch, without retaining any existing rules, using a new instance of 'FileSecurity' for files and folders, and 'RegistrySecurity' for registry keys. There is no inheritance or merging with existing rules. This means all previous permissions are fully removed, and only the new rules for SYSTEM, Administrators, and TrustedInstaller will be applied. Summary: 1) No retrieval of the existing ACL. 2) Creation of a brand-new empty ACL. 3) Only the new rules are applied. Other key points: - Explicit use of 'SetAccessRuleProtection($true, $false)' for a "crushing" approach. - Caching of NT accounts to avoid repeated lookups. - Registry path handling with support for hives. - Clear separation between registry and file system logic. - Uses 'ShouldProcess' to support '-WhatIf'. This PowerShell module must be run with administrator rights. .PARAMETER Path Path to the file, folder, or registry key. .PARAMETER Recurse Applies recursively to sub-items if it's a folder. .PARAMETER VerboseLevel Controls the verbosity level of the script: - "Disabled": no console output. - "Debug": detailed output. .EXAMPLE Set-RestrictiveACL -Path "C:\test\protection.txt" Protect file. .EXAMPLE Set-RestrictiveACL -Path "C:\test\protection" -Recurse Protect folder, with recursion. .EXAMPLE Set-RestrictiveACL -Path "HKEY_CURRENT_USER:\System\Protection" Protect registry key. .NOTES Use with caution, as non-administrator users will lose all access. Version : 1.0.2 Author : Frederic PETIT Created : 2025-05-16 Revised : 2025-06-01 Compatibility : PowerShell 5+ #> [CmdletBinding(SupportsShouldProcess = $true)] Param ( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $false)][switch]$Recurse, [Parameter(Mandatory=$false)][ValidateSet("Disabled", "Debug")][string]$VerboseLevel = "Disabled" ) # Cache pour les comptes NT. $NTAccountCache = @{}; # Liste des SIDs et règles associées. $SIDs = @{ "S-1-5-18" = @{ Name = "SYSTEM" FileSystemAccessRule = "FullControl,Allow" RegistryAccessRule = "FullControl,None,None,Allow" } "S-1-5-32-544" = @{ Name = "Administrateurs" FileSystemAccessRule = "FullControl,Allow" RegistryAccessRule = "FullControl,None,None,Allow" } "S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464" = @{ Name = "TrustedInstaller" FileSystemAccessRule = "ReadAndExecute,Allow" RegistryAccessRule = "ReadKey,None,None,Allow" } } function Get-NTAccountFromSID { <# .SYNOPSIS Convertit un SID en compte NT, avec mise en cache. .DESCRIPTION La fonction prend un SID au format chaîne et retourne le compte NT correspondant, sous forme d'un objet [System.Security.Principal.NTAccount]. Pour améliorer les performances, les résultats sont mis en cache afin d'éviter les traductions répétées. .PARAMETER SID Le SID à convertir en compte NT. .OUTPUTS System.Security.Principal.NTAccount .EXAMPLE Get-NTAccountFromSID -SID "S-1-5-18" Retourne le compte NT correspondant, par exemple : NT AUTHORITY\SYSTEM. #> Param ( [Parameter(Mandatory = $true)][string]$SID ) # Test : vérifie si le NTAccount est déjà dans le cache. if ($NTAccountCache.ContainsKey($SID)) { return $NTAccountCache[$SID]; } else { # Essai : traduction. try { $SecurityIdentifier = New-Object System.Security.Principal.SecurityIdentifier($SID); $NTAccount = $SecurityIdentifier.Translate([System.Security.Principal.NTAccount]); # Test : vérifier que le compte existe dans le système (TrustedInstaller est un compte de service protégé, qui n'est pas toujours exposé via WinNT://, et la vérification [ADSI] peut échouer inutilement, même si le compte est parfaitement utilisable pour une ACL.). if (-not $SIDs.ContainsKey($SID) -and -not ([ADSI]"WinNT://$($NTAccount.Value.Replace('\','/'))")) { if ($VerboseLevel -eq "Debug") { Write-Warning "Account '$($NTAccount.Value)' not found on this system. Rule ignored."; } return $null; } else { $NTAccountCache[$SID] = $NTAccount; return $NTAccount; } } catch { if ($VerboseLevel -eq "Debug") { Write-Warning "Error translating SID '$SID' to NTAccount."; } return $null; } } } function Open-RegistryKey { <# .SYNOPSIS Ouvre une clé de registre avec les droits nécessaires pour modifier ses permissions. .DESCRIPTION Cette fonction ouvre une clé de registre spécifiée en mode lecture/écriture, avec les droits 'ChangePermissions'. Elle retourne un objet 'RegistryKey' utilisable pour la manipulation des ACL. En cas d'erreur, elle affiche un message et retourne '$null'. .PARAMETER hive Nom de la ruche (ex: LocalMachine, CurrentUser, etc.). .PARAMETER subkey Chemin relatif de la sous-clé à partir de la ruche. .OUTPUTS Microsoft.Win32.RegistryKey .EXAMPLE Open-RegistryKey -hive "LocalMachine" -subkey "SOFTWARE\MyApp" #> Param ( [Parameter(Mandatory = $true)][string]$hive, [Parameter(Mandatory = $true)][string]$subkey ) # Essai : lecture. try { return [Microsoft.Win32.Registry]::$hive.OpenSubKey( $subkey, [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree, [System.Security.AccessControl.RegistryRights]::ChangePermissions ) } catch { if ($VerboseLevel -eq "Debug") { Write-Error "Error opening registry key '$hive\$subkey' : $($_.Exception.Message)"; } return $null; } } function New-AccessRule { <# .SYNOPSIS Crée une règle d'accès pour un fichier ou une clé registre. .DESCRIPTION Cette fonction génère un objet 'FileSystemAccessRule' ou 'RegistryAccessRule' à partir d'une chaîne formatée et du nom de compte NT fourni. Elle vérifie d'abord que la chaîne 'RuleString' contient le nombre d'éléments requis (2 pour FileSystem, 4 pour Registry). En cas de chaîne mal formée, elle retourne $null et émet un avertissement. .PARAMETER NtAccount Nom du compte NT cible (ex. : "NT AUTHORITY\SYSTEM"). .PARAMETER RuleString Chaîne représentant les paramètres de la règle, séparés par des virgules. - Pour FileSystem : "<Rights>,<ControlType>" (par ex. "FullControl,Allow"). - Pour Registry : "<Rights>,<ControlType>,<InheritanceFlags>,<PropagationFlags>" (par ex. "ReadKey,Allow,None,None"). .PARAMETER Type Type de règle à générer. Valeurs autorisées : - "FileSystem" : crée un objet FileSystemAccessRule. - "Registry" : crée un objet RegistryAccessRule. .EXAMPLE $rule = New-AccessRule -NtAccount "NT AUTHORITY\SYSTEM" -RuleString "FullControl,Allow" -Type "FileSystem" Retourne un objet FileSystemAccessRule. .EXAMPLE $rule = New-AccessRule -NtAccount "BUILTIN\Administrators" -RuleString "ReadKey,Allow,None,None" -Type "Registry" Retourne un objet RegistryAccessRule. .OUTPUTS [System.Security.AccessControl.FileSystemAccessRule] si Type = "FileSystem" et RuleString valide. [System.Security.AccessControl.RegistryAccessRule] si Type = "Registry" et RuleString valide. $null en cas d'erreur (chaîne mal formée ou exception). #> Param ( [Parameter(Mandatory = $true)][string]$NtAccount, [Parameter(Mandatory = $true)][string]$RuleString, [Parameter(Mandatory = $true)][ValidateSet("FileSystem", "Registry")][string]$Type ) try { # Test : vérifier que la chaîne est bien formée (au moins 2 parties pour FileSystem, 4 pour Registry) $parts = $RuleString -split ","; if ($Type -eq "FileSystem" -and $parts.Count -lt 2) { throw "RuleString invalide pour FileSystem : '$RuleString'"; } if ($Type -eq "Registry" -and $parts.Count -lt 4) { throw "RuleString invalide pour Registry : '$RuleString'"; } switch ($Type) { "FileSystem" { return New-Object System.Security.AccessControl.FileSystemAccessRule($NtAccount, $parts[0], $parts[1]); } "Registry" { return New-Object System.Security.AccessControl.RegistryAccessRule($NtAccount, $parts[0], $parts[1], $parts[2], $parts[3]); } } } catch { Write-Warning "Impossible de créer la règle d'accès : $_"; return $null; } } function Create-AccessRules { <# .SYNOPSIS Crée une liste de règles d'accès à partir d'un ensemble de SIDs. .DESCRIPTION À partir d'une table de SIDs contenant des informations sur les règles d'accès, cette fonction génère les objets 'FileSystemAccessRule' ou 'RegistryAccessRule' appropriés selon le type spécifié. Elle utilise 'Get-NTAccountFromSID' et 'New-AccessRule'. .PARAMETER SIDs Hashtable contenant les SIDs et leurs règles d'accès pour les types FileSystem ou Registry. .PARAMETER Type Type d'accès ciblé : "FileSystem" ou "Registry". .EXAMPLE $rules = Create-AccessRules -SIDs $SIDs -Type "FileSystem" #> Param ( [Parameter(Mandatory = $true)][hashtable]$SIDs, [Parameter(Mandatory = $true)][string]$Type ) $rules = @(); foreach ($sidStr in $SIDs.Keys) { $Nt = Get-NTAccountFromSID $sidStr; if ($Nt) { $rule = New-AccessRule -NtAccount $Nt -RuleString $SIDs[$sidStr]["$($Type)AccessRule"] -Type $Type; $rules += $rule; } } return $rules; } function Apply-FilesystemACL { <# .SYNOPSIS Applique une ACL restrictive sur un fichier ou dossier. .DESCRIPTION Cette fonction crée une nouvelle ACL (vierge) pour un fichier ou dossier, désactive l'héritage, supprime toutes les règles existantes, et applique uniquement celles définies pour SYSTEM, Administrateurs et TrustedInstaller. .PARAMETER TargetPath Chemin complet vers le fichier ou dossier cible. .EXAMPLE Apply-FilesystemACL -TargetPath "C:\Program Files\MonApp" .OUTPUTS [pscustomobject] Objet contenant les champs : - Success [bool] : Indique si l'opération a réussi. - Code [int] : Code d'état (0 = OK, 1 = lien symbolique ignoré, 2 = erreur d'accès, 3 = erreur ACL). - Msg [string] : Message d'information ou d'erreur. #> Param ( [Parameter(Mandatory = $true)][string]$TargetPath ) # Obtenir l'objet FileSystemInfo. try { $item = Get-Item -LiteralPath $TargetPath -Force; # Ignorer les liens symboliques ou junctions : lorsque l'outil est exécuté sur des répertoires comme C:\ProgramData ou C:\Users, il peut rencontrer des junctions (liens NTFS spéciaux) pointant vers d'autres emplacements système sensibles, les modifier pourrait causer des erreurs ou des corruptions, d'où l'importance de les ignorer. if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { $Result = [PSCustomObject]@{ Success = $false; Code = 1; Msg = "Ignored (symbolic link or junction) : '$TargetPath'."; } return $Result; } } catch { $Result = [PSCustomObject]@{ Success = $false; Code = 2; Msg = "Unable to access : '$TargetPath' - $_"; } return $Result; } # Créer une nouvelle ACL vierge, suivant le type. if ($item.PSIsContainer) { $Acl = New-Object System.Security.AccessControl.DirectorySecurity; } else { $Acl = New-Object System.Security.AccessControl.FileSecurity; } # Désactiver l'héritage des permissions (protection contre l'héritage) et ne pas conserver les règles existantes. $Acl.SetAccessRuleProtection($true, $false); # Générer les règles. $rules = Create-AccessRules -SIDs $SIDs -Type "FileSystem"; foreach ($rule in $rules) { $Acl.AddAccessRule($rule); } # Appliquer l'ACL. try { $item.SetAccessControl($Acl); $Result = [PSCustomObject]@{ Success = $true; Code = 0; Msg = "ACL applied for : '$TargetPath'."; } } catch { $Result = [PSCustomObject]@{ Success = $false; Code = 3; Msg = "Error applying ACL on : '$TargetPath' - $_"; } } # Finale. return $Result; } function Apply-RegistryACL { <# .SYNOPSIS Applique une ACL restrictive sur une clé de registre. .DESCRIPTION Cette fonction crée une nouvelle ACL pour une clé de registre, supprime toutes les règles d'accès existantes, et applique uniquement celles définies pour SYSTEM, Administrateurs et TrustedInstaller. .PARAMETER TargetPath Chemin complet vers la clé de registre (ex. : HKLM:\Software\MonApp). .EXAMPLE Apply-RegistryACL -TargetPath "HKLM:\Software\MonApp" .OUTPUTS [pscustomobject] Objet contenant les champs : - Success [bool] : Indique si l'opération a réussi. - Code [int] : Code d'état (0 = OK, 1 = hive invalide, 2 = clé inaccessible, 3 = erreur d'application ACL). - Msg [string] : Message d'information ou d'erreur. #> Param ( [Parameter(Mandatory = $true)][string]$TargetPath ) # Déterminer la ruche et le chemin interne. switch -Regex ($TargetPath) { '^(HKLM|HKCU|HKCR|HKU|HKCC|HKEY_LOCAL_MACHINE|HKEY_CURRENT_USER|HKEY_CLASSES_ROOT|HKEY_USERS|HKEY_CURRENT_CONFIG):?\\(.+)' { $hiveRaw = $Matches[1] $subkey = $Matches[2] $hive = switch ($hiveRaw.ToUpper()) { "HKLM" { "LocalMachine" } "HKCU" { "CurrentUser" } "HKCR" { "ClassesRoot" } "HKU" { "Users" } "HKCC" { "CurrentConfig" } "HKEY_LOCAL_MACHINE" { "LocalMachine" } "HKEY_CURRENT_USER" { "CurrentUser" } "HKEY_CLASSES_ROOT" { "ClassesRoot" } "HKEY_USERS" { "Users" } "HKEY_CURRENT_CONFIG" { "CurrentConfig" } } } default { $Result = [PSCustomObject]@{ Success = $false; Code = 1; Msg = "Unsupported hive : '$TargetPath'."; } return $Result; } } # Ouvrir la clé du registre. $Key = Open-RegistryKey -hive $hive -subkey $subkey; if ($Key) { # Créer une nouvelle ACL. $Acl = New-Object System.Security.AccessControl.RegistrySecurity; # Désactiver l'héritage des permissions (protection contre l'héritage) et ne pas conserver les règles existantes. $Acl.SetAccessRuleProtection($true, $false); # Ajouter les nouvelles ACL pour SYSTEM, Administrateurs et TrustedInstaller. $rules = Create-AccessRules -SIDs $SIDs -Type "Registry"; foreach ($rule in $rules) { $Acl.AddAccessRule($rule); } # Appliquer l'ACL. try { $Key.SetAccessControl($Acl); $Result = [PSCustomObject]@{ Success = $true; Code = 0; Msg = "ACL applied for : '$TargetPath'."; } } catch { $Result = [PSCustomObject]@{ Success = $false; Code = 3; Msg = "Error applying ACL on : '$TargetPath' - $_"; } } finally { # Fermeture explicite de la clé après modification des ACL. $Key.Close(); } } else { $Result = [PSCustomObject]@{ Success = $false; Code = 2; Msg = "Inaccessible key : '$TargetPath'."; } } # Finale. return $Result; } # ---------- Routine. # Test : Root. if (-not ([Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) { if ($VerboseLevel -eq "Debug") { Write-Warning "Administrator rights required to run this program."; } } else { # Test : registre, filesystem. if ($Path -match "^(HKLM|HKCU|HKCR|HKU|HKCC|HKEY_LOCAL_MACHINE|HKEY_CURRENT_USER|HKEY_CLASSES_ROOT|HKEY_USERS|HKEY_CURRENT_CONFIG)(:|\\\\)") { $result = Apply-RegistryACL -TargetPath $Path; if ($VerboseLevel -eq "Debug") { if ($result.Success) { Write-Host $result.Msg -ForegroundColor Green; } else { Write-Warning "ACL registry - ERROR code $($result.Code) :: $($result.Msg)"; } } } else { # Résolution du chemin absolu. # Test : fichier/dossier existe ou arrêt immédiat pour éviter toute exception. if (-not (Test-Path -LiteralPath $Path)) { if ($VerboseLevel -eq "Debug") { Write-Warning "Source not found : '$Path'."; } } else { $Path = Resolve-Path -LiteralPath $Path | Select-Object -ExpandProperty Path; # Essai : validation du chemin. try { $item = Get-Item -LiteralPath $Path -Force; } catch { if ($VerboseLevel -eq "Debug") { Write-Error "The specified path does not exist or is inaccessible : '$Path' - $_"; } } if ($item -is [System.IO.FileSystemInfo]) { if ($PSCmdlet.ShouldProcess($Path, "Apply restricted filesystem ACL")) { $result = Apply-FilesystemACL -TargetPath $Path; if ($VerboseLevel -eq "Debug") { if ($result.Success) { Write-Host $result.Msg -ForegroundColor Green; } else { Write-Warning "ACL filesystem - ERROR code $($result.Code) :: $($result.Msg)"; } } } if ($Recurse -and $item.PSIsContainer) { Get-ChildItem -Path $item.FullName -Recurse -Force | ForEach-Object { if ($PSCmdlet.ShouldProcess($_.FullName, "Apply restricted filesystem ACL")) { $result = Apply-FilesystemACL -TargetPath $_.FullName; if ($VerboseLevel -eq "Debug") { if ($result.Success) { Write-Host $result.Msg -ForegroundColor Green; } else { Write-Warning "ACL filesystem - ERROR code $($result.Code) :: $($result.Msg)"; } } } } } } else { if ($VerboseLevel -eq "Debug") { Write-Warning "The specified path is not recognized : '$Path'."; } } } } } } |