From 946057232d72c0009a2a99b08caf27d277d144c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20QUEROL?= <2+kawa@not.obvious> Date: Wed, 10 Jun 2026 14:35:10 +0200 Subject: [PATCH] Update filer-manager.ps1 --- filer-manager.ps1 | 652 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 597 insertions(+), 55 deletions(-) diff --git a/filer-manager.ps1 b/filer-manager.ps1 index e8b8b68..dbb5f8b 100644 --- a/filer-manager.ps1 +++ b/filer-manager.ps1 @@ -1,8 +1,8 @@ -#Requires -Version 5.1 +#Requires -Version 5.1 <# .SYNOPSIS Filer Manager - analyse des dossiers Windows et rapporte les tailles des - dossiers, les chemins trop longs et les permissions NTFS, avec un export HTML. + dossiers, les chemins trop longs et les permissions NTFS, avec export HTML et CSV. .DESCRIPTION S'exécute avec une interface graphique WinForms par défaut. Peut aussi @@ -19,6 +19,12 @@ .PARAMETER Output Chemin du rapport HTML à écrire en mode headless. +.PARAMETER CsvOutput + Chemin de base du/des rapport(s) CSV à écrire en mode headless. Un fichier CSV + distinct est produit par catégorie ayant des données (p. ex. base.csv -> + base-tree.csv, base-permissions.csv, ...). Peut être combiné avec -Output pour + écrire HTML et CSV en même temps, ou utilisé seul pour un export CSV uniquement. + .PARAMETER MaxPathLength Longueur de chemin (caractères) à partir de laquelle un élément est signalé comme « trop long ». Par défaut 260 (Windows MAX_PATH). @@ -32,6 +38,19 @@ Inclure les fichiers individuels (pas seulement les dossiers) dans l'arborescence des tailles. +.PARAMETER HideInheritedChildPerms + Omettre des rapports les permissions héritées portées par les dossiers + enfants (les entrées héritées qui ne font que recopier celles du parent). Les + permissions héritées des dossiers racines sont conservées, car le parent d'une + racine est hors analyse et ces entrées sont la seule trace des droits en vigueur. + +.PARAMETER HideSystemPrincipals + Omettre des rapports les permissions portées par des comptes/groupes système + et intégrés bien connus (p. ex. NT AUTHORITY\SYSTEM, BUILTIN\Administrators, + CREATOR OWNER, NT SERVICE\*). La détection se fait par SID (indépendante de la + langue), avec un repli sur le nom. Utile pour ne garder que les identités + métier dans l'audit des permissions. + .PARAMETER GrantAccess Lorsqu'un dossier/fichier ne peut pas être lu (accès refusé), s'approprie automatiquement la propriété pour Administrators et accorde le FullControl à @@ -68,9 +87,12 @@ param( [string[]]$Path, [string]$Output, + [string]$CsvOutput, [int]$MaxPathLength = 260, [int]$PermissionDepth = 1, [switch]$IncludeFilesInTree, + [switch]$HideInheritedChildPerms, + [switch]$HideSystemPrincipals, [switch]$GrantAccess, [switch]$KeepGrants, [switch]$NoGui, @@ -110,6 +132,56 @@ function Test-IsAccessDenied { return $false } +function Resolve-PrincipalSid { + # Best-effort translation of an ACE IdentityReference (an NTAccount or a + # SecurityIdentifier) to its SID string. Returns $null when unresolvable + # (e.g. an orphaned account from a deleted domain user). + param($IdentityReference) + try { + if ($IdentityReference -is [System.Security.Principal.SecurityIdentifier]) { return $IdentityReference.Value } + return $IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value + } catch { + # The reference may already be a raw SID string (unresolved account). + try { return (New-Object System.Security.Principal.SecurityIdentifier ([string]$IdentityReference)).Value } catch { return $null } + } +} + +function Test-IsSystemPrincipal { + # True for well-known built-in / system accounts and groups, so the audit can + # focus on real business identities. Detection is by SID prefix (locale- + # independent), with a name-based fallback for the few cases where the SID + # could not be resolved. Examples: NT AUTHORITY\SYSTEM (S-1-5-18), + # BUILTIN\Administrators (S-1-5-32-544), CREATOR OWNER (S-1-3-*), + # NT SERVICE\TrustedInstaller (S-1-5-80-*). + param([string]$Identity, [string]$Sid) + + if ($Sid) { + switch -Regex ($Sid) { + '^S-1-5-(18|19|20)$' { return $true } # SYSTEM / LOCAL SERVICE / NETWORK SERVICE + '^S-1-5-(6|9|17)$' { return $true } # SERVICE / Enterprise DCs / IUSR + '^S-1-5-32-' { return $true } # BUILTIN\* (Administrators, Users, ...) + '^S-1-5-(80|83|90|96)-' { return $true } # NT SERVICE / VM / Window Manager / Font driver + '^S-1-3-' { return $true } # CREATOR OWNER / CREATOR GROUP + } + } + + # Name fallback: only when the SID could not be resolved (offline domain, + # broken trust). When a SID is known it is authoritative, so groups like + # Authenticated Users (S-1-5-11) are intentionally not treated as system even + # though their name carries the NT AUTHORITY prefix. Covers common + # English/French/German forms of the built-in domains and standalone principals. + if (-not $Sid -and $Identity) { + $id = $Identity.Trim() + foreach ($p in @('NT AUTHORITY\', 'AUTORITE NT\', "AUTORIT$([char]0xC9) NT\", 'NT-AUTORIT', 'BUILTIN\', 'NT SERVICE\', 'AUTORITE DE SECURITE')) { + if ($id.StartsWith($p, [System.StringComparison]::OrdinalIgnoreCase)) { return $true } + } + foreach ($e in @('SYSTEM', 'CREATOR OWNER', 'CREATOR GROUP', 'TrustedInstaller')) { + if ($id.Equals($e, [System.StringComparison]::OrdinalIgnoreCase)) { return $true } + } + } + return $false +} + function Grant-AdminAccess { <# Seizes ownership for Administrators and grants BUILTIN\Administrators FullControl on a single item (no recursion), so a denied path can be @@ -318,6 +390,7 @@ function Get-FolderPermissions { Folder = $Path Owner = $acl.Owner Identity = [string]$ace.IdentityReference + Sid = Resolve-PrincipalSid $ace.IdentityReference Rights = [string]$ace.FileSystemRights Type = [string]$ace.AccessControlType Inherited = $ace.IsInherited @@ -420,6 +493,32 @@ function Invoke-FilerScan { } } +function Get-FilerRootPathSet { + # Case-insensitive set of the scan-root full paths. Used to decide which + # permission rows sit on a scan root (vs. a child folder). + param($Scan) + $set = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) + foreach ($r in $Scan.Roots) { [void]$set.Add([string]$r.FullPath) } + return $set +} + +function Select-FilerPerms { + # Returns the scan's permission ACEs, optionally dropping inherited ACEs that + # sit on a child folder and/or ACEs held by well-known system principals. + # Inherited ACEs on the scan roots are always kept: a root's parent is outside + # the scan, so those entries are the only record of the permissions in effect there. + param($Scan, [bool]$HideInheritedChildPerms, [bool]$HideSystemPrincipals) + $rows = @($Scan.Permissions) + if ($HideInheritedChildPerms) { + $roots = Get-FilerRootPathSet -Scan $Scan + $rows = @($rows | Where-Object { (-not $_.Inherited) -or $roots.Contains([string]$_.Folder) }) + } + if ($HideSystemPrincipals) { + $rows = @($rows | Where-Object { -not (Test-IsSystemPrincipal -Identity $_.Identity -Sid $_.Sid) }) + } + return $rows +} + function ConvertTo-FilerHtmlReport { param( [Parameter(Mandatory)] $Scan, @@ -427,7 +526,11 @@ function ConvertTo-FilerHtmlReport { # Which report categories to include. 'All' (default) emits everything; # otherwise pass any combination of Tree, LongPaths, Permissions, Grants, Errors. [ValidateSet('All', 'Tree', 'LongPaths', 'Permissions', 'Grants', 'Errors')] - [string[]]$Categories = @('All') + [string[]]$Categories = @('All'), + # When set, inherited permissions on child folders are omitted (roots keep theirs). + [bool]$HideInheritedChildPerms = $false, + # When set, ACEs held by well-known system/built-in principals are omitted. + [bool]$HideSystemPrincipals = $false ) function _enc([string]$s) { [System.Net.WebUtility]::HtmlEncode($s) } @@ -482,20 +585,63 @@ function ConvertTo-FilerHtmlReport { if ($Scan.LongPaths.Count -eq 0) { [void]$longRows.Append("
| Identité | Droits | Type | Hérité |
|---|---|---|---|
| $(_enc $ace.Identity) | $(_enc $ace.Rights) | $(_enc $ace.Type) | $($ace.Inherited) |
| Identité | Droits | Type | Hérité |
|---|---|---|---|
| $(_enc $ace.Identity) | $(_enc $ace.Rights) | $(_enc $ace.Type) | $($ace.Inherited) |
Aucune permission collectée.
") } } @@ -575,6 +721,12 @@ function ConvertTo-FilerHtmlReport { tr.deny td { color:var(--deny); } .ok { color:var(--ok); } .muted { color:var(--mut); } details.perm { background:var(--pan2); border-radius:10px; padding:6px 10px; margin:6px 0; border-left:2px solid var(--acc); } + details.perm.top { border-left:2px solid var(--bar); } + details.perm.top > summary > .nm { font-family:Consolas,'Cascadia Code',monospace; } + details.perm .kids { margin-left:16px; } + .permtoggle { display:inline-flex; align-items:center; gap:8px; margin-bottom:14px; color:var(--mut); + font-size:13px; cursor:pointer; user-select:none; } + .permtoggle input { accent-color:var(--acc); width:15px; height:15px; cursor:pointer; } .scroll { max-height:480px; overflow:auto; } input.filter { width:100%; padding:8px 12px; margin-bottom:12px; background:var(--pan2); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:13px; } @@ -617,7 +769,11 @@ function ConvertTo-FilerHtmlReport { $(if ($wantPerm) { @"