From ec2953001f48f65141ebae6f57f57d23a2b7fb8b 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:37:39 +0200 Subject: [PATCH] Delete filer-manager.ps1 --- filer-manager.ps1 | 1762 --------------------------------------------- 1 file changed, 1762 deletions(-) delete mode 100644 filer-manager.ps1 diff --git a/filer-manager.ps1 b/filer-manager.ps1 deleted file mode 100644 index dbb5f8b..0000000 --- a/filer-manager.ps1 +++ /dev/null @@ -1,1762 +0,0 @@ -#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 export HTML et CSV. - -.DESCRIPTION - S'exécute avec une interface graphique WinForms par défaut. Peut aussi - s'exécuter sans interface (mode headless) pour la planification : - - .\filer-manager.ps1 -Path "D:\Shares\Public" -Output report.html -NoGui - - Fonctionne sur Windows PowerShell 5.1 et PowerShell 7+. - -.PARAMETER Path - Un ou plusieurs dossiers racines à analyser. S'il est fourni avec -Output (ou - avec -NoGui), le script s'exécute sans interface et écrit un rapport HTML. - -.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). - -.PARAMETER PermissionDepth - Nombre de niveaux de dossiers sous chaque racine pour lesquels collecter les - permissions NTFS. 0 = racines uniquement, 1 = racines + enfants immédiats - (par défaut), etc. - -.PARAMETER IncludeFilesInTree - 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 à - BUILTIN\Administrators, puis réessaye. Nécessite une exécution en mode élevé. - Chaque modification est consignée dans le rapport. - -.PARAMETER KeepGrants - Conserver l'accès accordé par -GrantAccess après l'analyse. Par défaut, l'outil - annule chaque modification une fois l'analyse terminée (en restaurant le - propriétaire et l'ACL d'origine là où ils ont pu être lus au préalable). - -.PARAMETER NoGui - Forcer le mode sans interface (nécessite -Path et -Output). - -.PARAMETER Category - Catégories de rapport à inclure dans le HTML. Par défaut 'All'. Passez toute - combinaison de : Tree (tailles des dossiers), LongPaths, Permissions, Grants - (journal des accès accordés), Errors. À utiliser pour garder de gros rapports - petits, p. ex. -Category Permissions ou -Category Tree,LongPaths. - -.EXAMPLE - .\filer-manager.ps1 - Lance l'interface graphique. - -.EXAMPLE - .\filer-manager.ps1 -Path "\\FILER01\Data","D:\Profiles" -Output C:\Reports\filer.html -NoGui - -.EXAMPLE - .\filer-manager.ps1 -Path "D:\Shares" -Output filer.html -NoGui -GrantAccess - Analyse D:\Shares en accordant l'accès Administrators aux éléments refusés afin - que l'analyse puisse se terminer, puis annule ces modifications ensuite. -#> -[CmdletBinding()] -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, - [ValidateSet('All', 'Tree', 'LongPaths', 'Permissions', 'Grants', 'Errors')] - [string[]]$Category = @('All') -) - -# ============================================================================ -# CORE (GUI-independent). Kept as a string so it can be dot-sourced both here -# and inside a background runspace used by the GUI to stay responsive. -# ============================================================================ -$CoreFunctions = @' -function Format-Bytes { - param([long]$Bytes) - if ($Bytes -ge 1TB) { '{0:N2} TB' -f ($Bytes / 1TB) } - elseif ($Bytes -ge 1GB) { '{0:N2} GB' -f ($Bytes / 1GB) } - elseif ($Bytes -ge 1MB) { '{0:N2} MB' -f ($Bytes / 1MB) } - elseif ($Bytes -ge 1KB) { '{0:N2} KB' -f ($Bytes / 1KB) } - else { "$Bytes B" } -} - -function Test-IsElevated { - # True when the current process is running with Administrator rights. - try { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $pr = New-Object System.Security.Principal.WindowsPrincipal($id) - return $pr.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } catch { return $false } -} - -function Test-IsAccessDenied { - # Recognise an "access denied" failure across locales and PowerShell hosts. - param($ErrorRecord) - $ex = $ErrorRecord.Exception - if ($ex -is [System.UnauthorizedAccessException]) { return $true } - if ($ErrorRecord.CategoryInfo -and $ErrorRecord.CategoryInfo.Category -eq 'PermissionDenied') { return $true } - 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 - scanned. Captures the original owner/ACL first (when readable) so the - change can be reverted later. Records every attempt in $Grants. Returns - $true when something was changed (so the caller can retry). #> - param( - [string]$Path, - [string]$Reason, - [System.Collections.ArrayList]$Grants, - [System.Collections.ArrayList]$ScanErrors - ) - - # BUILTIN\Administrators well-known SID - locale-independent (the group is - # named differently on non-English systems). - $adminSid = 'S-1-5-32-544' - - # Don't act twice on the same path. - foreach ($g in $Grants) { if ($g.Path -eq $Path) { return $g.Success } } - - # Capture the original state for a possible revert (may be unreadable). - $origSddl = $null; $origOwner = $null - try { - $a = Get-Acl -LiteralPath $Path -ErrorAction Stop - $origSddl = $a.Sddl; $origOwner = [string]$a.Owner - } catch { } - - $isDir = $true - try { $isDir = [bool]((Get-Item -LiteralPath $Path -Force -ErrorAction Stop).PSIsContainer) } catch { } - - $changes = New-Object System.Collections.ArrayList - $errText = $null - - # 1) Seize ownership for Administrators (icacls enables the needed privilege - # itself and, unlike takeown, has no locale-specific confirmation prompt). - $null = & icacls.exe $Path /setowner "*$adminSid" /C /Q 2>&1 - if ($LASTEXITCODE -eq 0) { [void]$changes.Add('Propriétaire défini -> Administrators') } - else { $errText = "setowner a échoué (code $LASTEXITCODE)" } - - # 2) Grant Administrators FullControl (inheritable on directories). - $perm = if ($isDir) { "*${adminSid}:(OI)(CI)F" } else { "*${adminSid}:F" } - $null = & icacls.exe $Path /grant $perm /C /Q 2>&1 - if ($LASTEXITCODE -eq 0) { [void]$changes.Add('Administrators -> FullControl accordé') } - else { if ($errText) { $errText += '; ' }; $errText += "grant a échoué (code $LASTEXITCODE)" } - - $success = ($changes.Count -gt 0) - - [void]$Grants.Add([pscustomobject]@{ - Path = $Path - IsDir = $isDir - Reason = $Reason - Changes = ($changes -join '; ') - OriginalOwner = $origOwner - OriginalSddl = $origSddl - Success = $success - Error = $errText - Reverted = $false - RevertError = $null - }) - - if (-not $success -and $errText) { - [void]$ScanErrors.Add([pscustomobject]@{ Path = $Path; Error = "Octroi automatique échoué : $errText" }) - } - return $success -} - -function Restore-Grants { - <# Reverts the changes made by Grant-AdminAccess, children before parents. - Restores the full original security descriptor (owner + ACL) when it was - captured; otherwise removes only the Administrators ACE we added. #> - param( - [System.Collections.ArrayList]$Grants, - [System.Collections.ArrayList]$ScanErrors, - [hashtable]$Progress - ) - $adminSid = 'S-1-5-32-544' - for ($i = $Grants.Count - 1; $i -ge 0; $i--) { - $g = $Grants[$i] - if (-not $g.Success) { continue } - if ($Progress) { $Progress.Status = "Annulation de l'accès : $($g.Path)" } - - $done = $false; $err = $null - if ($g.OriginalSddl) { - try { - if ($g.IsDir) { $sec = New-Object System.Security.AccessControl.DirectorySecurity } - else { $sec = New-Object System.Security.AccessControl.FileSecurity } - $sec.SetSecurityDescriptorSddlForm($g.OriginalSddl) - Set-Acl -LiteralPath $g.Path -AclObject $sec -ErrorAction Stop - $done = $true - } catch { $err = "Échec de la restauration Set-Acl : $($_.Exception.Message)" } - } - if (-not $done) { - # Best-effort fallback: drop the ACE we added and put the owner back. - try { - $null = & icacls.exe $g.Path /remove:g "*$adminSid" /C /Q 2>&1 - if ($g.OriginalOwner) { $null = & icacls.exe $g.Path /setowner $g.OriginalOwner /C /Q 2>&1 } - if (-not $g.OriginalSddl) { - $done = $true - $err = "l'ACL d'origine était illisible ; seul l'ACE Administrators ajouté a été supprimé" - } - } catch { if ($err) { $err += '; ' }; $err += "Repli icacls échoué : $($_.Exception.Message)" } - } - - $g.Reverted = $done - $g.RevertError = $err - if ($err) { [void]$ScanErrors.Add([pscustomobject]@{ Path = $g.Path; Error = "Annulation : $err" }) } - } -} - -function Get-FolderNode { - <# Recursively builds a size tree. Accumulates long paths and errors via - synchronized collections passed by the caller. #> - param( - [string]$Path, - [int]$MaxLen, - [System.Collections.ArrayList]$LongPaths, - [System.Collections.ArrayList]$ScanErrors, - [hashtable]$Progress, - [int]$Depth = 0 - ) - - $name = Split-Path -Path $Path -Leaf - if ([string]::IsNullOrEmpty($name)) { $name = $Path } # e.g. a drive root - - $node = [ordered]@{ - Name = $name - FullPath = $Path - Size = [long]0 - FileCount = 0 - FolderCount = 0 - Depth = $Depth - Children = New-Object System.Collections.ArrayList - } - - if ($Progress) { $Progress.Status = "Analyse : $Path" } - - if ($Path.Length -ge $MaxLen) { - [void]$LongPaths.Add([pscustomobject]@{ Type = 'Dossier'; Length = $Path.Length; Path = $Path }) - } - - $entries = $null - $granted = $false - while ($true) { - try { - $entries = Get-ChildItem -LiteralPath $Path -Force -ErrorAction Stop - break - } - catch { - if ($GrantAccessFlag -and -not $granted -and (Test-IsAccessDenied $_)) { - $granted = $true - if ($Progress) { $Progress.Status = "Octroi de l'accès : $Path" } - if (Grant-AdminAccess -Path $Path -Reason 'énumérer le dossier' -Grants $GrantList -ScanErrors $ScanErrors) { - continue # retry once, now that access has been granted - } - } - [void]$ScanErrors.Add([pscustomobject]@{ Path = $Path; Error = $_.Exception.Message }) - return [pscustomobject]$node - } - } - - foreach ($entry in $entries) { - if ($entry.PSIsContainer) { - $child = Get-FolderNode -Path $entry.FullName -MaxLen $MaxLen ` - -LongPaths $LongPaths -ScanErrors $ScanErrors ` - -Progress $Progress -Depth ($Depth + 1) - $node.Size += $child.Size - $node.FileCount += $child.FileCount - $node.FolderCount += 1 + $child.FolderCount - [void]$node.Children.Add($child) - } - else { - $len = 0 - try { $len = [long]$entry.Length } catch { $len = 0 } - $node.Size += $len - $node.FileCount += 1 - if ($entry.FullName.Length -ge $MaxLen) { - [void]$LongPaths.Add([pscustomobject]@{ Type = 'Fichier'; Length = $entry.FullName.Length; Path = $entry.FullName }) - } - if ($IncludeFilesInTreeFlag) { - [void]$node.Children.Add([pscustomobject]@{ - Name = $entry.Name; FullPath = $entry.FullName; Size = $len - FileCount = 0; FolderCount = 0; Depth = $Depth + 1; IsFile = $true - Children = (New-Object System.Collections.ArrayList) - }) - } - } - } - - return [pscustomobject]$node -} - -function Get-FolderPermissions { - param( - [string]$Path, - [int]$Depth, - [System.Collections.ArrayList]$ScanErrors, - [int]$Current = 0 - ) - $results = New-Object System.Collections.ArrayList - $granted = $false - while ($true) { - try { - $acl = Get-Acl -LiteralPath $Path -ErrorAction Stop - foreach ($ace in $acl.Access) { - [void]$results.Add([pscustomobject]@{ - 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 - }) - } - break - } - catch { - if ($GrantAccessFlag -and -not $granted -and (Test-IsAccessDenied $_)) { - $granted = $true - if (Grant-AdminAccess -Path $Path -Reason 'lire les permissions' -Grants $GrantList -ScanErrors $ScanErrors) { - continue # retry once, now that the ACL is readable - } - } - [void]$ScanErrors.Add([pscustomobject]@{ Path = $Path; Error = "ACL : $($_.Exception.Message)" }) - break - } - } - - if ($Current -lt $Depth) { - $subDirs = $null - try { $subDirs = Get-ChildItem -LiteralPath $Path -Directory -Force -ErrorAction Stop } catch { $subDirs = @() } - foreach ($d in $subDirs) { - $child = Get-FolderPermissions -Path $d.FullName -Depth $Depth -ScanErrors $ScanErrors -Current ($Current + 1) - foreach ($r in $child) { [void]$results.Add($r) } - } - } - return $results -} - -function Invoke-FilerScan { - param( - [string[]]$Paths, - [int]$MaxPathLength = 260, - [int]$PermissionDepth = 1, - [bool]$IncludeFilesInTree = $false, - [bool]$GrantAccess = $false, - [bool]$RevertGrants = $true, - [hashtable]$Progress - ) - - $script:IncludeFilesInTreeFlag = $IncludeFilesInTree - $script:GrantAccessFlag = $GrantAccess - $script:GrantList = New-Object System.Collections.ArrayList - - $longPaths = New-Object System.Collections.ArrayList - $scanErrors = New-Object System.Collections.ArrayList - $permissions = New-Object System.Collections.ArrayList - $roots = New-Object System.Collections.ArrayList - - foreach ($p in $Paths) { - if (-not (Test-Path -LiteralPath $p)) { - [void]$scanErrors.Add([pscustomobject]@{ Path = $p; Error = 'Chemin introuvable' }) - continue - } - $fullPath = (Resolve-Path -LiteralPath $p).Path - $node = Get-FolderNode -Path $fullPath -MaxLen $MaxPathLength ` - -LongPaths $longPaths -ScanErrors $scanErrors -Progress $Progress - [void]$roots.Add($node) - - if ($Progress) { $Progress.Status = "Lecture des permissions : $fullPath" } - $perms = Get-FolderPermissions -Path $fullPath -Depth $PermissionDepth -ScanErrors $scanErrors - foreach ($r in $perms) { [void]$permissions.Add($r) } - } - - # Revert the access we granted, once the scan (and ACL reads) are done. - if ($GrantAccess -and $RevertGrants -and $script:GrantList.Count -gt 0) { - if ($Progress) { $Progress.Status = 'Annulation des accès accordés...' } - Restore-Grants -Grants $script:GrantList -ScanErrors $scanErrors -Progress $Progress - } - - $totalSize = ($roots | Measure-Object -Property Size -Sum).Sum - if (-not $totalSize) { $totalSize = 0 } - - $grantsMade = @($script:GrantList | Where-Object { $_.Success }) - - return [pscustomobject]@{ - Roots = $roots - LongPaths = ($longPaths | Sort-Object Length -Descending) - Permissions = $permissions - Grants = $script:GrantList - Errors = $scanErrors - Settings = [pscustomobject]@{ - MaxPathLength = $MaxPathLength - PermissionDepth = $PermissionDepth - IncludeFilesInTree = $IncludeFilesInTree - GrantAccess = $GrantAccess - RevertGrants = $RevertGrants - } - Stats = [pscustomobject]@{ - TotalSize = [long]$totalSize - TotalFiles = ($roots | Measure-Object -Property FileCount -Sum).Sum - TotalFolders = ($roots | Measure-Object -Property FolderCount -Sum).Sum - LongPaths = $longPaths.Count - Errors = $scanErrors.Count - Grants = $grantsMade.Count - } - Computer = $env:COMPUTERNAME - GeneratedAt = (Get-Date) - } -} - -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, - [Parameter(Mandatory)] [string]$Path, - # 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'), - # 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) } - - # Resolve the requested categories into per-section switches. - $all = ($Categories -contains 'All') -or ($Categories.Count -eq 0) - $wantTree = $all -or ($Categories -contains 'Tree') - $wantLong = $all -or ($Categories -contains 'LongPaths') - $wantPerm = $all -or ($Categories -contains 'Permissions') - $wantGrants = $all -or ($Categories -contains 'Grants') - $wantErrors = $all -or ($Categories -contains 'Errors') - - $sb = New-Object System.Text.StringBuilder - - # ---- recursive tree renderer (folders sorted largest-first) ---- - function _renderNode { - param($Node, [long]$ParentSize, [System.Text.StringBuilder]$Out) - $pct = if ($ParentSize -gt 0) { [math]::Round(($Node.Size / $ParentSize) * 100, 1) } else { 100 } - $sizeStr = Format-Bytes $Node.Size - $isFile = ($Node.PSObject.Properties.Name -contains 'IsFile' -and $Node.IsFile) - $icon = if ($isFile) { '📄' } else { '📁' } - $meta = if ($isFile) { '' } else { " $($Node.FolderCount) dossiers, $($Node.FileCount) fichiers" } - $kids = @($Node.Children | Where-Object { $_ } ) - - $summary = "$icon $(_enc $Node.Name) $sizeStr" + - "$pct%$meta" - - if ($kids.Count -gt 0) { - $open = if ($Node.Depth -eq 0) { ' open' } else { '' } - [void]$Out.Append("$summary
") - foreach ($c in ($kids | Sort-Object @{E={$_.Size}} -Descending)) { - _renderNode -Node $c -ParentSize $Node.Size -Out $Out - } - [void]$Out.Append("
") - } - else { - [void]$Out.Append("
$summary
") - } - } - - $treeSb = New-Object System.Text.StringBuilder - if ($wantTree) { - foreach ($root in $Scan.Roots) { _renderNode -Node $root -ParentSize $root.Size -Out $treeSb } - } - - # ---- long paths ---- - $longRows = New-Object System.Text.StringBuilder - if ($wantLong) { - foreach ($lp in $Scan.LongPaths) { - [void]$longRows.Append("$($lp.Length)$(_enc $lp.Type)$(_enc $lp.Path)") - } - if ($Scan.LongPaths.Count -eq 0) { [void]$longRows.Append("Aucun chemin égal ou supérieur à $($Scan.Settings.MaxPathLength) caractères. ✅") } - } - - # ---- permissions (nested folder tree; every level is collapsible) ---- - $permSb = New-Object System.Text.StringBuilder - if ($wantPerm) { - # Canonicalise so 8.3 / trailing-slash / casing differences between a folder - # and its parent (from Get-ChildItem) still line up when building the tree. - function _canon([string]$p) { try { ([System.IO.Path]::GetFullPath($p)).TrimEnd('\') } catch { $p.TrimEnd('\') } } - $byFolder = (Select-FilerPerms -Scan $Scan -HideInheritedChildPerms $HideInheritedChildPerms -HideSystemPrincipals $HideSystemPrincipals) | Group-Object Folder - - # One node per folder, keyed by canonical path. - $nodes = @{} - foreach ($grp in $byFolder) { - $canon = _canon ([string]$grp.Name) - $nodes[$canon] = [pscustomobject]@{ - Canon = $canon - Display = [string]$grp.Name - Owner = ($grp.Group | Select-Object -First 1).Owner - Aces = $grp.Group - Children = New-Object System.Collections.ArrayList - } - } - # Attach each folder to its nearest ancestor that also has permissions; the - # ones with no such ancestor are the tree's top-level (scan-root) nodes. - $tops = New-Object System.Collections.ArrayList - foreach ($node in $nodes.Values) { - $parent = [System.IO.Path]::GetDirectoryName($node.Canon) - $attached = $false - while ($parent) { - if ($nodes.ContainsKey($parent)) { [void]$nodes[$parent].Children.Add($node); $attached = $true; break } - $up = [System.IO.Path]::GetDirectoryName($parent) - if ($up -eq $parent) { break } - $parent = $up - } - if (-not $attached) { [void]$tops.Add($node) } - } - - function _renderPermNode { - param($Node, [bool]$IsTop, [System.Text.StringBuilder]$Out) - # Top-level nodes show their full path; nested ones just the leaf name. - $label = if ($IsTop) { $Node.Display } else { Split-Path -Leaf $Node.Display } - $topCls = if ($IsTop) { ' top' } else { '' } - [void]$Out.Append("
$(_enc $label) Propriétaire : $(_enc $Node.Owner)") - [void]$Out.Append("") - foreach ($ace in $Node.Aces) { - $cls = if ($ace.Type -eq 'Deny') { ' class=deny' } else { '' } - $sys = Test-IsSystemPrincipal -Identity $ace.Identity -Sid $ace.Sid - [void]$Out.Append("") - } - [void]$Out.Append("
IdentitéDroitsTypeHérité
$(_enc $ace.Identity)$(_enc $ace.Rights)$(_enc $ace.Type)$($ace.Inherited)
") - if ($Node.Children.Count -gt 0) { - [void]$Out.Append("
") - foreach ($c in ($Node.Children | Sort-Object Display)) { _renderPermNode -Node $c -IsTop $false -Out $Out } - [void]$Out.Append("
") - } - [void]$Out.Append("
") - } - - foreach ($t in ($tops | Sort-Object Display)) { _renderPermNode -Node $t -IsTop $true -Out $permSb } - if ($byFolder.Count -eq 0) { [void]$permSb.Append("

Aucune permission collectée.

") } - } - - # ---- granted access (auto-remediation log) ---- - $grantSb = New-Object System.Text.StringBuilder - $grantRows = @($Scan.Grants | Where-Object { $_.Success }) - if ($wantGrants) { - foreach ($g in $grantRows) { - if ($g.Reverted) { $state = "annulé" } - elseif ($Scan.Settings.RevertGrants) { $state = "NON annulé$(if($g.RevertError){' - ' + (_enc $g.RevertError)})" } - else { $state = "conservé" } - [void]$grantSb.Append("$(_enc $g.Path)$(_enc $g.Reason)$(_enc $g.Changes)$(_enc $g.OriginalOwner)$state") - } - } - - # ---- errors ---- - $errSb = New-Object System.Text.StringBuilder - if ($wantErrors) { - foreach ($e in $Scan.Errors) { - [void]$errSb.Append("$(_enc $e.Path)$(_enc $e.Error)") - } - } - - $rootsList = ($Scan.Roots | ForEach-Object { _enc $_.FullPath }) -join '
' - $gen = $Scan.GeneratedAt.ToString('yyyy-MM-dd HH:mm:ss') - - # When only some categories are exported, name them in the header. - $catLabels = [ordered]@{ - Tree = 'Tailles des dossiers'; LongPaths = 'Chemins trop longs'; Permissions = 'Permissions' - Grants = 'Accès accordé'; Errors = 'Erreurs' - } - $included = @() - if ($wantTree) { $included += $catLabels.Tree } - if ($wantLong) { $included += $catLabels.LongPaths } - if ($wantPerm) { $included += $catLabels.Permissions } - if ($wantGrants -and $Scan.Settings.GrantAccess) { $included += $catLabels.Grants } - if ($wantErrors -and $Scan.Errors.Count -gt 0) { $included += $catLabels.Errors } - $catNote = if ($all) { '' } else { "
Catégories : $(_enc ($included -join ', '))
" } - - $html = @" - - - -Rapport Filer Manager - $gen - - -
-

📁 Rapport Filer Manager

-
Généré le $gen · Hôte $(_enc $Scan.Computer) · - Seuil chemin trop long $($Scan.Settings.MaxPathLength) · Profondeur des permissions $($Scan.Settings.PermissionDepth)
-
Analysé : $rootsList
- $catNote -
-
-
-
$(Format-Bytes $Scan.Stats.TotalSize)
Taille totale
-
$('{0:N0}' -f $Scan.Stats.TotalFolders)
Dossiers
-
$('{0:N0}' -f $Scan.Stats.TotalFiles)
Fichiers
-
$($Scan.Stats.LongPaths)
Chemins trop longs
-
$($Scan.Stats.Errors)
Erreurs / refusés
- $(if ($Scan.Settings.GrantAccess) { "
$($Scan.Stats.Grants)
Accès accordé
" }) -
- - $(if ($wantTree) { @" -
-

📊 Tailles des dossiers

-
$($treeSb.ToString())
-
-"@ }) - - $(if ($wantLong) { @" -
-

⚠️ Noms de fichiers / chemins trop longs (≥ $($Scan.Settings.MaxPathLength) caractères)

- -
- $($longRows.ToString())
LongueurTypeChemin
-
-"@ }) - - $(if ($wantPerm) { @" -
-

🔐 Permissions

- - -
$($permSb.ToString())
-
-"@ }) - - $(if ($wantGrants -and $Scan.Settings.GrantAccess) { @" -
-

🔨 Accès accordé pour terminer l'analyse

-

Éléments qui étaient refusés, sur lesquels la propriété/le FullControl Administrators a été appliqué afin de pouvoir les analyser. - Mode : $(if ($Scan.Settings.RevertGrants) { "annuler après l'analyse" } else { 'conserver les modifications' }).

- $(if ($grantRows.Count -gt 0) { @" -
- $($grantSb.ToString())
CheminRaisonModificationPropriétaire d'origineÉtat
-"@ } else { "

Aucune modification d'accès n'était nécessaire. ✅

" }) -
-"@ }) - - $(if ($wantErrors -and $Scan.Errors.Count -gt 0) { @" -
-

❌ Erreurs et accès refusé

-
- $($errSb.ToString())
CheminErreur
-
-"@ }) -
- - - -"@ - - Set-Content -LiteralPath $Path -Value $html -Encoding UTF8 - return $Path -} - -function ConvertTo-FilerCsvReport { - <# Writes the scan results to CSV. Because the report categories have very - different shapes, each selected category with data is written to its own - file, derived from the base $Path (e.g. report.csv -> report-tree.csv, - report-permissions.csv, ...). Returns the list of files written. The list - separator follows the current culture so the files open cleanly in the - local Excel (';' on French systems, ',' elsewhere). #> - param( - [Parameter(Mandatory)] $Scan, - [Parameter(Mandatory)] [string]$Path, - [ValidateSet('All', 'Tree', 'LongPaths', 'Permissions', 'Grants', 'Errors')] - [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 - ) - - $all = ($Categories -contains 'All') -or ($Categories.Count -eq 0) - $wantTree = $all -or ($Categories -contains 'Tree') - $wantLong = $all -or ($Categories -contains 'LongPaths') - $wantPerm = $all -or ($Categories -contains 'Permissions') - $wantGrants = $all -or ($Categories -contains 'Grants') - $wantErrors = $all -or ($Categories -contains 'Errors') - - $delim = (Get-Culture).TextInfo.ListSeparator - if ([string]::IsNullOrEmpty($delim)) { $delim = ',' } - - # Derive a per-category file path from the base path. - $dir = Split-Path -Path $Path -Parent - $base = [System.IO.Path]::GetFileNameWithoutExtension($Path) - if ([string]::IsNullOrEmpty($base)) { $base = 'filer-report' } - - $written = New-Object System.Collections.ArrayList - function _writeCsv { - param($Rows, [string]$Suffix) - $rows = @($Rows | Where-Object { $null -ne $_ }) - if ($rows.Count -eq 0) { return } # don't emit empty files - $name = "$base-$Suffix.csv" - $p = if ([string]::IsNullOrEmpty($dir)) { $name } else { Join-Path $dir $name } - $rows | Export-Csv -LiteralPath $p -NoTypeInformation -Encoding UTF8 -Delimiter $delim - [void]$written.Add($p) - } - - # ---- tree (flattened depth-first, folders largest-first like the HTML) ---- - if ($wantTree) { - $treeRows = New-Object System.Collections.ArrayList - function _flattenNode { - param($Node, [string]$Root, [System.Collections.ArrayList]$Acc) - $isFile = ($Node.PSObject.Properties.Name -contains 'IsFile' -and $Node.IsFile) - [void]$Acc.Add([pscustomobject]@{ - Racine = $Root - Chemin = $Node.FullPath - Nom = $Node.Name - Type = if ($isFile) { 'Fichier' } else { 'Dossier' } - Profondeur = $Node.Depth - TailleOctets = [long]$Node.Size - Taille = Format-Bytes $Node.Size - Fichiers = $Node.FileCount - Dossiers = $Node.FolderCount - }) - foreach ($c in @($Node.Children | Where-Object { $_ } | Sort-Object @{E={$_.Size}} -Descending)) { - _flattenNode -Node $c -Root $Root -Acc $Acc - } - } - foreach ($root in $Scan.Roots) { _flattenNode -Node $root -Root $root.FullPath -Acc $treeRows } - _writeCsv -Rows $treeRows -Suffix 'tree' - } - - # ---- long paths ---- - if ($wantLong) { - $longRows = foreach ($lp in $Scan.LongPaths) { - [pscustomobject]@{ Longueur = $lp.Length; Type = $lp.Type; Chemin = $lp.Path } - } - _writeCsv -Rows $longRows -Suffix 'longpaths' - } - - # ---- permissions ---- - if ($wantPerm) { - $permRows = foreach ($ace in (Select-FilerPerms -Scan $Scan -HideInheritedChildPerms $HideInheritedChildPerms -HideSystemPrincipals $HideSystemPrincipals)) { - [pscustomobject]@{ - Dossier = $ace.Folder - Proprietaire = $ace.Owner - Identite = $ace.Identity - Droits = $ace.Rights - Type = $ace.Type - Herite = $ace.Inherited - } - } - _writeCsv -Rows $permRows -Suffix 'permissions' - } - - # ---- granted access (successful remediations only, like the HTML) ---- - if ($wantGrants -and $Scan.Settings.GrantAccess) { - $grantRows = foreach ($g in @($Scan.Grants | Where-Object { $_.Success })) { - if ($g.Reverted) { $state = 'annulé' } - elseif ($Scan.Settings.RevertGrants) { $state = 'NON annulé' } - else { $state = 'conservé' } - [pscustomobject]@{ - Chemin = $g.Path - Raison = $g.Reason - Modification = $g.Changes - ProprietaireOrigine = $g.OriginalOwner - Etat = $state - ErreurAnnulation = $g.RevertError - } - } - _writeCsv -Rows $grantRows -Suffix 'grants' - } - - # ---- errors ---- - if ($wantErrors) { - $errRows = foreach ($e in $Scan.Errors) { - [pscustomobject]@{ Chemin = $e.Path; Erreur = $e.Error } - } - _writeCsv -Rows $errRows -Suffix 'errors' - } - - return $written.ToArray() -} -'@ - -# Dot-source core functions into the current scope (for headless + GUI export). -. ([scriptblock]::Create($CoreFunctions)) - -# ============================================================================ -# HEADLESS MODE -# ============================================================================ -$runHeadless = $NoGui -or ($Path -and ($Output -or $CsvOutput)) -if ($runHeadless) { - if (-not $Path) { throw "Le mode sans interface requiert -Path." } - if (-not $Output -and -not $CsvOutput) { throw "Le mode sans interface requiert -Output et/ou -CsvOutput." } - - if ($GrantAccess -and -not (Test-IsElevated)) { - Write-Warning "-GrantAccess nécessite des droits Administrateur ; ce processus n'est pas élevé. Les éléments refusés peuvent ne pas être corrigés." - } - - Write-Host "Analyse de $($Path -join ', ') en cours ..." -ForegroundColor Cyan - $progress = [hashtable]::Synchronized(@{ Status = '' }) - $scan = Invoke-FilerScan -Paths $Path -MaxPathLength $MaxPathLength ` - -PermissionDepth $PermissionDepth ` - -IncludeFilesInTree:$IncludeFilesInTree ` - -GrantAccess:$GrantAccess -RevertGrants:(-not $KeepGrants) -Progress $progress - - if ($Output) { - $out = ConvertTo-FilerHtmlReport -Scan $scan -Path $Output -Categories $Category -HideInheritedChildPerms:$HideInheritedChildPerms -HideSystemPrincipals:$HideSystemPrincipals - Write-Host "Rapport HTML généré : $out" -ForegroundColor Green - } - if ($CsvOutput) { - $csvFiles = @(ConvertTo-FilerCsvReport -Scan $scan -Path $CsvOutput -Categories $Category -HideInheritedChildPerms:$HideInheritedChildPerms -HideSystemPrincipals:$HideSystemPrincipals) - if ($csvFiles.Count -gt 0) { - Write-Host "Rapport(s) CSV généré(s) :" -ForegroundColor Green - foreach ($f in $csvFiles) { Write-Host " $f" -ForegroundColor Green } - } else { - Write-Host "Aucun fichier CSV généré (catégories sans données)." -ForegroundColor Yellow - } - } - Write-Host (" {0} au total, {1} dossiers, {2} fichiers, {3} chemins trop longs, {4} erreurs" -f ` - (Format-Bytes $scan.Stats.TotalSize), $scan.Stats.TotalFolders, $scan.Stats.TotalFiles, - $scan.Stats.LongPaths, $scan.Stats.Errors) - if ($GrantAccess) { - Write-Host (" {0} élément(s) ont reçu un accès ({1})" -f ` - $scan.Stats.Grants, $(if ($KeepGrants) { 'conservé' } else { 'annulé' })) -ForegroundColor Yellow - } - return -} - -# ============================================================================ -# GUI MODE -# ============================================================================ -Add-Type -AssemblyName System.Windows.Forms -Add-Type -AssemblyName System.Drawing -[System.Windows.Forms.Application]::EnableVisualStyles() - -$form = New-Object System.Windows.Forms.Form -$form.Text = 'Filer Manager' -$form.Size = New-Object System.Drawing.Size(960, 720) -$form.StartPosition = 'CenterScreen' -$form.MinimumSize = New-Object System.Drawing.Size(760, 560) - -# ---- top: folder list + add/remove ---- -$lblFolders = New-Object System.Windows.Forms.Label -$lblFolders.Text = 'Dossiers à analyser :' -$lblFolders.Location = '12,12'; $lblFolders.AutoSize = $true -$form.Controls.Add($lblFolders) - -$lstFolders = New-Object System.Windows.Forms.ListBox -$lstFolders.Location = '12,32'; $lstFolders.Size = '700,84' -$lstFolders.Anchor = 'Top,Left,Right' -$lstFolders.HorizontalScrollbar = $true -$form.Controls.Add($lstFolders) - -$btnAdd = New-Object System.Windows.Forms.Button -$btnAdd.Text = 'Ajouter...'; $btnAdd.Location = '724,32'; $btnAdd.Size = '110,28' -$btnAdd.Anchor = 'Top,Right' -$form.Controls.Add($btnAdd) - -$btnRemove = New-Object System.Windows.Forms.Button -$btnRemove.Text = 'Supprimer'; $btnRemove.Location = '724,66'; $btnRemove.Size = '110,28' -$btnRemove.Anchor = 'Top,Right' -$form.Controls.Add($btnRemove) - -# ---- settings row ---- -$lblMax = New-Object System.Windows.Forms.Label -$lblMax.Text = 'Longueur max :'; $lblMax.Location = '12,128'; $lblMax.AutoSize = $true -$form.Controls.Add($lblMax) - -$numMax = New-Object System.Windows.Forms.NumericUpDown -$numMax.Location = '118,126'; $numMax.Size = '70,24' -$numMax.Minimum = 1; $numMax.Maximum = 32767; $numMax.Value = $MaxPathLength -$form.Controls.Add($numMax) - -$lblDepth = New-Object System.Windows.Forms.Label -$lblDepth.Text = 'Profondeur :'; $lblDepth.Location = '202,128'; $lblDepth.AutoSize = $true -$form.Controls.Add($lblDepth) - -$numDepth = New-Object System.Windows.Forms.NumericUpDown -$numDepth.Location = '288,126'; $numDepth.Size = '60,24' -$numDepth.Minimum = 0; $numDepth.Maximum = 20; $numDepth.Value = $PermissionDepth -$form.Controls.Add($numDepth) - -$chkFiles = New-Object System.Windows.Forms.CheckBox -$chkFiles.Text = 'Inclure les fichiers'; $chkFiles.Location = '362,127'; $chkFiles.AutoSize = $true -$chkFiles.Checked = [bool]$IncludeFilesInTree -$form.Controls.Add($chkFiles) - -# ---- auto-grant row ---- -$chkGrant = New-Object System.Windows.Forms.CheckBox -$chkGrant.Text = "Accorder l'accès Administrators aux éléments refusés" -$chkGrant.Location = '12,156'; $chkGrant.AutoSize = $true -$chkGrant.Checked = [bool]$GrantAccess -$form.Controls.Add($chkGrant) - -$chkRevert = New-Object System.Windows.Forms.CheckBox -$chkRevert.Text = "Annuler les modifications après l'analyse" -$chkRevert.Location = '380,156'; $chkRevert.AutoSize = $true -$chkRevert.Checked = (-not $KeepGrants) -$chkRevert.Enabled = $chkGrant.Checked -$form.Controls.Add($chkRevert) - -$chkGrant.Add_CheckedChanged({ $chkRevert.Enabled = $chkGrant.Checked }) - -$btnScan = New-Object System.Windows.Forms.Button -$btnScan.Text = 'Analyser'; $btnScan.Location = '600,124'; $btnScan.Size = '110,30' -$btnScan.Anchor = 'Top,Right' -$btnScan.BackColor = [System.Drawing.Color]::FromArgb(79,140,255) -$btnScan.ForeColor = [System.Drawing.Color]::White -$btnScan.FlatStyle = 'Flat' -$form.Controls.Add($btnScan) - -$btnExport = New-Object System.Windows.Forms.Button -$btnExport.Text = 'Exporter...'; $btnExport.Location = '724,124'; $btnExport.Size = '110,30' -$btnExport.Anchor = 'Top,Right'; $btnExport.Enabled = $false -$form.Controls.Add($btnExport) - -# ---- tabs ---- -$tabs = New-Object System.Windows.Forms.TabControl -$tabs.Location = '12,188'; $tabs.Size = '922,456' -$tabs.Anchor = 'Top,Bottom,Left,Right' -$form.Controls.Add($tabs) - -$tabTree = New-Object System.Windows.Forms.TabPage; $tabTree.Text = 'Tailles des dossiers' -$tabLong = New-Object System.Windows.Forms.TabPage; $tabLong.Text = 'Chemins trop longs' -$tabPerm = New-Object System.Windows.Forms.TabPage; $tabPerm.Text = 'Permissions' -$tabGrant = New-Object System.Windows.Forms.TabPage; $tabGrant.Text = 'Accès accordé' -$tabs.TabPages.AddRange(@($tabTree, $tabLong, $tabPerm, $tabGrant)) - -$tree = New-Object System.Windows.Forms.TreeView -$tree.Dock = 'Fill'; $tree.HideSelection = $false -# Owner-draw the labels so we can paint a proportional "space taken" bar after -# each node, mirroring the bars in the HTML report. The control still draws the -# +/- expanders and connecting lines itself. -$tree.DrawMode = 'OwnerDrawText' -$tabTree.Controls.Add($tree) -# Fill a node's real children the first time it is expanded (lazy loading keeps -# the GUI responsive no matter how large the scanned tree is). -$tree.Add_BeforeExpand({ param($s, $e) Expand-FilerTreeNode -TreeNode $e.Node }) - -# Render each tree node: the label, then a bar showing the folder's share of its -# parent's size (roots = 100%), then the percentage - matching the HTML report. -$tree.Add_DrawNode({ - param($s, $e) - $nodeData = $e.Node.Tag - # Placeholder ("Chargement...") and not-yet-measured nodes: draw normally. - if ($null -eq $nodeData -or $e.Bounds.Width -le 0 -or - -not ($nodeData.PSObject.Properties.Name -contains 'Size')) { - $e.DrawDefault = $true; return - } - - $selected = ($e.State -band [System.Windows.Forms.TreeNodeStates]::Selected) -ne 0 - $foreColor = if ($selected) { [System.Drawing.SystemColors]::HighlightText } else { $tree.ForeColor } - if ($selected) { - $hl = New-Object System.Drawing.SolidBrush ([System.Drawing.SystemColors]::Highlight) - $e.Graphics.FillRectangle($hl, $e.Bounds); $hl.Dispose() - } - - $flags = [System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor ` - [System.Windows.Forms.TextFormatFlags]::Left -bor ` - [System.Windows.Forms.TextFormatFlags]::NoPrefix - [System.Windows.Forms.TextRenderer]::DrawText($e.Graphics, $e.Node.Text, $tree.Font, $e.Bounds, $foreColor, $flags) - - # Share of the parent folder's size (top-level roots have no parent = 100%). - $pct = 100.0 - $parent = $e.Node.Parent - if ($parent -and $parent.Tag -and - ($parent.Tag.PSObject.Properties.Name -contains 'Size') -and $parent.Tag.Size -gt 0) { - $pct = [math]::Round(($nodeData.Size / $parent.Tag.Size) * 100, 1) - } - if ($pct -lt 0) { $pct = 0 } elseif ($pct -gt 100) { $pct = 100 } - - # Bar just to the right of the label. - $barW = 120; $barH = 9 - $barX = $e.Bounds.Right + 10 - $barY = $e.Bounds.Top + [int](($e.Bounds.Height - $barH) / 2) - - $track = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(228, 230, 235)) - $e.Graphics.FillRectangle($track, $barX, $barY, $barW, $barH); $track.Dispose() - - $fillW = [int][math]::Round($barW * $pct / 100) - if ($fillW -gt 0) { - $fill = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(59, 111, 212)) - $e.Graphics.FillRectangle($fill, $barX, $barY, $fillW, $barH); $fill.Dispose() - } - $pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(170, 175, 185)) - $e.Graphics.DrawRectangle($pen, $barX, $barY, $barW, $barH); $pen.Dispose() - - $pctRect = New-Object System.Drawing.Rectangle (($barX + $barW + 6), $e.Bounds.Top, 52, $e.Bounds.Height) - [System.Windows.Forms.TextRenderer]::DrawText($e.Graphics, ('{0:N1}%' -f $pct), $tree.Font, $pctRect, $tree.ForeColor, $flags) -}) - -$lvLong = New-Object System.Windows.Forms.ListView -$lvLong.Dock = 'Fill'; $lvLong.View = 'Details'; $lvLong.FullRowSelect = $true; $lvLong.GridLines = $true -[void]$lvLong.Columns.Add('Longueur', 70); [void]$lvLong.Columns.Add('Type', 70); [void]$lvLong.Columns.Add('Chemin', 760) -$tabLong.Controls.Add($lvLong) - -$lvPerm = New-Object System.Windows.Forms.ListView -$lvPerm.Dock = 'Fill'; $lvPerm.View = 'Details'; $lvPerm.FullRowSelect = $true; $lvPerm.GridLines = $true -[void]$lvPerm.Columns.Add('Dossier', 320); [void]$lvPerm.Columns.Add('Identité', 200) -[void]$lvPerm.Columns.Add('Droits', 200); [void]$lvPerm.Columns.Add('Type', 60); [void]$lvPerm.Columns.Add('Hérité', 70) -$tabPerm.Controls.Add($lvPerm) - -$lvGrant = New-Object System.Windows.Forms.ListView -$lvGrant.Dock = 'Fill'; $lvGrant.View = 'Details'; $lvGrant.FullRowSelect = $true; $lvGrant.GridLines = $true -[void]$lvGrant.Columns.Add('Chemin', 360); [void]$lvGrant.Columns.Add('Raison', 110) -[void]$lvGrant.Columns.Add('Modification', 240); [void]$lvGrant.Columns.Add("Propriétaire d'origine", 150) -[void]$lvGrant.Columns.Add('État', 90) -$tabGrant.Controls.Add($lvGrant) - -# Filter toolbar for the permissions tab (docked above the list). -$permBar = New-Object System.Windows.Forms.Panel -$permBar.Dock = 'Top'; $permBar.Height = 30 -$lblPermHint = New-Object System.Windows.Forms.Label -$lblPermHint.Text = 'Cliquez sur un en-tête de colonne pour filtrer ou trier.' -$lblPermHint.AutoSize = $true; $lblPermHint.Location = '6,7' -$lblPermHint.ForeColor = [System.Drawing.Color]::DimGray -$btnPermClear = New-Object System.Windows.Forms.Button -$btnPermClear.Text = 'Effacer les filtres'; $btnPermClear.Size = '130,24' -$btnPermClear.Dock = 'Right'; $btnPermClear.Enabled = $false -$chkPermHideInh = New-Object System.Windows.Forms.CheckBox -$chkPermHideInh.Text = 'Masquer les permissions héritées (enfants)' -$chkPermHideInh.AutoSize = $true; $chkPermHideInh.Dock = 'Right' -$chkPermHideInh.Padding = '0,4,8,0' -$chkPermHideSys = New-Object System.Windows.Forms.CheckBox -$chkPermHideSys.Text = 'Masquer les comptes/groupes système' -$chkPermHideSys.AutoSize = $true; $chkPermHideSys.Dock = 'Right' -$chkPermHideSys.Padding = '0,4,8,0' -$permBar.Controls.Add($lblPermHint) -$permBar.Controls.Add($chkPermHideInh) -$permBar.Controls.Add($chkPermHideSys) -$permBar.Controls.Add($btnPermClear) -$tabPerm.Controls.Add($permBar) - -$lvPerm.Add_ColumnClick({ param($s, $e) Show-PermColumnMenu -ColumnIndex $e.Column }) -$btnPermClear.Add_Click({ $script:PermFilters = @{}; Update-PermView }) -$chkPermHideInh.Add_CheckedChanged({ $script:PermHideInherited = $chkPermHideInh.Checked; Update-PermView }) -$chkPermHideSys.Add_CheckedChanged({ $script:PermHideSystem = $chkPermHideSys.Checked; Update-PermView }) - -# ---- status bar ---- -$status = New-Object System.Windows.Forms.StatusStrip -$statusLbl = New-Object System.Windows.Forms.ToolStripStatusLabel -$statusLbl.Text = 'Prêt. Ajoutez un ou plusieurs dossiers, puis Analyser.' -$progressBar = New-Object System.Windows.Forms.ToolStripProgressBar -$progressBar.Style = 'Marquee'; $progressBar.Visible = $false; $progressBar.Width = 140 -[void]$status.Items.Add($statusLbl) -[void]$status.Items.Add($progressBar) -$form.Controls.Add($status) - -# ---- state ---- -$script:LastScan = $null -$script:Runspace = $null -$script:PowerShell = $null -$script:Handle = $null -$script:Shared = $null - -# Background export state (report generation runs off the UI thread too). -$script:ExportRunspace = $null -$script:ExportPowerShell = $null -$script:ExportHandle = $null -$script:ExportShared = $null -$script:ExportHtmlPath = $null - -# Permissions view state: source rows, active per-column filters, and sort. -$script:AllPerms = @() -$script:PermCols = @('Folder', 'Identity', 'Rights', 'Type', 'Inherited') # column index -> property -# Display labels for the permission columns (property name -> French header). -$script:PermColLabels = @{ Folder = 'Dossier'; Identity = 'Identité'; Rights = 'Droits'; Type = 'Type'; Inherited = 'Hérité' } -$script:PermFilters = @{} # property -> System.Collections.Generic.HashSet[string] of allowed values -$script:PermSort = @{ Prop = 'Folder'; Asc = $true } -# When $true, inherited ACEs on child folders are hidden (scan roots keep theirs). -$script:PermHideInherited = $false -# When $true, ACEs held by well-known system/built-in principals are hidden. -$script:PermHideSystem = $false -$script:PermRootPaths = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - -# ---- helpers ---- -function New-FilerTreeNode { - # Creates a TreeNode for a single scan node WITHOUT recursing into its - # children. A placeholder child is added so the [+] expander shows; the real - # children are filled on demand by Expand-FilerTreeNode (see the tree's - # BeforeExpand handler). This makes even very large trees appear instantly. - param($Node) - $sizeStr = Format-Bytes $Node.Size - $isFile = ($Node.PSObject.Properties.Name -contains 'IsFile' -and $Node.IsFile) - if ($isFile) { - $text = "$($Node.Name) - $sizeStr" - } else { - $text = "$($Node.Name) - $sizeStr ($($Node.FolderCount) dossiers, $($Node.FileCount) fichiers)" - } - $tn = New-Object System.Windows.Forms.TreeNode($text) - $tn.Tag = $Node # keep the scan node so its children can be filled lazily - $kids = @($Node.Children | Where-Object { $_ }) - if ($kids.Count -gt 0) { - # Placeholder (Tag stays $null, which marks the node as "not yet loaded"). - [void]$tn.Nodes.Add((New-Object System.Windows.Forms.TreeNode('Chargement...'))) - } - return $tn -} - -function Expand-FilerTreeNode { - # Replaces the placeholder with the node's real children the first time it is - # expanded. A node is "not yet loaded" while its single child has a $null Tag. - param($TreeNode) - if ($TreeNode.Nodes.Count -ne 1 -or $null -ne $TreeNode.Nodes[0].Tag) { return } - $node = $TreeNode.Tag - $TreeNode.TreeView.BeginUpdate() - $TreeNode.Nodes.Clear() - $kids = @($node.Children | Where-Object { $_ } | Sort-Object @{E={$_.Size}} -Descending) - foreach ($c in $kids) { [void]$TreeNode.Nodes.Add((New-FilerTreeNode -Node $c)) } - $TreeNode.TreeView.EndUpdate() -} - -function Show-Results { - param($Scan) - $script:LastScan = $Scan - - $tree.BeginUpdate(); $tree.Nodes.Clear() - foreach ($root in $Scan.Roots) { [void]$tree.Nodes.Add((New-FilerTreeNode -Node $root)) } - if ($tree.Nodes.Count -gt 0) { $tree.Nodes[0].Expand() } # loads first level lazily - $tree.EndUpdate() - - $lvLong.BeginUpdate(); $lvLong.Items.Clear() - foreach ($lp in $Scan.LongPaths) { - $it = New-Object System.Windows.Forms.ListViewItem([string]$lp.Length) - [void]$it.SubItems.Add($lp.Type); [void]$it.SubItems.Add($lp.Path) - [void]$lvLong.Items.Add($it) - } - $lvLong.EndUpdate() - - $script:AllPerms = @($Scan.Permissions) - $script:PermRootPaths = Get-FilerRootPathSet -Scan $Scan - $script:PermFilters = @{} - $script:PermSort = @{ Prop = 'Folder'; Asc = $true } - Update-PermView - - $lvGrant.BeginUpdate(); $lvGrant.Items.Clear() - foreach ($g in @($Scan.Grants | Where-Object { $_.Success })) { - $it = New-Object System.Windows.Forms.ListViewItem([string]$g.Path) - [void]$it.SubItems.Add([string]$g.Reason) - [void]$it.SubItems.Add([string]$g.Changes) - [void]$it.SubItems.Add([string]$g.OriginalOwner) - if ($g.Reverted) { $state = 'annulé' } - elseif ($Scan.Settings.RevertGrants) { $state = 'NON annulé'; $it.ForeColor = [System.Drawing.Color]::Firebrick } - else { $state = 'conservé'; $it.ForeColor = [System.Drawing.Color]::DarkGoldenrod } - [void]$it.SubItems.Add($state) - [void]$lvGrant.Items.Add($it) - } - $lvGrant.EndUpdate() - $tabGrant.Text = if ($lvGrant.Items.Count -gt 0) { "Accès accordé ($($lvGrant.Items.Count))" } else { 'Accès accordé' } - - $statusLbl.Text = ("Terminé. {0} au total, {1} dossiers, {2} fichiers, {3} chemins trop longs, {4} erreurs." -f ` - (Format-Bytes $Scan.Stats.TotalSize), $Scan.Stats.TotalFolders, $Scan.Stats.TotalFiles, - $Scan.Stats.LongPaths, $Scan.Stats.Errors) - $btnExport.Enabled = $true -} - -function Get-PermCellValue { - param($Ace, [string]$Prop) - if ($Prop -eq 'Inherited') { return [string]$Ace.Inherited } - return [string]$Ace.$Prop -} - -function Update-PermView { - # Re-renders the permissions ListView from $script:AllPerms applying the - # active per-column filters and the current sort. - $rows = $script:AllPerms - if ($script:PermHideInherited) { - $rows = @($rows | Where-Object { (-not $_.Inherited) -or $script:PermRootPaths.Contains([string]$_.Folder) }) - } - if ($script:PermHideSystem) { - $rows = @($rows | Where-Object { -not (Test-IsSystemPrincipal -Identity $_.Identity -Sid $_.Sid) }) - } - foreach ($prop in $script:PermFilters.Keys) { - $allowed = $script:PermFilters[$prop] - $rows = @($rows | Where-Object { $allowed.Contains((Get-PermCellValue $_ $prop)) }) - } - - $sortProp = $script:PermSort.Prop - $rows = @($rows | Sort-Object @{ E = { Get-PermCellValue $_ $sortProp } }) - if (-not $script:PermSort.Asc) { [array]::Reverse($rows) } - - $lvPerm.BeginUpdate(); $lvPerm.Items.Clear() - foreach ($ace in $rows) { - $it = New-Object System.Windows.Forms.ListViewItem($ace.Folder) - [void]$it.SubItems.Add($ace.Identity); [void]$it.SubItems.Add($ace.Rights) - [void]$it.SubItems.Add($ace.Type); [void]$it.SubItems.Add([string]$ace.Inherited) - if ($ace.Type -eq 'Deny') { $it.ForeColor = [System.Drawing.Color]::Firebrick } - [void]$lvPerm.Items.Add($it) - } - $lvPerm.EndUpdate() - - # Update column headers to show sort arrow and filter funnel. - for ($i = 0; $i -lt $script:PermCols.Count; $i++) { - $prop = $script:PermCols[$i] - $label = $script:PermColLabels[$prop] - if ($script:PermFilters.ContainsKey($prop)) { $label += ' (v)' } - if ($prop -eq $sortProp) { $label += $(if ($script:PermSort.Asc) { ' ^' } else { ' v' }) } - $lvPerm.Columns[$i].Text = $label - } - - $shown = $lvPerm.Items.Count; $total = $script:AllPerms.Count - $nFilt = $script:PermFilters.Count - $btnPermClear.Enabled = ($nFilt -gt 0) - if ($script:AllPerms.Count -gt 0) { - $msg = "Permissions : $shown sur $total affichées" - if ($nFilt -gt 0) { $msg += " ($nFilt filtre$(if($nFilt -gt 1){'s'}) de colonne) - cliquez sur un en-tête pour filtrer/trier" } - else { $msg += " - cliquez sur un en-tête de colonne pour filtrer ou trier" } - $statusLbl.Text = $msg - } -} - -function Show-PermColumnMenu { - param([int]$ColumnIndex) - if ($script:AllPerms.Count -eq 0) { return } - $prop = $script:PermCols[$ColumnIndex] - - $menu = New-Object System.Windows.Forms.ContextMenuStrip - $menu.ShowImageMargin = $false - - $asc = New-Object System.Windows.Forms.ToolStripMenuItem("Trier A -> Z") - $asc.Add_Click({ $script:PermSort = @{ Prop = $prop; Asc = $true }; Update-PermView }.GetNewClosure()) - $desc = New-Object System.Windows.Forms.ToolStripMenuItem("Trier Z -> A") - $desc.Add_Click({ $script:PermSort = @{ Prop = $prop; Asc = $false }; Update-PermView }.GetNewClosure()) - [void]$menu.Items.Add($asc); [void]$menu.Items.Add($desc) - [void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) - - # Select all / Clear toggles for the value checklist. - $selAll = New-Object System.Windows.Forms.ToolStripMenuItem("(Tout sélectionner)") - $clrAll = New-Object System.Windows.Forms.ToolStripMenuItem("(Tout effacer)") - [void]$menu.Items.Add($selAll); [void]$menu.Items.Add($clrAll) - [void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) - - # Distinct values for this column (from the unfiltered source). - $distinct = @($script:AllPerms | ForEach-Object { Get-PermCellValue $_ $prop } | Sort-Object -Unique) - $current = $script:PermFilters[$prop] # $null => everything allowed - $valueItems = New-Object System.Collections.ArrayList - foreach ($v in $distinct) { - $label = if ([string]::IsNullOrEmpty($v)) { '(vide)' } else { $v } - $mi = New-Object System.Windows.Forms.ToolStripMenuItem($label) - $mi.CheckOnClick = $true - $mi.Checked = ($null -eq $current) -or $current.Contains($v) - $mi.Tag = $v - [void]$menu.Items.Add($mi) - [void]$valueItems.Add($mi) - } - - $selAll.Add_Click({ foreach ($m in $valueItems) { $m.Checked = $true } }.GetNewClosure()) - $clrAll.Add_Click({ foreach ($m in $valueItems) { $m.Checked = $false } }.GetNewClosure()) - - [void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) - $apply = New-Object System.Windows.Forms.ToolStripMenuItem("Appliquer le filtre") - $apply.Add_Click({ - $checked = New-Object 'System.Collections.Generic.HashSet[string]' - foreach ($m in $valueItems) { if ($m.Checked) { [void]$checked.Add([string]$m.Tag) } } - if ($checked.Count -eq $valueItems.Count) { $script:PermFilters.Remove($prop) } # all selected = no filter - else { $script:PermFilters[$prop] = $checked } - Update-PermView - }.GetNewClosure()) - $clear = New-Object System.Windows.Forms.ToolStripMenuItem("Supprimer le filtre de cette colonne") - $clear.Add_Click({ [void]$script:PermFilters.Remove($prop); Update-PermView }.GetNewClosure()) - [void]$menu.Items.Add($apply); [void]$menu.Items.Add($clear) - - # Keep the menu open while ticking value checkboxes or using Select/Clear all; - # close only on Sort / Apply / Remove filter. - $menu.Add_Closing({ - param($s, $e) - if ($e.CloseReason -eq [System.Windows.Forms.ToolStripDropDownCloseReason]::ItemClicked) { - $clicked = $s.GetItemAt($s.PointToClient([System.Windows.Forms.Cursor]::Position)) - if ($clicked -and (($valueItems -contains $clicked) -or ($clicked -eq $selAll) -or ($clicked -eq $clrAll))) { - $e.Cancel = $true - } - } - }.GetNewClosure()) - - # Show just under the clicked column header. - $x = 0 - for ($i = 0; $i -lt $ColumnIndex; $i++) { $x += $lvPerm.Columns[$i].Width } - $menu.Show($lvPerm, $x, 4) -} - -function Show-ExportDialog { - <# Modal picker for the export: which report categories and which formats - (HTML and/or CSV). Returns $null if the user cancelled, otherwise an object - with .Categories (array of Tree/LongPaths/Permissions/Grants/Errors or - @('All')), .Html and .Csv booleans. Categories with no data are disabled. #> - param($Scan) - - $hasGrants = @($Scan.Grants | Where-Object { $_.Success }).Count -gt 0 - $hasErrors = $Scan.Errors.Count -gt 0 - - # label, canonical name, enabled? - $cats = @( - @{ Text = 'Tailles des dossiers'; Name = 'Tree'; Enabled = $true }, - @{ Text = 'Chemins trop longs'; Name = 'LongPaths'; Enabled = $true }, - @{ Text = 'Permissions'; Name = 'Permissions'; Enabled = $true }, - @{ Text = 'Accès accordé'; Name = 'Grants'; Enabled = $hasGrants }, - @{ Text = 'Erreurs / accès refusé'; Name = 'Errors'; Enabled = $hasErrors } - ) - - $dlg = New-Object System.Windows.Forms.Form - $dlg.Text = 'Exporter - choisir les catégories et formats' - $dlg.FormBorderStyle = 'FixedDialog' - $dlg.StartPosition = 'CenterParent' - $dlg.MaximizeBox = $false; $dlg.MinimizeBox = $false - $dlg.ClientSize = New-Object System.Drawing.Size(320, 330) - - $lbl = New-Object System.Windows.Forms.Label - $lbl.Text = 'Inclure ces catégories dans le rapport :' - $lbl.Location = '14,12'; $lbl.AutoSize = $true - $dlg.Controls.Add($lbl) - - $boxes = @() - $y = 40 - foreach ($c in $cats) { - $cb = New-Object System.Windows.Forms.CheckBox - $cb.Text = $c.Text - $cb.Tag = $c.Name - $cb.Location = "20,$y"; $cb.AutoSize = $true - $cb.Checked = $c.Enabled - $cb.Enabled = $c.Enabled - if (-not $c.Enabled) { $cb.Text += ' (aucune donnée)' } - $dlg.Controls.Add($cb) - $boxes += $cb - $y += 26 - } - - # ---- format selection (HTML and/or CSV) ---- - $y += 8 - $lblFmt = New-Object System.Windows.Forms.Label - $lblFmt.Text = 'Formats à générer :' - $lblFmt.Location = "14,$y"; $lblFmt.AutoSize = $true - $dlg.Controls.Add($lblFmt) - $y += 26 - - $chkHtml = New-Object System.Windows.Forms.CheckBox - $chkHtml.Text = 'HTML (rapport unique)'; $chkHtml.Tag = 'Html' - $chkHtml.Location = "20,$y"; $chkHtml.AutoSize = $true; $chkHtml.Checked = $true - $dlg.Controls.Add($chkHtml) - $y += 26 - - $chkCsv = New-Object System.Windows.Forms.CheckBox - $chkCsv.Text = 'CSV (un fichier par catégorie)'; $chkCsv.Tag = 'Csv' - $chkCsv.Location = "20,$y"; $chkCsv.AutoSize = $true; $chkCsv.Checked = $false - $dlg.Controls.Add($chkCsv) - $y += 36 - - $btnOk = New-Object System.Windows.Forms.Button - $btnOk.Text = 'Exporter...'; $btnOk.Size = '90,28'; $btnOk.Location = "124,$y" - $btnOk.DialogResult = [System.Windows.Forms.DialogResult]::OK - $dlg.Controls.Add($btnOk); $dlg.AcceptButton = $btnOk - - $btnCancel = New-Object System.Windows.Forms.Button - $btnCancel.Text = 'Annuler'; $btnCancel.Size = '90,28'; $btnCancel.Location = "220,$y" - $btnCancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel - $dlg.Controls.Add($btnCancel); $dlg.CancelButton = $btnCancel - - # Enable Export only when at least one category AND at least one format is ticked. - $sync = { - $anyCat = @($boxes | Where-Object { $_.Checked }).Count -gt 0 - $anyFmt = $chkHtml.Checked -or $chkCsv.Checked - $btnOk.Enabled = $anyCat -and $anyFmt - }.GetNewClosure() - foreach ($cb in $boxes) { $cb.Add_CheckedChanged($sync) } - $chkHtml.Add_CheckedChanged($sync) - $chkCsv.Add_CheckedChanged($sync) - - $result = $dlg.ShowDialog($form) - $picked = @($boxes | Where-Object { $_.Checked } | ForEach-Object { [string]$_.Tag }) - $wantHtml = $chkHtml.Checked - $wantCsv = $chkCsv.Checked - $dlg.Dispose() - - if ($result -ne [System.Windows.Forms.DialogResult]::OK -or $picked.Count -eq 0) { return $null } - if (-not ($wantHtml -or $wantCsv)) { return $null } - # All categories ticked -> 'All' so the header shows the full report. - $categories = if ($picked.Count -eq $boxes.Count) { @('All') } else { $picked } - return [pscustomobject]@{ Categories = $categories; Html = $wantHtml; Csv = $wantCsv } -} - -# ---- poll timer (reads background runspace) ---- -$timer = New-Object System.Windows.Forms.Timer -$timer.Interval = 200 -$timer.Add_Tick({ - if ($script:Shared) { - if ($script:Shared.Status) { $statusLbl.Text = $script:Shared.Status } - if ($script:Handle -and $script:Handle.IsCompleted) { - $timer.Stop() - try { - $result = $script:PowerShell.EndInvoke($script:Handle) - $scan = $script:Shared.Result - if ($scan) { Show-Results -Scan $scan } - else { $statusLbl.Text = 'Analyse terminée mais aucune donnée renvoyée.' } - } - catch { - [System.Windows.Forms.MessageBox]::Show("Échec de l'analyse :`n$($_.Exception.Message)", 'Filer Manager', - 'OK', 'Error') | Out-Null - $statusLbl.Text = "Échec de l'analyse." - } - finally { - if ($script:PowerShell) { $script:PowerShell.Dispose() } - if ($script:Runspace) { $script:Runspace.Close(); $script:Runspace.Dispose() } - $script:PowerShell = $null; $script:Runspace = $null; $script:Handle = $null - $progressBar.Visible = $false - $btnScan.Enabled = $true; $btnAdd.Enabled = $true; $btnRemove.Enabled = $true - } - } - } -}) - -# ---- poll timer (reads background export runspace) ---- -$exportTimer = New-Object System.Windows.Forms.Timer -$exportTimer.Interval = 200 -$exportTimer.Add_Tick({ - if (-not $script:ExportHandle) { $exportTimer.Stop(); return } - if (-not $script:ExportHandle.IsCompleted) { return } - $exportTimer.Stop() - - $written = $null; $err = $null - try { - $script:ExportPowerShell.EndInvoke($script:ExportHandle) | Out-Null - if ($script:ExportShared) { $written = $script:ExportShared.Written; $err = $script:ExportShared.Error } - } - catch { $err = $_.Exception.Message } - finally { - if ($script:ExportPowerShell) { $script:ExportPowerShell.Dispose() } - if ($script:ExportRunspace) { $script:ExportRunspace.Close(); $script:ExportRunspace.Dispose() } - $script:ExportPowerShell = $null; $script:ExportRunspace = $null; $script:ExportHandle = $null - $progressBar.Visible = $false - $btnScan.Enabled = $true; $btnAdd.Enabled = $true; $btnRemove.Enabled = $true - $btnExport.Enabled = ($null -ne $script:LastScan) - } - - $htmlPath = $script:ExportHtmlPath - if ($err) { - $statusLbl.Text = "Échec de l'export." - [System.Windows.Forms.MessageBox]::Show("Échec de l'export :`n$err", 'Filer Manager', 'OK', 'Error') | Out-Null - return - } - - $written = @($written) - if ($written.Count -eq 0) { - $statusLbl.Text = 'Aucun fichier généré.' - [System.Windows.Forms.MessageBox]::Show('Aucun fichier généré (catégories CSV sans données).', - 'Filer Manager', 'OK', 'Information') | Out-Null - return - } - - $statusLbl.Text = "Export terminé : $($written.Count) fichier(s)." - $list = ($written -join "`n") - $prompt = if ($htmlPath) { "`n`nOuvrir le rapport HTML maintenant ?" } else { "`n`nOuvrir le dossier maintenant ?" } - if ([System.Windows.Forms.MessageBox]::Show("Fichier(s) enregistré(s) :`n$list$prompt", 'Filer Manager', - 'YesNo', 'Question') -eq 'Yes') { - if ($htmlPath) { Start-Process $htmlPath } - else { Start-Process (Split-Path -Path $written[0] -Parent) } - } -}) - -# ---- events ---- -$btnAdd.Add_Click({ - $dlg = New-Object System.Windows.Forms.FolderBrowserDialog - $dlg.Description = 'Sélectionnez un dossier à analyser' - if ($dlg.ShowDialog() -eq 'OK') { - if (-not $lstFolders.Items.Contains($dlg.SelectedPath)) { [void]$lstFolders.Items.Add($dlg.SelectedPath) } - } -}) - -$btnRemove.Add_Click({ - if ($lstFolders.SelectedIndex -ge 0) { $lstFolders.Items.RemoveAt($lstFolders.SelectedIndex) } -}) - -$btnScan.Add_Click({ - if ($lstFolders.Items.Count -eq 0) { - [System.Windows.Forms.MessageBox]::Show('Ajoutez au moins un dossier à analyser.', 'Filer Manager', 'OK', 'Information') | Out-Null - return - } - $paths = @($lstFolders.Items) - $maxLen = [int]$numMax.Value - $depth = [int]$numDepth.Value - $incFiles = [bool]$chkFiles.Checked - $grant = [bool]$chkGrant.Checked - $revert = [bool]$chkRevert.Checked - - if ($grant -and -not (Test-IsElevated)) { - $msg = "L'octroi automatique nécessite des droits Administrateur, mais Filer Manager n'est pas exécuté en mode élevé.`n`n" + - "Les éléments refusés peuvent ne pas être corrigés. Continuer quand même ?" - if ([System.Windows.Forms.MessageBox]::Show($msg, 'Filer Manager', 'YesNo', 'Warning') -ne 'Yes') { return } - } - - $btnScan.Enabled = $false; $btnAdd.Enabled = $false; $btnRemove.Enabled = $false; $btnExport.Enabled = $false - $progressBar.Visible = $true - $statusLbl.Text = 'Analyse en cours...' - - $script:Shared = [hashtable]::Synchronized(@{ Status = 'Démarrage...'; Result = $null }) - $script:Runspace = [runspacefactory]::CreateRunspace() - $script:Runspace.ApartmentState = 'STA' - $script:Runspace.Open() - $script:Runspace.SessionStateProxy.SetVariable('Shared', $script:Shared) - - $script:PowerShell = [powershell]::Create() - $script:PowerShell.Runspace = $script:Runspace - [void]$script:PowerShell.AddScript($CoreFunctions) - [void]$script:PowerShell.AddScript(@' -param($Paths, $MaxLen, $Depth, $IncFiles, $Grant, $Revert, $Shared) -$Shared.Result = Invoke-FilerScan -Paths $Paths -MaxPathLength $MaxLen -PermissionDepth $Depth -IncludeFilesInTree $IncFiles -GrantAccess $Grant -RevertGrants $Revert -Progress $Shared -'@).AddArgument($paths).AddArgument($maxLen).AddArgument($depth).AddArgument($incFiles).AddArgument($grant).AddArgument($revert).AddArgument($script:Shared) - - $script:Handle = $script:PowerShell.BeginInvoke() - $timer.Start() -}) - -$btnExport.Add_Click({ - if (-not $script:LastScan) { return } - - $choice = Show-ExportDialog -Scan $script:LastScan - if (-not $choice) { return } # cancelled or nothing selected - $categories = $choice.Categories - - # Build a filename hint reflecting the chosen scope. - $scope = if ($categories -contains 'All') { 'all' } else { ($categories -join '-').ToLower() } - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $baseName = "filer-report-$scope-$stamp" - - $dlg = New-Object System.Windows.Forms.SaveFileDialog - if ($choice.Html -and $choice.Csv) { - $dlg.Title = 'Enregistrer les rapports (nom de base) - HTML + CSV' - $dlg.Filter = 'Rapports HTML + CSV|*.html' - $dlg.FileName = "$baseName.html" - } - elseif ($choice.Html) { - $dlg.Title = 'Enregistrer le rapport HTML' - $dlg.Filter = 'Rapport HTML (*.html)|*.html' - $dlg.FileName = "$baseName.html" - } - else { - $dlg.Title = 'Enregistrer le(s) rapport(s) CSV (nom de base)' - $dlg.Filter = 'Rapport CSV (*.csv)|*.csv' - $dlg.FileName = "$baseName.csv" - } - - if ($dlg.ShowDialog() -ne 'OK') { return } - - # Generate the report(s) on a background runspace so the GUI stays responsive; - # rendering a large scan to HTML/CSV can take several seconds. - $htmlPath = if ($choice.Html) { [System.IO.Path]::ChangeExtension($dlg.FileName, '.html') } else { $null } - $csvBase = $dlg.FileName - $script:ExportHtmlPath = $htmlPath - - $btnScan.Enabled = $false; $btnAdd.Enabled = $false; $btnRemove.Enabled = $false; $btnExport.Enabled = $false - $progressBar.Visible = $true - $statusLbl.Text = 'Export en cours...' - - $script:ExportShared = [hashtable]::Synchronized(@{ Written = $null; Error = $null }) - $script:ExportRunspace = [runspacefactory]::CreateRunspace() - $script:ExportRunspace.ApartmentState = 'STA' - $script:ExportRunspace.Open() - - $script:ExportPowerShell = [powershell]::Create() - $script:ExportPowerShell.Runspace = $script:ExportRunspace - [void]$script:ExportPowerShell.AddScript($CoreFunctions) - [void]$script:ExportPowerShell.AddScript(@' -param($Scan, $HtmlPath, $WantHtml, $WantCsv, $CsvBase, $Categories, $HidePerm, $HideSys, $Shared) -try { - $written = New-Object System.Collections.ArrayList - if ($WantHtml) { - $out = ConvertTo-FilerHtmlReport -Scan $Scan -Path $HtmlPath -Categories $Categories -HideInheritedChildPerms $HidePerm -HideSystemPrincipals $HideSys - [void]$written.Add($out) - } - if ($WantCsv) { - $csvFiles = @(ConvertTo-FilerCsvReport -Scan $Scan -Path $CsvBase -Categories $Categories -HideInheritedChildPerms $HidePerm -HideSystemPrincipals $HideSys) - foreach ($f in $csvFiles) { [void]$written.Add($f) } - } - $Shared.Written = $written.ToArray() -} -catch { $Shared.Error = $_.Exception.Message } -'@).AddArgument($script:LastScan).AddArgument($htmlPath).AddArgument([bool]$choice.Html).AddArgument([bool]$choice.Csv).AddArgument($csvBase).AddArgument($categories).AddArgument([bool]$script:PermHideInherited).AddArgument([bool]$script:PermHideSystem).AddArgument($script:ExportShared) - - $script:ExportHandle = $script:ExportPowerShell.BeginInvoke() - $exportTimer.Start() -}) - -# Pre-fill folders if -Path was supplied without -Output -if ($Path) { foreach ($p in $Path) { [void]$lstFolders.Items.Add($p) } } - -[void]$form.ShowDialog() -$form.Dispose() \ No newline at end of file