Faire l'inventaire d'un serveur de fichiers avec PowerShell — analyse de la capacité et audit des droits d'accès (ACL)

· · PowerShell, Windows, Serveur de fichiers, ACL, Droits d'accès, Amélioration opérationnelle, Sécurité, Script

« L’espace libre du serveur de fichiers est descendu sous 10 % » — le travail qui commence par cet avertissement est presque toujours pénible. On ne sait pas où passe la capacité. On ne peut pas trancher si un dossier vieux de dix ans, que plus personne ne touche, peut être supprimé. Et de toute façon, on ne sait pas répondre immédiatement à la question de savoir qui peut accéder à quoi, ni si les droits d’une personne partie de l’entreprise sont encore présents. Une investigation menée en ouvrant les propriétés une par une dans l’explorateur de fichiers ne se termine jamais sur un partage de plusieurs téraoctets.

À chaque fois que je mets la main à la pâte sur ce genre de demande, je me dis que ranger un serveur de fichiers n’est pas une « technique de suppression », mais une « technique d’inventaire ». Demander « peut-on supprimer ? » sans chiffres ni liste, et un service ne dira jamais oui. À l’inverse, un tableau du type « ce seul dossier pèse 800 Go, dont 620 Go de fichiers non modifiés depuis plus de trois ans » fait avancer la discussion d’un coup.

Cet article s’adresse aux services informatiques et aux équipes d’exploitation des PME et organise la procédure pratique pour faire l’inventaire de la capacité et des droits d’accès (ACL) d’un serveur de fichiers avec PowerShell, jusqu’à la production d’un rapport CSV. Le principe directeur, constant tout au long de l’article, est : « d’abord la lecture, la modification en dernier, toujours avec sauvegarde et -WhatIf ».

1. La conclusion, d’abord

  • L’analyse de la capacité repose fondamentalement sur Get-ChildItem -Recurse + Measure-Object -Sum. Plutôt que d’agréger l’ensemble d’un coup, repérez d’abord les « endroits qui pèsent lourd » dossier par dossier au premier niveau.12
  • N’ignorez jamais silencieusement les refus d’accès : enregistrez-les. En contrepartie de ne pas interrompre le balayage avec -ErrorAction SilentlyContinue, notez systématiquement les emplacements refusés avec -ErrorVariable. Un emplacement qui n’a pas pu être compté n’est pas « zéro », il est « inconnu ».3
  • Basez l’identification des fichiers anciens sur LastWriteTime. Sous NTFS, la mise à jour de la date de dernier accès est souvent désactivée par défaut (ou gérée au niveau système), ce qui la rend peu fiable comme critère de décision.45
  • Extrayez les « candidats » à un doublon avec Get-FileHash (SHA256 par défaut). La bonne pratique pour économiser les E/S consiste à filtrer d’abord par taille avant de calculer le hachage.6
  • L’inventaire des ACL passe par la propriété Access de Get-Acl. Exportez en CSV IdentityReference (qui), FileSystemRights (quoi) et IsInherited (hérité ou direct), et détectez les emplacements dont l’héritage est coupé avec AreAccessRulesProtected.78
  • Ce que cet article inventorie, ce sont les autorisations NTFS. L’accès effectif via un partage (SMB) est déterminé par la combinaison des autorisations du partage et des ACL NTFS. Dressez séparément la liste côté partage avec Get-SmbShareAccess, et consultez-la en parallèle du registre NTFS.9
  • Précisez explicitement l’encodage des rapports CSV. Sous Windows PowerShell 5.1, l’encodage par défaut d’Export-Csv est ASCII et casse le japonais ; sous PowerShell 7, il s’agit d’UTF-8 sans BOM, qui peut mal s’entendre avec le comportement par défaut d’Excel.1011
  • La modification (Set-Acl) n’intervient qu’après l’inventaire et la vérification par le service. Prenez une sauvegarde avec icacls /save, vérifiez la cible avec -WhatIf, puis appliquez.1213

Une fois que vous avez suivi l’ensemble de la procédure de cet article, vous disposez des quatre livrables suivants. Voici d’abord à quoi ressemble le résultat final.

Livrable Chapitre de production Colonnes Ce qu’on peut y lire
Classement de la capacité par dossier Chapitre 2 Folder / SizeGB / FileCount Quels dossiers pèsent le plus lourd. Les chiffres nécessaires pour dire « le partage entier fait ◯ Go, dont ◯ Go pour le service commercial » viennent d’ici
old-files.csv Chapitre 3 FullName / LastWriteTime / SizeMB Liste des fichiers non modifiés depuis plus de trois ans. Triée par SizeMB décroissant, les premières dizaines de lignes suffisent à voir « la capacité qui compte »
duplicate-candidates.csv Chapitre 3 Hash / Path Groupes de fichiers au contenu identique. Trier par la colonne Hash regroupe ensemble les fichiers de même contenu
acl-report.csv Chapitre 4 Path / Identity / Rights / Type / IsInherited / Inheritance / InheritanceBroken Le registre de qui a quel droit où. Une ACE correspond à une ligne

À cela s’ajoutent des fichiers texte enregistrant les emplacements qui n’ont pas pu être balayés (denied-paths.txt, old-files-uninspected.txt, hash-uninspected.txt, acl-uninspected.txt). Si ceux-ci ne sont pas vides, les quatre livrables ci-dessus sont des « rapports troués ». Remettez-les en le précisant.

La phrase citée en introduction — « ce seul dossier fait 800 Go, dont 620 Go de fichiers non modifiés depuis plus de trois ans » — peut se construire en rapprochant le premier et le deuxième livrable. Le second chiffre s’obtient simplement en relisant old-files.csv et en le totalisant.

# Calculer, à partir de old-files.csv, "la capacité totale des fichiers non modifiés depuis plus de trois ans"
$old   = Import-Csv .\old-files.csv
$sumMB = ($old | ForEach-Object { [double]$_.SizeMB } | Measure-Object -Sum).Sum
'Non modifié depuis 3 ans ou plus : {0} fichiers / {1:N1} Go' -f $old.Count, ($sumMB / 1024)

2. Analyse de la capacité — chiffrer les endroits qui pèsent lourd

La première chose à faire, c’est la taille totale de chaque dossier situé juste sous la racine du partage. Énumérez récursivement les fichiers avec Get-ChildItem, puis totalisez Length avec Measure-Object.12

$root = 'D:\share'   # à exécuter sur le serveur de fichiers. Voir le chapitre 6 pour les précautions via UNC
$denied = @()

# Agréger dossier par dossier au premier niveau -- repérer d'abord, plutôt que tout agréger d'un coup
# Pour que l'échec d'énumération de la racine elle-même finisse aussi dans $denied,
# on applique le même paramétrage au Get-ChildItem externe
$report = foreach ($dir in Get-ChildItem -LiteralPath $root -Directory `
    -ErrorAction SilentlyContinue -ErrorVariable +denied) {
    # SilentlyContinue pour ne pas s'arrêter sur un refus d'accès, mais les refus sont
    # accumulés dans $denied
    # L'énumération est envoyée directement par le pipe vers Measure-Object (la recevoir
    # dans une variable garderait tous les FileInfo en mémoire, ce qui devient pénible
    # pour un dossier de plusieurs millions de fichiers)
    $stats = Get-ChildItem -LiteralPath $dir.FullName -Recurse -File `
        -ErrorAction SilentlyContinue -ErrorVariable +denied |
        Measure-Object -Property Length -Sum
    [PSCustomObject]@{
        Folder    = $dir.Name
        SizeGB    = [math]::Round([double]$stats.Sum / 1GB, 2)
        FileCount = $stats.Count
    }
}
# Compter aussi comme une ligne les fichiers posés directement sous la racine
# (ils échapperaient sinon à l'agrégation par dossier)
$rootStats = Get-ChildItem -LiteralPath $root -File `
    -ErrorAction SilentlyContinue -ErrorVariable +denied |
    Measure-Object -Property Length -Sum
if ($rootStats.Count -gt 0) {
    $report += [PSCustomObject]@{
        Folder    = '(Directement sous la racine)'
        SizeGB    = [math]::Round([double]$rootStats.Sum / 1GB, 2)
        FileCount = $rootStats.Count
    }
}

$report | Sort-Object SizeGB -Descending | Format-Table -AutoSize

# Conserver systématiquement "les emplacements qui n'ont pas pu être comptés" -- si ce
# fichier n'est pas vide, l'agrégation est incomplète
$denied | ForEach-Object { $_.TargetObject } | Sort-Object -Unique |
    Set-Content -Path .\denied-paths.txt

Deux points à retenir. Premièrement, -ErrorAction SilentlyContinue n’est pas un interrupteur qui « fait comme si l’erreur n’avait pas eu lieu ». Il se contente de supprimer l’affichage et de continuer : l’erreur elle-même se produit bel et bien.3 C’est pourquoi on la capture avec -ErrorVariable. En préfixant le nom de la variable par +, on obtient un ajout plutôt qu’un remplacement, ce qui permet de rassembler dans une seule variable les refus survenus sur toute la boucle.3 Un dossier dont l’accès a été refusé échappe à l’agrégation, et le rapport le fera donc apparaître plus petit qu’il ne l’est réellement. Si denied-paths.txt n’est pas vide, indiquez-le systématiquement dans le rapport. Pour pousser plus loin la conception de la gestion des erreurs, « Gestion des erreurs et conception des tentatives dans PowerShell », publié en même temps, est une référence utile.

Deuxièmement, le problème des hiérarchies profondes. Un partage exploité depuis de nombreuses années comporte presque certainement des chemins dépassant 260 caractères (MAX_PATH), et selon l’outil utilisé, l’énumération peut y échouer.14 Si le résultat du balayage présente des trous inexpliqués, soupçonnez d’abord la longueur des chemins. La vue d’ensemble de cette limite et les moyens d’y remédier sont réunis dans « MAX_PATH et les pièges des chemins et noms de fichiers sous Windows ».

3. Fichiers anciens et candidats en double — l’inventaire avant la suppression

3.1. Faire la liste des « fichiers non touchés depuis trois ans »

Une fois identifiés les endroits qui pèsent lourd, l’étape suivante consiste à rassembler la matière pour les « candidats à la suppression ». Le critère utilisé est LastWriteTime (date de dernière modification).

$cutoff = (Get-Date).AddYears(-3)

# Liste des fichiers non modifiés depuis 3 ans ou plus. On ne supprime pas -- on prépare
# d'abord la matière à montrer au service
# Les emplacements où l'énumération a échoué sont aussi enregistrés avec -ErrorVariable
# pour être divulgués plus tard
Get-ChildItem -LiteralPath $root -Recurse -File `
    -ErrorAction SilentlyContinue -ErrorVariable oldEnumErrors |
    Where-Object LastWriteTime -lt $cutoff |
    Select-Object FullName, LastWriteTime,
        @{ Name = 'SizeMB'; Expression = { [math]::Round($_.Length / 1MB, 2) } } |
    Sort-Object SizeMB -Descending |
    Export-Csv -Path .\old-files.csv -NoTypeInformation -Encoding utf8BOM   # UTF8 sous 5.1 (les deux ont un BOM)

# Les emplacements qui n'ont pas pu être énumérés doivent toujours être divulgués, pour
# éviter que la liste ne paraisse "complète"
$oldEnumErrors | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\old-files-uninspected.txt

On est tenté de se dire qu’utiliser LastAccessTime (date du dernier accès) permettrait même de savoir « n’a même pas été lu », mais c’est un piège. Sous NTFS, la mise à jour de la date de dernier accès affecte les performances, si bien que son activation/désactivation est contrôlée par la commande fsutil behavior et un registre (NtfsDisableLastAccessUpdate)4, et elle est désactivée par défaut depuis Windows Vista (gérée au niveau système depuis les versions récentes de Windows 10, et toujours désactivée sur les serveurs).5 Autrement dit, il existe couramment des fichiers réellement ouverts mais dont la date reste ancienne. Pour le document explicatif de l’inventaire, utilisez LastWriteTime — « la date de la dernière modification du contenu » — et précisez-en explicitement le sens : cela évite que la conversation avec le service ne se complique.

3.2. Lister les « candidats » à un doublon

Les dossiers partagés accumulent en grand nombre des fichiers « version finale », « version finale_corrigée », « copie de ~ ». On peut déterminer si le contenu est identique avec Get-FileHash. L’algorithme par défaut est SHA256, et une correspondance de hachage permet de conclure que le contenu des fichiers est identique.6

# Calculer le hachage de tous les fichiers coûte cher en E/S. On filtre d'abord aux
# "fichiers de même taille"
# Les emplacements où l'énumération a échoué (refus d'accès, etc.) sont aussi enregistrés
# avec -ErrorVariable pour être divulgués plus tard
$candidates = Get-ChildItem -LiteralPath $root -Recurse -File `
    -ErrorAction SilentlyContinue -ErrorVariable enumErrors |
    Group-Object -Property Length |
    Where-Object { $_.Count -ge 2 -and [long]$_.Name -gt 0 } |   # exclure la taille 0
    ForEach-Object { $_.Group }

# Ne calculer le hachage que pour les candidats filtrés, et sortir les groupes concordants
# comme "candidats" à un doublon
$candidates |
    Get-FileHash -ErrorAction SilentlyContinue -ErrorVariable hashErrors |   # SHA256 par défaut
    Group-Object -Property Hash |
    Where-Object Count -ge 2 |
    ForEach-Object { $_.Group } |
    Select-Object Hash, Path |
    Export-Csv -Path .\duplicate-candidates.csv -NoTypeInformation -Encoding utf8BOM   # UTF8 sous 5.1

# Les dossiers qui n'ont pas pu être énumérés, ainsi que les fichiers dont le hachage n'a
# pas pu être calculé (verrouillage, absence de droit de lecture, etc.), doivent toujours
# rester dans une liste "non examinés"
# (les faire disparaître silencieusement les rendrait indiscernables d'un zéro doublon)
@($enumErrors) + @($hashErrors) | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\hash-uninspected.txt

Le terme « candidat » est employé à dessein : décider lequel conserver relève d’une question métier, pas technique. Deux fichiers de contenu identique peuvent avoir un sens différent — « original du service A » contre « copie de référence du service B ». Ne supprimez rien mécaniquement : ce script s’arrête à la production d’une liste pour en discuter avec les parties prenantes. La nature de l’outil qu’est le hachage (ce qui est garanti, ce qui ne l’est pas) est présentée dans « Procédure pratique pour identifier un format à partir d’une chaîne de hachage ».

3.3. Mettre en quarantaine avant de supprimer — la procédure « quarantaine → observation → suppression »

Même après que le service a vérifié la liste, on ne passe pas directement à la suppression. Voici le contenu de la procédure « quarantaine → observation → suppression » mentionnée dans le tableau de décision du chapitre 7.

  1. Choisissez l’emplacement de quarantaine. Créez un dossier daté sur le même serveur. Dans la mesure du possible, sur le même volume que l’original (raison expliquée plus loin). Limitez les droits à l’administrateur et à la personne en charge. Attention : restreindre les droits du seul dossier n’affecte pas les fichiers qu’il contient. Un déplacement au sein du même volume conserve les droits d’origine tels quels ; l’ensemble inclut donc aussi l’étape consistant à ramener chaque fichier déplacé à l’héritage par défaut (également détaillée plus loin).
  2. Déplacez en conservant la hiérarchie d’origine. Un déplacement à plat provoque des collisions de fichiers homonymes, et vous ne saurez plus non plus retrouver l’emplacement d’origine au moment de restaurer.
  3. Conservez un registre des déplacements. Sans table de correspondance entre le chemin d’origine et le chemin de destination, vous ne pourrez pas répondre à « où est passé ce fichier ? ». Le registre ne doit noter que ce qui a réellement été déplacé. Séparez dans des fichiers distincts les résultats d’un essai à blanc et ce qui n’a pas pu être déplacé. Les mélanger vous ferait chercher, lors d’une restauration, un emplacement qui n’existe pas.
  4. Fixez une période d’observation. Plutôt que « un mois », il est plus pratique de la caler sur le cycle métier, comme « jusqu’à ce qu’une clôture trimestrielle ait été franchie ». Si une demande arrive, consultez le registre et restaurez.
  5. Supprimez une fois la période écoulée. Juste avant la suppression, partagez à nouveau le registre avec les services concernés.
# Déplacer vers le dossier de quarantaine la liste validée par le service
# (celle où l'on n'a gardé, dans old-files.csv, que les lignes concernées)
$root       = 'D:\share'
$quarantine = 'D:\quarantine\2026-07'
$list       = Import-Csv .\old-files-approved.csv   # mêmes colonnes que old-files.csv
$dryRun     = $true       # vérifier d'abord la cible avec $true, puis passer à $false pour l'exécution réelle
$planned    = @()   # résultat de l'essai à blanc. Exporté dans un fichier séparé du registre d'exécution réelle
$moved      = @()   # ce qui a réellement pu être déplacé (pour compter les lignes ; le registre réel est le fichier ci-dessous)
$failed     = @()   # ce qui n'a pas été déplacé. Reste à l'emplacement d'origine
$needsAcl   = @()   # déplacé, mais dont les droits n'ont pas pu être restreints

# Donner un nom différent au fichier de sortie à chaque exécution. Avec un nom fixe,
# le registre précédent serait écrasé dès qu'on lance le lot suivant, rendant impossible
# de restaurer les fichiers déplacés à ce moment-là
$runId    = Get-Date -Format 'yyyyMMdd-HHmmss'
$ledger   = ".\quarantine-log-$runId.csv"        # registre de restauration. Complété à chaque déplacement
$planFile = ".\quarantine-plan-$runId.csv"
$failFile = ".\quarantine-failed-$runId.csv"
$aclFile  = ".\quarantine-needs-acl-$runId.csv"
$enc      = 'utf8BOM'                            # 'UTF8' sous 5.1

# Chemins absolus utilisés pour déterminer le périmètre. Aligner la fin sur \ évite
# qu'on juge à tort D:\share2 comme étant à l'intérieur de D:\share
$rootFull       = [System.IO.Path]::GetFullPath($root).TrimEnd('\') + '\'
$quarantineFull = [System.IO.Path]::GetFullPath($quarantine).TrimEnd('\') + '\'

foreach ($row in $list) {
    $src = $row.FullName
    if (-not (Test-Path -LiteralPath $src)) { continue }   # ignorer ce qui n'existe déjà plus

    # Ce CSV est édité par une personne : une ligne pointant en dehors de $root peut s'y glisser.
    # Le Substring ci-dessous fonctionnerait alors silencieusement et mettrait en quarantaine
    # un fichier d'un tout autre emplacement.
    # On convertit d'abord vers le chemin réel, puis on vérifie qu'il est bien à l'intérieur
    # du périmètre audité avant de continuer
    $srcFull = (Resolve-Path -LiteralPath $src).ProviderPath
    if (-not $srcFull.StartsWith($rootFull, [StringComparison]::OrdinalIgnoreCase)) {
        $failed += [PSCustomObject]@{ Source = $src; Destination = ''
                                      Error  = "Pointe en dehors du périmètre audité ($root)" }
        continue
    }

    # Déplacer en conservant la hiérarchie d'origine (évite les collisions de fichiers
    # homonymes et l'impossibilité de restaurer)
    $relative = $srcFull.Substring($rootFull.Length)
    # Join-Path ne résout pas '..'. On réduit avec GetFullPath, puis on vérifie que le
    # résultat reste bien à l'intérieur de la destination de quarantaine
    $dest     = [System.IO.Path]::GetFullPath((Join-Path $quarantine $relative))
    if (-not $dest.StartsWith($quarantineFull, [StringComparison]::OrdinalIgnoreCase)) {
        $failed += [PSCustomObject]@{ Source = $src; Destination = $dest
                                      Error  = "Sort de la zone de quarantaine ($quarantine)" }
        continue
    }

    if ($dryRun) {
        # Ne pas mélanger les lignes de l'essai à blanc avec $moved. Les écrire dans le
        # même registre ferait que, après l'exécution réelle, un simple nouvel essai à blanc
        # écraserait le registre valide par une liste de "destinations qui n'existent pas"
        $planned += [PSCustomObject]@{ Source = $src; Destination = $dest }
        continue
    }

    try {
        New-Item -ItemType Directory -Path (Split-Path -Parent $dest) -Force -ErrorAction Stop | Out-Null

        # Les erreurs de Move-Item ne sont pas terminales par défaut. Sans -ErrorAction Stop,
        # un échec dû à un verrouillage, un refus d'accès ou une collision passerait à la
        # ligne suivante, et on se retrouverait à écrire "déplacé" dans le registre alors
        # que le fichier est resté à l'emplacement d'origine
        Move-Item -LiteralPath $srcFull -Destination $dest -ErrorAction Stop
    }
    catch {
        # Un fichier qui échoue ici n'a pas bougé. Il reste à l'emplacement d'origine
        $failed += [PSCustomObject]@{ Source = $src; Destination = $dest; Error = $_.Exception.Message }
        continue
    }

    # À partir d'ici, le fichier se trouve dans $dest. Quoi qu'il échoue par la suite,
    # le fait qu'il "se trouve dans $dest" doit toujours rester dans le registre. Le
    # classer côté failed créerait la pire des incohérences : la trace indiquerait
    # "emplacement d'origine" alors que le fichier réel est en quarantaine, et comme
    # Test-Path $src redeviendrait faux, il ne serait plus jamais repris lors d'une
    # nouvelle exécution.
    #
    # Et il ne faut pas non plus attendre la sortie de la boucle pour tout écrire d'un
    # coup : si l'alimentation coupe en cours de route, aucun des fichiers déjà déplacés
    # ne serait enregistré. On complète donc le registre ligne par ligne
    $record = [PSCustomObject]@{ Source = $srcFull; Destination = $dest; MovedAt = Get-Date }
    $record | Export-Csv -Path $ledger -NoTypeInformation -Encoding $enc -Append
    $moved += $record

    # Un déplacement au sein du même volume conserve les droits d'origine tels quels
    # (voir plus loin). On les ramène à l'héritage par défaut, pour que seuls les droits
    # du dossier de quarantaine s'appliquent
    try {
        & icacls $dest /reset /q
        if ($LASTEXITCODE -ne 0) { throw "Échec de icacls /reset (code de sortie $LASTEXITCODE)" }
    }
    catch {
        # Le déplacement a réussi mais les droits n'ont pas pu être restreints. Le fichier
        # se retrouve en quarantaine avec les droits larges d'origine.
        # On l'ajoute aussi ligne par ligne
        $aclRow = [PSCustomObject]@{ Destination = $dest; Error = $_.Exception.Message }
        $aclRow | Export-Csv -Path $aclFile -NoTypeInformation -Encoding $enc -Append
        $needsAcl += $aclRow
    }
}

if ($dryRun) {
    $planned | Export-Csv -Path $planFile -NoTypeInformation -Encoding $enc
    Write-Host "Essai à blanc : $($planned.Count) fichier(s) concerné(s). Aucun déplacement effectué ($planFile)"
    return
}

$failed | Export-Csv -Path $failFile -NoTypeInformation -Encoding $enc

# Ne pas laisser passer silencieusement les droits non restreints. Ne pas démarrer la
# période d'observation tant que ce nombre n'est pas à 0
if ($needsAcl.Count -gt 0) {
    Write-Warning "Il y a $($needsAcl.Count) fichier(s) en quarantaine dont les droits n'ont pas été restreints. Consultez $aclFile pour y remédier"
}
Write-Host "Déplacés : $($moved.Count) / Échecs : $($failed.Count) / Droits non traités : $($needsAcl.Count)"
Write-Host "Registre : $ledger"

Avant de déplacer quoi que ce soit, vérifiez que le chemin se trouve vraiment à l’intérieur du périmètre audité. Ce script lit old-files-approved.csv — un fichier qu’une personne ouvre, dont elle supprime des lignes, et auquel elle en ajoute parfois à la main. Même si une ligne dont le FullName pointe en dehors de $root s’y glisse, $src.Substring($root.Length) ne lève aucune exception. Comme il se contente de compter des caractères et de couper le début, il construit sans broncher la destination d’un fichier totalement sans rapport. Réutiliser un vieux CSV, avoir mal copié un chemin, avoir ajouté une cible « en passant » — tout cela arrive. Pour la même raison, Join-Path ne résolvant pas .., une ligne contenant un séparateur relatif peut aboutir à un $dest situé hors du dossier de quarantaine.

C’est pourquoi on convertit d’abord vers le chemin réel avec Resolve-Path et on vérifie qu’il se trouve bien à l’intérieur de $root, puis on réduit également $dest avec GetFullPath avant de vérifier qu’il reste à l’intérieur de $quarantine. Si l’on ajoute \ à la fin des chemins de comparaison, c’est pour éviter que D:\share2 ne soit jugé à tort comme étant à l’intérieur de D:\share. Une ligne hors périmètre n’est pas déplacée : elle est renvoyée dans $failed, à charge pour la personne de corriger le CSV. Ces deux vérifications garantissent, côté code également, que l’opération de quarantaine « décide de la cible avant d’agir ».

Si le script intercale icacls /reset, c’est pour une raison de droits d’accès. Comme la documentation officielle le précise clairement, un objet hérite par défaut des droits du parent de destination, mais la seule exception est « le déplacement vers un autre dossier au sein du même volume », cas dans lequel seuls les droits d’origine sont conservés.15

Autrement dit, la forme recommandée à l’étape 1 — « déplacer vers le même volume que l’original » — est aussi une forme où les droits du dossier de quarantaine n’affectent pas les fichiers qu’il contient. Un fichier ouvert à un groupe partagé du service reste lisible par ce groupe après le déplacement, même si le dossier de quarantaine est restreint au seul administrateur. C’est précisément là que survient l’échec le plus fâcheux de ce travail : « ce n’est pas supprimé, mais ce n’est pas non plus vraiment mis en quarantaine ». icacls <path> /reset remplace l’ACL par l’ACL d’héritage par défaut : en le faisant passer juste après le déplacement, seuls les droits du dossier de quarantaine s’appliquent ensuite.13

Si vous déplacez vers un autre volume (un autre disque ou un autre serveur), le fichier hérite dès ce moment-là des droits du dossier de destination.15 Dans ce cas, /reset n’est pas nécessaire, mais cela suppose d’avoir déjà restreint au préalable les droits du dossier de quarantaine lui-même. Dans les deux cas, il est plus sûr de prélever quelques fichiers après le déplacement avec Get-Acl et de vérifier que les droits sont bien ceux attendus.

Faites aussi attention au traitement des fichiers qui ont été déplacés mais pour lesquels /reset a échoué. Dans ce cas, le fichier est déjà passé en quarantaine, mais seuls les droits — larges, d’origine — sont restés inchangés. Si vous les classez comme un « échec » retiré du registre, la trace laisse croire qu’ils sont restés à l’emplacement d’origine, et comme le chemin d’origine n’existe plus, une nouvelle exécution ne les reprendra pas non plus. C’est la pire des situations : le fichier reste en quarantaine, avec des droits larges, sans figurer dans aucun registre. C’est pourquoi le script ci-dessus l’ajoute au registre dès que le déplacement est terminé, et sort dans un CSV séparé la partie dont les droits n’ont pas pu être restreints. Si ce nombre n’est pas à 0, ne commencez pas la période d’observation avant d’avoir traité ce point.

Ne fixez pas le nom de fichier du registre. La quarantaine ne se termine jamais en une seule fois : elle se répète service par service, trimestre par trimestre. Avec une construction qui écrit dans un nom fixe quarantine-log.csv, le registre de la première fois disparaît dès que le deuxième lot est lancé. Les fichiers déplacés à ce moment-là se trouvent bien en quarantaine, mais plus aucune trace nulle part n’indique d’où ils viennent — impossible de restaurer. C’est pourquoi le script ci-dessus ajoute -$runId à chaque exécution. Pour restaurer, lisez tout avec Import-Csv .\quarantine-log-*.csv.

Le registre s’écrit ligne par ligne. Avec une construction qui fait un Export-Csv global après la sortie de la boucle, si la session se coupe ou si le serveur redémarre en cours de route, aucun des fichiers déjà déplacés ne se retrouve enregistré. Comme le déplacement a réellement eu lieu, ce n’est pas un état « avant travail », mais un état où « seul l’enregistrement manque ». En écrivant ligne par ligne avec -Append, quel que soit l’endroit où l’on s’arrête, on peut restaurer jusque-là. Rouvrir le fichier à chaque fois ralentit un peu, mais le registre a besoin de fiabilité plus que de vitesse.

Le script produit des fichiers répartis par rôle ($runId prend une forme comme 20260718-143052, correspondant à l’heure d’exécution).

Fichier Contenu Quand il est produit
quarantine-plan-$runId.csv Liste de ce qui est prévu d’être déplacé Lors d’un essai à blanc ($dryRun = $true)
quarantine-log-$runId.csv Ce qui a réellement été déplacé. Le registre utilisé pour restaurer. Complété ligne par ligne Lors de l’exécution réelle
quarantine-failed-$runId.csv Ce qui n’a pas pu être déplacé. Reste à l’emplacement d’origine Lors de l’exécution réelle
quarantine-needs-acl-$runId.csv Déplacé, mais dont les droits n’ont pas pu être restreints Uniquement si le cas se présente

4. Inventaire des droits d’accès (ACL) — mettre en tableau qui peut faire quoi

Un autre objet de l’inventaire, à côté de la capacité, ce sont les droits d’accès. Le vocabulaire va se multiplier d’un coup à partir d’ici, donc clarifions d’abord les relations entre ces termes.

Autre représentation du même contenuDescripteur de sécuritéAttaché à chaque fichier/dossierC'est ce que récupère Get-AclPropriétaireEnregistré sous forme de SIDDACLListe de qui est autorisé/refusé pour quoiC'est ce qu'expose la propriété Access de Get-AclSACLParamètres d'audit. Non traité dans cet articleACE — une ligne de la DACLIdentityReference : qui (SID ou nom résolu)FileSystemRights : ce qui est possibleAccessControlType : autorisation ou refusIsInherited : hérité ou attribué directementACEACE ...SDDLFormat qui représente tout ce descripteur sous forme d'une seule chaîne

Formulé en mots, c’est une structure imbriquée : la DACL se trouve dans le descripteur de sécurité, des ACE s’alignent dans la DACL, et l’ACE désigne un SID comme interlocuteur. Ce que l’on appelle « ACL » dans la pratique désigne presque toujours cette DACL. Considérez que les tableaux et les CSV qui suivent écrivent, ligne par ligne, les ACE les plus internes. Le SDDL est une représentation sous forme de chaîne du même contenu ; il n’est pas nécessaire de le lire au stade de l’inventaire.

Cela posé, entrons dans le vif du sujet. Get-Acl récupère le descripteur de sécurité d’un fichier ou d’un dossier, et sa propriété Access permet de lire la liste des entrées de contrôle d’accès (ACE) de la DACL.7

# Inventorier l'ACL des dossiers sur deux niveaux à partir de la racine du partage
# Le "tronc" de la conception des droits se concentre sur les niveaux peu profonds :
# on commence donc par là
$aclErrors = @()   # -ErrorVariable avec + est cumulatif : on réinitialise à chaque exécution
                    # pour ne pas reporter ce qui vient d'une exécution précédente
$targets = @(Get-Item -LiteralPath $root `
                 -ErrorAction SilentlyContinue -ErrorVariable +aclErrors) +
           @(Get-ChildItem -LiteralPath $root -Directory -Recurse -Depth 1 `
                 -ErrorAction SilentlyContinue -ErrorVariable +aclErrors)

$aclReport = foreach ($t in $targets) {
    # Un échec de récupération (lecture impossible, suppression pendant le balayage, etc.)
    # est aussi enregistré pour pouvoir être divulgué plus tard
    $acl = Get-Acl -LiteralPath $t.FullName -ErrorAction SilentlyContinue -ErrorVariable +aclErrors
    if (-not $acl) { continue }
    if (@($acl.Access).Count -eq 0) {
        # Un dossier avec une DACL vide (zéro entrée explicite) reste aussi une ligne, pour
        # que l'information de rupture d'héritage ne disparaisse pas du registre
        [PSCustomObject]@{
            Path = $t.FullName; Identity = '(aucune entrée)'; Rights = $null; Type = $null
            IsInherited = $null; Inheritance = $null
            InheritanceBroken = $acl.AreAccessRulesProtected
        }
        continue
    }
    foreach ($ace in $acl.Access) {
        [PSCustomObject]@{
            Path        = $t.FullName
            Identity    = $ace.IdentityReference    # qui (utilisateur/groupe)
            Rights      = $ace.FileSystemRights     # ce qui est possible
            Type        = $ace.AccessControlType    # Allow / Deny
            IsInherited = $ace.IsInherited          # hérité du parent, ou attribué directement
            # Même droit, mais distinguer s'il s'applique "seulement à ce dossier" ou aussi
            # "aux fichiers/dossiers en dessous"
            Inheritance = "$($ace.InheritanceFlags)/$($ace.PropagationFlags)"
            InheritanceBroken = $acl.AreAccessRulesProtected  # l'héritage est-il coupé sur ce dossier
        }
    }
}
$aclReport | Export-Csv -Path .\acl-report.csv -NoTypeInformation -Encoding utf8BOM   # UTF8 sous 5.1

# Les cibles dont l'ACL n'a pas pu être récupérée sont elles aussi systématiquement
# divulguées comme un trou du registre (même logique que les autres investigations)
$aclErrors | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\acl-uninspected.txt

Une fois ce CSV ouvert, filtrez-le selon les critères suivants pour repérer les anomalies.

Colonne à surveiller Signal d’anomalie Contexte typique
Identity Un nom d’utilisateur individuel figure directement Accumulation de « donnons les droits à cette personne pour l’instant ». Se dégrade avec les mutations et les départs
Identity Affiché sous la forme brute d’un SID, du type S-1-5-21-... Les droits d’un compte déjà supprimé subsistent (premier candidat au nettoyage)
IsInherited = False Des ACE attribuées directement, disséminées dans les niveaux profonds Attributions individuelles faites au coup par coup. Exceptions sorties de la conception
InheritanceBroken = True Dossier dont l’héritage est coupé Réponse passée du type « je ne veux pas montrer ce dossier-là uniquement ». Cible prioritaire de l’inventaire
Type = Deny Une ACE de refus est présente À conditions égales, un refus l’emporte sur une autorisation. Mais comme une ACE explicite est évaluée avant une ACE héritée, il arrive qu’une autorisation explicite l’emporte sur un refus hérité. Examinez l’origine de l’ACE pour en évaluer l’impact, et vérifiez en dernier ressort l’accès effectif

Une entrée avec IsInherited à False est « un droit ajouté à la main par quelqu’un sur ce dossier », et un dossier avec AreAccessRulesProtected à True est « une île que les changements de droits du parent n’atteignent pas ».78 La plupart des incidents liés aux droits se concentrent sur ces deux cas : commencez donc l’inventaire par là. Précisons aussi une limite : cet exemple étant un balayage peu profond avec -Depth 1, les dossiers ayant coupé l’héritage à un niveau plus profond n’entrent pas dans le périmètre, et aucune erreur ne le signale. Si vous voulez inspecter exhaustivement toutes les ruptures d’héritage, relancez le même script sans limite de -Depth (cela prendra du temps), ou relancez-le en désignant comme racine un dossier de service suspect. Notez par ailleurs que Get-Acl peut aussi afficher le descripteur de sécurité sous forme de chaîne SDDL7, mais pour un objectif d’inventaire, le tableau tiré de la propriété Access suffit amplement. Il n’est jamais trop tard pour se plonger dans la lecture du SDDL une fois que vous rencontrez un phénomène que ce tableau n’explique pas.

Une dernière remarque pratique. Précisez toujours l’encodage du CSV. Sous Windows PowerShell 5.1, Export-Csv produit par défaut une sortie ASCII qui casse le japonais.10 Sous les versions PowerShell 7, la valeur par défaut est utf8NoBOM (UTF-8 sans BOM)11, et certains environnements affichent des caractères corrompus si le fichier est ouvert tel quel dans Excel. Pour un fichier destiné à un service, mieux vaut s’orienter vers de l’UTF-8 avec BOM, avec -Encoding UTF8BOM (-Encoding UTF8 sous 5.1).

5. Comment effectuer les modifications — choisir entre icacls et Set-Acl, sauvegarde et -WhatIf

Ce n’est qu’une fois l’inventaire et la vérification par le service terminés que commence la phase de modification. Il existe alors deux familles d’outils.

Usage Outil Raison
Investigation, rapport Get-Acl Le résultat étant un objet, le filtrage et l’export en CSV sont faciles7
Sauvegarde avant modification icacls /save Enregistre l’ACL dans un fichier, restaurable telle quelle avec /restore13
Attribution/suppression de droits standard icacls /grant, /remove Se règle en une seule ligne, application récursive avec /t, réactivation de l’héritage possible (/inheritancelevel)13
Modification conditionnelle complexe Set-Acl Les règles peuvent être construites en code. Compatible -WhatIf12
Remise en état d’une ACL corrompue icacls /reset Remplace par l’ACL d’héritage par défaut (impact important. Dernier recours)13

Quel que soit l’outil choisi, l’ordre reste fixe : sauvegarde → -WhatIf (ou vérification de la cible) → application → vérification a posteriori.

# 1. Sauvegarder l'ACL dans un fichier avant modification (/t pour tout ce qui est dessous,
#    /c pour continuer même en cas d'erreur)
icacls "D:\share\sales" /save "C:\aclbackup\sales-acl.txt" /t /c
# Ne pas avancer vers la modification si la sauvegarde n'a pas abouti. Le succès d'icacls
# se vérifie par le code de sortie
if ($LASTEXITCODE -ne 0) {
    throw "Échec de la sauvegarde de l'ACL (icacls ExitCode=$LASTEXITCODE). Arrêt de l'opération de modification"
}

# 2. Pour modifier avec Set-Acl : trois étapes -- récupération -> édition des règles -> application
$path = 'D:\share\sales\estimate'
$acl  = Get-Acl -LiteralPath $path

# Règle accordant le droit de modification au groupe commercial (hérité par les sous-dossiers/fichiers)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
    'CONTOSO\SalesTeam', 'Modify', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
# L'ajout se fait avec AddAccessRule. SetAccessRule remplace la règle Allow déjà existante
# pour le même utilisateur/groupe, ce qui peut faire disparaître silencieusement des droits
# fins déjà configurés pour ce groupe
$acl.AddAccessRule($rule)

# 3. Vérifier d'abord avec -WhatIf "où cela s'appliquera", avant de passer à l'exécution réelle
Set-Acl -LiteralPath $path -AclObject $acl -WhatIf
# Si tout va bien : Set-Acl -LiteralPath $path -AclObject $acl

Set-Acl fonctionne en « appliquant comme modèle le descripteur de sécurité récupéré par Get-Acl »12, donc une erreur de cible entre la récupération et l’application se traduit par un remplacement complet et non intentionnel de l’ACL. Des habitudes discrètes comme conserver le chemin de récupération et le chemin d’application dans la même variable, comme ci-dessus, ou intercaler -WhatIf avant l’application, évitent les incidents. Couper l’héritage (SetAccessRuleProtection) se fait dans le même cadre12, mais comme on l’a vu au chapitre 4, une rupture d’héritage représente un coût de gestion futur : limitez-en la création aux endroits où c’est réellement nécessaire.

Pour la conception à adopter si vous faites évoluer cet ensemble de travaux en un script exécuté régulièrement (SupportsShouldProcess, la façon de conserver des traces, l’inscription dans le Planificateur de tâches), le modèle traité dans « PowerShell avancé — automatiser en toute sécurité l’investigation des journaux, l’archivage et la génération de rapports » s’applique tel quel.

6. Où exécuter — balayage via UNC et compte d’exécution

Pour finir, un point qui influence le résultat plus qu’on ne le pense : « où » et « avec quel compte » exécuter.

  • Le principe est d’exécuter un balayage à grande échelle directement sur le serveur. Une énumération récursive via un chemin UNC (\\fs01\share) transforme chaque récupération de métadonnées de fichier en un aller-retour réseau, ce qui fait changer le temps nécessaire d’un ordre de grandeur à l’échelle de plusieurs centaines de milliers de fichiers. Si vous voulez le faire depuis un poste d’administration, il est plus pratique de faire tourner le script côté serveur via PowerShell Remoting et de ne rapatrier que le CSV de résultat. Pour la méthode, consultez « Introduction à PowerShell Remoting (WinRM) », publié en même temps.
  • Le résultat n’est qu’un instantané des droits du compte d’exécution. Un emplacement inaccessible échappe aussi bien à l’énumération qu’à Get-Acl. Menez l’investigation avec un compte disposant de droits d’administration, et incluez malgré tout dans le rapport, comme « emplacements non examinés », les endroits qui ont quand même été refusés (denied-paths.txt du chapitre 2).
  • En cas d’exécution via UNC, méfiez-vous aussi des pièges liés aux identifiants et à la session. Une lettre de lecteur étant propre à la session d’ouverture de session, un Z: attribué pendant une session interactive n’est pas visible depuis une tâche du Planificateur de tâches. Par ailleurs, l’erreur 1219 (ERROR_SESSION_CREDENTIAL_CONFLICT) est une erreur inhérente à la spécification : il n’est pas permis qu’un même utilisateur se connecte au même serveur (ou partage) avec plusieurs noms d’utilisateur différents.16 Si vous essayez de vous connecter au même serveur de fichiers avec deux types d’identifiants — « uniquement le partage d’investigation avec le compte administrateur, le reste avec mon propre compte » — la deuxième connexion échoue pour cette raison. Si vous investiguez via UNC, alignez les connexions existantes sur un seul jeu d’identifiants avant de lancer le script. La vue d’ensemble de ces pièges est réunie dans « Les pièges des lecteurs réseau et des chemins UNC ».
  • Mesurez le temps nécessaire dans votre propre environnement, pas avec les chiffres de quelqu’un d’autre. Le type de disque, le nombre de fichiers, l’analyse en temps réel de l’antivirus et la profondeur de la hiérarchie font tous fortement varier le résultat. Avant de lancer sur l’ensemble, il est plus fiable de choisir un dossier de service représentatif, de mesurer concrètement, puis d’extrapoler au nombre de fichiers de tout le partage. En effectuant la même mesure à la fois via UNC et sur le serveur, vous pouvez aussi démontrer par des chiffres s’il faut ou non exécuter sur le serveur.
# Choisir un dossier de service représentatif, mesurer concrètement la vitesse de balayage,
# puis en tirer une estimation pour l'ensemble
# (choisir un dossier sans le moindre fichier ferait échouer la division finale : choisissez
# un endroit avec des données réelles)
$sample = 'D:\share\sales'
$start  = Get-Date
$count  = (Get-ChildItem -LiteralPath $sample -Recurse -File `
    -ErrorAction SilentlyContinue | Measure-Object).Count
$sec    = ((Get-Date) - $start).TotalSeconds
'{0} fichiers énumérés en {1:N1} s -> {2:N1} s pour 10 000 fichiers' -f `
    $count, $sec, ($sec / $count * 10000)

Cette estimation vaut pour les traitements centrés sur l’énumération (l’agrégation de capacité du chapitre 2, la liste des fichiers anciens du chapitre 3, la récupération des ACL du chapitre 4). Seul le calcul de hachage du chapitre 3 voit son temps déterminé non par le nombre de fichiers, mais par le nombre total d’octets : estimez-le séparément.

7. Bonnes pratiques (tableau de décision)

Point Options Critère de décision
Périmètre de l’analyse de capacité Tout d’un coup / Par dossier au premier niveau Repérer d’abord grossièrement à un niveau peu profond, puis approfondir uniquement les endroits lourds12
Traitement des erreurs Arrêt / Ignorer / Enregistrer et continuer SilentlyContinue + ErrorVariable pour « continuer tout en enregistrant tout ». Signaler les zones refusées dans le rapport3
Critère pour les fichiers anciens LastAccessTime / LastWriteTime La date d’accès n’est pas retenue, la mise à jour étant désactivée dans de nombreux environnements. Convenir avec le service de la date de modification + d’une période45
Détection des doublons Hachage de tous les fichiers / Filtrer par taille avant de hacher Le hachage coûte cher en E/S. Ne le calculer que pour les groupes de même taille6
Profondeur de l’investigation ACL Tous les dossiers / Niveaux peu profonds + emplacements avec rupture d’héritage Si l’héritage est intact, regarder le parent suffit. Se concentrer sur IsInherited=False et Protected=True78
Outil de modification des droits icacls / Set-Acl icacls pour les modifications standard et la sauvegarde. Set-Acl + -WhatIf pour un traitement en masse avec conditions1312
Exécution de la suppression Suppression immédiate / Quarantaine → observation → suppression Après vérification de la liste par le service, déplacer d’abord vers un dossier de quarantaine, puis supprimer après une période donnée sans problème

8. Conclusion

  • Le rangement d’un serveur de fichiers commence par l’inventaire, pas par la suppression. Constituez en CSV les quatre livrables — classement de la capacité au premier niveau, liste des fichiers anciens, candidats en double, registre ACL — et servez-vous-en pour discuter avec le service.
  • En contrepartie de ne pas interrompre le balayage avec SilentlyContinue, enregistrez systématiquement avec ErrorVariable les emplacements refusés. Un emplacement qui n’a pas pu être compté n’est pas « zéro », il est « inconnu ».
  • Le critère d’ancienneté repose sur LastWriteTime. La date de dernier accès NTFS n’est pas fiable, sa mise à jour étant désactivée dans de nombreux environnements.
  • Pour les doublons, filtrez par taille avant d’utiliser Get-FileHash (SHA256 par défaut). Une correspondance est un « candidat » : la décision de conserver revient au métier.
  • Exportez en CSV la propriété Access de Get-Acl pour les ACL, et vérifiez en priorité les attributions directes à des individus, les entrées réduites à un SID, et les ruptures d’héritage (AreAccessRulesProtected). Il n’est pas nécessaire d’approfondir le SDDL.
  • La modification suit l’ordre : sauvegarde avec icacls /save → -WhatIf ou vérification de la cible → application → vérification a posteriori. Menez les balayages à grande échelle sur le serveur (ou via Remoting), avec des droits d’administration.

Articles connexes

Domaines de conseil associés

合同会社小村ソフト (Komura Software LLC) prend en charge l’aide à la création de scripts d’inventaire de la capacité et des droits d’un serveur de fichiers, l’investigation liée à la refonte de la conception des droits, ainsi que l’automatisation des rapports périodiques (jusqu’à l’exploitation via le Planificateur de tâches). Les consultations dès le stade « je veux d’abord chiffrer l’état actuel » sont également les bienvenues.

Références

  1. Microsoft Learn, Get-ChildItem. Sur l’énumération d’éléments par Get-ChildItem, la récursion avec -Recurse et la limitation de profondeur avec -Depth, et sur la recommandation d’utiliser -LiteralPath pour désigner la cible avec -Recurse afin d’éviter l’interprétation des caractères génériques.  2 3

  2. Microsoft Learn, Measure-Object. Sur le fait que Measure-Object peut calculer, avec -Property Length -Sum entre autres, la somme, le maximum, le minimum et la moyenne d’une taille de fichier, ainsi qu’un exemple d’agrégation des fichiers d’un répertoire combiné avec Get-ChildItem.  2 3

  3. Microsoft Learn, about_CommonParameters. Sur le fait que -ErrorAction SilentlyContinue supprime l’affichage de l’erreur et continue l’exécution, que -ErrorVariable stocke les enregistrements d’erreur dans la variable indiquée, et que préfixer le nom de la variable par + en fait un ajout plutôt qu’un remplacement.  2 3 4

  4. Microsoft Learn, fsutil behavior. Sur le fait que l’activation/désactivation de la mise à jour de la date de dernier accès (Last Access Time) de NTFS est contrôlée par le paramètre disablelastaccess de fsutil behavior et par la valeur de registre NtfsDisableLastAccessUpdate, et que cette désactivation existe pour accélérer l’accès aux fichiers/répertoires.  2 3

  5. Microsoft Learn, [MS-FSA]: Appendix A: Product Behavior. Sur le fait que la mise à jour de la date de dernier accès de NTFS/ReFS est désactivée par défaut depuis Windows Vista, qu’elle est gérée au niveau système à partir de Windows 10 v1803, et que sur les systèmes serveur, la mise à jour de la date de dernier accès est toujours désactivée.  2 3

  6. Microsoft Learn, Get-FileHash. Sur le fait que l’algorithme par défaut de Get-FileHash est SHA256, que deux fichiers dont la valeur de hachage correspond peuvent être considérés comme ayant un contenu identique, et que la valeur de hachage ne change pas même si le nom de fichier ou l’extension changent.  2 3

  7. Microsoft Learn, Get-Acl. Sur le fait que Get-Acl récupère le descripteur de sécurité d’un fichier ou d’une ressource, qu’il affiche par défaut la liste des entrées de contrôle d’accès de la DACL (Access), et qu’il peut aussi être récupéré sous forme SDDL (propriété Sddl).  2 3 4 5 6

  8. Microsoft Learn, ObjectSecurity.AreAccessRulesProtected Property. Sur le fait que la propriété AreAccessRulesProtected indique si la DACL du descripteur de sécurité est protégée (ne reçoit pas l’héritage du parent).  2 3

  9. Microsoft Learn, Get-SmbShareAccess. Sur le fait que Get-SmbShareAccess est une commande qui récupère l’ACL d’un partage SMB (les principaux de sécurité auxquels des droits d’accès au partage ont été accordés, ainsi que l’autorisation/le refus et les droits). 

  10. Microsoft Learn, about_Character_Encoding. Sur le fait que Windows PowerShell (5.1) n’a pas un encodage par défaut cohérent d’une cmdlet à l’autre, qu’Export-Csv crée le fichier en ASCII, et que PowerShell 6 et versions ultérieures ont unifié la valeur par défaut à utf8NoBOM.  2

  11. Microsoft Learn, Export-Csv. Sur le fait qu’Export-Csv crée un fichier CSV dont chaque colonne correspond à une propriété d’un objet, que la valeur par défaut de -Encoding sous les versions PowerShell 7 est UTF8NoBOM, et qu’il est possible de préciser explicitement UTF8BOM et d’autres valeurs.  2

  12. Microsoft Learn, Set-Acl. Sur le fait que Set-Acl modifie l’ACL de la cible en utilisant comme modèle le descripteur de sécurité passé via AclObject, qu’il est compatible -WhatIf/-Confirm, sur la procédure consistant à créer un FileSystemAccessRule puis à l’ajouter avec SetAccessRule(), et sur un exemple de désactivation de l’héritage avec SetAccessRuleProtection() (avec la possibilité de conserver les règles héritées existantes).  2 3 4 5

  13. Microsoft Learn, icacls. Sur le fait que /reset « remplace l’ACL du fichier cible par l’ACL d’héritage par défaut », et que /q supprime le message de succès.  2 3 4 5 6

  14. Microsoft Learn, Maximum Path Length Limitation. Sur le fait que la limite MAX_PATH de longueur de chemin de l’API Windows est de 260 caractères, et que cette limite n’est assouplie pour de nombreuses fonctions Win32 que lorsque le paramètre de registre LongPathsEnabled et la déclaration longPathAware côté application sont tous deux réunis. 

  15. Microsoft Learn, Permissions on copying and moving files and folders. Sur le fait qu’un objet hérite par défaut des droits d’accès du parent de destination ou de création, que la seule exception est le déplacement vers un autre dossier au sein du même volume — cas où les droits d’accès d’origine sont conservés —, et que la copie ou le déplacement vers un autre volume entraîne l’héritage des droits d’accès du dossier de destination.  2

  16. Microsoft Learn, System Error Codes (1000-1299). Sur le fait que l’erreur 1219 (ERROR_SESSION_CREDENTIAL_CONFLICT) signifie qu’« il n’est pas permis qu’un même utilisateur établisse plusieurs connexions vers le même serveur ou la même ressource partagée en utilisant plusieurs noms d’utilisateur ». 

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Comment calculer la taille totale de chaque dossier avec PowerShell ?
Énumérez les fichiers avec Get-ChildItem et -Recurse, puis totalisez avec Measure-Object -Property Length -Sum. Plutôt que d'agréger l'ensemble d'un coup, il est plus pratique de commencer par agréger dossier par dossier au premier niveau, juste sous la racine du partage, pour repérer les « endroits qui pèsent lourd ». Combinez cela avec -ErrorAction SilentlyContinue et -ErrorVariable, et enregistrez systématiquement les emplacements qui n'ont pas pu être comptés à cause d'un refus d'accès. Si vous faites disparaître silencieusement les erreurs, les emplacements manquants dans l'agrégation apparaîtront comme ayant une « taille nulle ».
Peut-on utiliser LastAccessTime (date du dernier accès) pour identifier les fichiers anciens ?
Ce n'est pas recommandé. Sous NTFS, la mise à jour de la date du dernier accès est souvent désactivée par défaut (ou gérée au niveau système) pour des raisons de performance, si bien qu'une date peut rester ancienne même si le fichier a réellement été lu. Pour l'inventaire, utilisez plutôt LastWriteTime (date de dernière modification) comme référence, et présentez-la aux services comme « le temps écoulé depuis la dernière modification du contenu » — c'est plus sûr. Ne prenez jamais de décision d'archivage de façon automatique : faites toujours vérifier la liste par les personnes concernées avant d'avancer.
Que peut-on savoir avec Get-Acl ? Faut-il lire le SDDL ?
Get-Acl récupère le descripteur de sécurité d'un fichier ou d'un dossier, et sa propriété Access permet de lire « qui » (IdentityReference), « quoi » (FileSystemRights), « autorisation ou refus » (AccessControlType) et « hérité ou attribué directement » (IsInherited). AreAccessRulesProtected indique en outre si l'héritage est coupé pour ce dossier. Il existe aussi un format sous forme de chaîne appelé SDDL, mais pour un objectif d'inventaire, exporter la propriété Access en CSV est plus facile à partager avec les parties prenantes, et il n'est pas nécessaire d'approfondir le SDDL.
Pour modifier une ACL, faut-il utiliser Set-Acl de PowerShell ou icacls ?
Le repère de base est d'utiliser Get-Acl, qui traite les résultats comme des objets, pour l'investigation et les rapports, et icacls comme premier choix pour les modifications concrètes. icacls permet de sauvegarder l'ACL dans un fichier avec /save et de la restaurer telle quelle avec /restore, ce qui simplifie la sauvegarde avant modification et le retour en arrière. Si vous utilisez Set-Acl, la procédure se déroule en trois étapes — récupération avec Get-Acl, modification des règles, puis application — et vous vérifiez la cible avec -WhatIf avant d'exécuter. Dans tous les cas, le principe est d'avancer vers la modification seulement après que l'inventaire et la vérification par les services concernés sont terminés.
Peut-on mener une analyse d'un serveur de fichiers via un chemin UNC ?
Pour un petit volume, cela ne pose pas de problème, mais un balayage complet à l'échelle de plusieurs centaines de milliers de fichiers devient nettement plus lent en passant par le réseau. Dans la mesure du possible, il est plus efficace d'exécuter directement sur le serveur de fichiers (ou via PowerShell Remoting) et de ne rapatrier que le CSV de résultat. Par ailleurs, le résultat du balayage dépend des droits du compte d'exécution : les dossiers inaccessibles disparaissent de l'énumération. Exécutez donc avec un compte administrateur, puis enregistrez malgré tout les emplacements refusés et incluez-les dans le rapport comme des « emplacements non examinés ».

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog