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'.";
                    }
                }
            }
        }
    }
}