1762 lines
82 KiB
PowerShell
1762 lines
82 KiB
PowerShell
#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 { " <span class='meta'>$($Node.FolderCount) dossiers, $($Node.FileCount) fichiers</span>" }
|
|
$kids = @($Node.Children | Where-Object { $_ } )
|
|
|
|
$summary = "$icon <span class='nm'>$(_enc $Node.Name)</span> <span class='sz'>$sizeStr</span>" +
|
|
"<span class='barwrap'><span class='bar' style='width:$pct%'></span></span><span class='pct'>$pct%</span>$meta"
|
|
|
|
if ($kids.Count -gt 0) {
|
|
$open = if ($Node.Depth -eq 0) { ' open' } else { '' }
|
|
[void]$Out.Append("<details$open><summary>$summary</summary><div class='kids'>")
|
|
foreach ($c in ($kids | Sort-Object @{E={$_.Size}} -Descending)) {
|
|
_renderNode -Node $c -ParentSize $Node.Size -Out $Out
|
|
}
|
|
[void]$Out.Append("</div></details>")
|
|
}
|
|
else {
|
|
[void]$Out.Append("<div class='leaf'>$summary</div>")
|
|
}
|
|
}
|
|
|
|
$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("<tr><td class='num'>$($lp.Length)</td><td>$(_enc $lp.Type)</td><td class='path'>$(_enc $lp.Path)</td></tr>")
|
|
}
|
|
if ($Scan.LongPaths.Count -eq 0) { [void]$longRows.Append("<tr><td colspan='3' class='ok'>Aucun chemin égal ou supérieur à $($Scan.Settings.MaxPathLength) caractères. ✅</td></tr>") }
|
|
}
|
|
|
|
# ---- 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("<details class='perm$topCls' open><summary><span class='nm'>$(_enc $label)</span> <span class='meta'>Propriétaire : $(_enc $Node.Owner)</span></summary>")
|
|
[void]$Out.Append("<table class='grid'><thead><tr><th>Identité</th><th>Droits</th><th>Type</th><th>Hérité</th></tr></thead><tbody>")
|
|
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("<tr$cls data-inh='$($ace.Inherited)' data-sys='$sys'><td>$(_enc $ace.Identity)</td><td>$(_enc $ace.Rights)</td><td>$(_enc $ace.Type)</td><td>$($ace.Inherited)</td></tr>")
|
|
}
|
|
[void]$Out.Append("</tbody></table>")
|
|
if ($Node.Children.Count -gt 0) {
|
|
[void]$Out.Append("<div class='kids'>")
|
|
foreach ($c in ($Node.Children | Sort-Object Display)) { _renderPermNode -Node $c -IsTop $false -Out $Out }
|
|
[void]$Out.Append("</div>")
|
|
}
|
|
[void]$Out.Append("</details>")
|
|
}
|
|
|
|
foreach ($t in ($tops | Sort-Object Display)) { _renderPermNode -Node $t -IsTop $true -Out $permSb }
|
|
if ($byFolder.Count -eq 0) { [void]$permSb.Append("<p class='muted'>Aucune permission collectée.</p>") }
|
|
}
|
|
|
|
# ---- 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 = "<span class='ok'>annulé</span>" }
|
|
elseif ($Scan.Settings.RevertGrants) { $state = "<span class='deny'>NON annulé$(if($g.RevertError){' - ' + (_enc $g.RevertError)})</span>" }
|
|
else { $state = "<span class='warn'>conservé</span>" }
|
|
[void]$grantSb.Append("<tr><td class='path'>$(_enc $g.Path)</td><td>$(_enc $g.Reason)</td><td>$(_enc $g.Changes)</td><td>$(_enc $g.OriginalOwner)</td><td>$state</td></tr>")
|
|
}
|
|
}
|
|
|
|
# ---- errors ----
|
|
$errSb = New-Object System.Text.StringBuilder
|
|
if ($wantErrors) {
|
|
foreach ($e in $Scan.Errors) {
|
|
[void]$errSb.Append("<tr><td class='path'>$(_enc $e.Path)</td><td>$(_enc $e.Error)</td></tr>")
|
|
}
|
|
}
|
|
|
|
$rootsList = ($Scan.Roots | ForEach-Object { _enc $_.FullPath }) -join '<br>'
|
|
$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 { "<div class='sub' style='margin-top:6px'>Catégories : $(_enc ($included -join ', '))</div>" }
|
|
|
|
$html = @"
|
|
<!DOCTYPE html>
|
|
<html lang='fr'><head><meta charset='utf-8'>
|
|
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
|
<title>Rapport Filer Manager - $gen</title>
|
|
<style>
|
|
:root { --bg:#0f1116; --panel:#171a21; --pan2:#1d2129; --ink:#e7eaf0; --mut:#8b93a7;
|
|
--acc:#4f8cff; --bar:#3b6fd4; --warn:#ffb454; --deny:#ff6b6b; --ok:#42d392; --line:#2a2f3a; }
|
|
* { box-sizing:border-box; }
|
|
body { margin:0; font:14px/1.5 'Segoe UI',system-ui,sans-serif; background:var(--bg); color:var(--ink); }
|
|
header { padding:24px 32px; background:linear-gradient(120deg,#1b2a4a,#142035); border-bottom:1px solid var(--line); }
|
|
header h1 { margin:0 0 4px; font-size:22px; }
|
|
header .sub { color:var(--mut); font-size:13px; }
|
|
main { padding:24px 32px; max-width:1200px; margin:0 auto; }
|
|
.cards { display:flex; flex-wrap:wrap; gap:14px; margin:8px 0 28px; }
|
|
.card { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:16px 20px; min-width:150px; flex:1; }
|
|
.card .v { font-size:24px; font-weight:600; }
|
|
.card .l { color:var(--mut); font-size:12px; text-transform:uppercase; letter-spacing:.5px; }
|
|
.card.warn .v { color:var(--warn); } .card.bad .v { color:var(--deny); }
|
|
section { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:18px 22px; margin-bottom:22px; }
|
|
section h2 { margin:0 0 14px; font-size:16px; display:flex; align-items:center; gap:8px; }
|
|
details { border-left:2px solid var(--line); margin:2px 0; }
|
|
summary { cursor:pointer; padding:3px 6px; border-radius:6px; list-style:none; display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
|
summary:hover { background:var(--pan2); }
|
|
summary::-webkit-details-marker { display:none; }
|
|
.kids { margin-left:18px; }
|
|
.leaf { padding:3px 6px; display:flex; align-items:center; gap:8px; margin-left:4px; }
|
|
.nm { font-weight:500; } .sz { color:var(--acc); font-variant-numeric:tabular-nums; min-width:84px; }
|
|
.pct { color:var(--mut); font-size:12px; min-width:42px; text-align:right; }
|
|
.meta { color:var(--mut); font-size:12px; }
|
|
.barwrap { flex:0 0 120px; height:7px; background:var(--pan2); border-radius:4px; overflow:hidden; }
|
|
.bar { display:block; height:100%; background:linear-gradient(90deg,var(--bar),var(--acc)); }
|
|
table.grid { width:100%; border-collapse:collapse; font-size:13px; }
|
|
table.grid th, table.grid td { text-align:left; padding:6px 10px; border-bottom:1px solid var(--line); }
|
|
table.grid th { color:var(--mut); font-weight:600; position:sticky; top:0; background:var(--panel); }
|
|
td.num { text-align:right; font-variant-numeric:tabular-nums; color:var(--warn); font-weight:600; }
|
|
td.path { font-family:Consolas,'Cascadia Code',monospace; font-size:12px; word-break:break-all; }
|
|
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; }
|
|
footer { color:var(--mut); font-size:12px; text-align:center; padding:24px; }
|
|
</style></head>
|
|
<body>
|
|
<header>
|
|
<h1>📁 Rapport Filer Manager</h1>
|
|
<div class='sub'>Généré le $gen · Hôte <b>$(_enc $Scan.Computer)</b> ·
|
|
Seuil chemin trop long $($Scan.Settings.MaxPathLength) · Profondeur des permissions $($Scan.Settings.PermissionDepth)</div>
|
|
<div class='sub' style='margin-top:6px'>Analysé : $rootsList</div>
|
|
$catNote
|
|
</header>
|
|
<main>
|
|
<div class='cards'>
|
|
<div class='card'><div class='v'>$(Format-Bytes $Scan.Stats.TotalSize)</div><div class='l'>Taille totale</div></div>
|
|
<div class='card'><div class='v'>$('{0:N0}' -f $Scan.Stats.TotalFolders)</div><div class='l'>Dossiers</div></div>
|
|
<div class='card'><div class='v'>$('{0:N0}' -f $Scan.Stats.TotalFiles)</div><div class='l'>Fichiers</div></div>
|
|
<div class='card $(if($Scan.Stats.LongPaths){'warn'})'><div class='v'>$($Scan.Stats.LongPaths)</div><div class='l'>Chemins trop longs</div></div>
|
|
<div class='card $(if($Scan.Stats.Errors){'bad'})'><div class='v'>$($Scan.Stats.Errors)</div><div class='l'>Erreurs / refusés</div></div>
|
|
$(if ($Scan.Settings.GrantAccess) { "<div class='card $(if($Scan.Stats.Grants){'warn'})'><div class='v'>$($Scan.Stats.Grants)</div><div class='l'>Accès accordé</div></div>" })
|
|
</div>
|
|
|
|
$(if ($wantTree) { @"
|
|
<section>
|
|
<h2>📊 Tailles des dossiers</h2>
|
|
<div class='tree'>$($treeSb.ToString())</div>
|
|
</section>
|
|
"@ })
|
|
|
|
$(if ($wantLong) { @"
|
|
<section>
|
|
<h2>⚠️ Noms de fichiers / chemins trop longs (≥ $($Scan.Settings.MaxPathLength) caractères)</h2>
|
|
<input class='filter' placeholder='Filtrer les chemins…' oninput="filterRows(this,'longtbl')">
|
|
<div class='scroll'><table class='grid' id='longtbl'><thead><tr><th>Longueur</th><th>Type</th><th>Chemin</th></tr></thead>
|
|
<tbody>$($longRows.ToString())</tbody></table></div>
|
|
</section>
|
|
"@ })
|
|
|
|
$(if ($wantPerm) { @"
|
|
<section>
|
|
<h2>🔐 Permissions</h2>
|
|
<label class='permtoggle'><input id='hideInh' type='checkbox' onchange='applyPermFilters()'>
|
|
Masquer les permissions héritées (afficher uniquement les permissions propres)</label>
|
|
<label class='permtoggle'><input id='hideSys' type='checkbox'$(if ($HideSystemPrincipals) { ' checked' }) onchange='applyPermFilters()'>
|
|
Masquer les comptes/groupes système (SYSTEM, Administrators, CREATOR OWNER…)</label>
|
|
<div id='permroot'>$($permSb.ToString())</div>
|
|
</section>
|
|
"@ })
|
|
|
|
$(if ($wantGrants -and $Scan.Settings.GrantAccess) { @"
|
|
<section>
|
|
<h2>🔨 Accès accordé pour terminer l'analyse</h2>
|
|
<p class='muted'>É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' }).</p>
|
|
$(if ($grantRows.Count -gt 0) { @"
|
|
<div class='scroll'><table class='grid'><thead><tr><th>Chemin</th><th>Raison</th><th>Modification</th><th>Propriétaire d'origine</th><th>État</th></tr></thead>
|
|
<tbody>$($grantSb.ToString())</tbody></table></div>
|
|
"@ } else { "<p class='ok'>Aucune modification d'accès n'était nécessaire. ✅</p>" })
|
|
</section>
|
|
"@ })
|
|
|
|
$(if ($wantErrors -and $Scan.Errors.Count -gt 0) { @"
|
|
<section>
|
|
<h2>❌ Erreurs et accès refusé</h2>
|
|
<div class='scroll'><table class='grid'><thead><tr><th>Chemin</th><th>Erreur</th></tr></thead>
|
|
<tbody>$($errSb.ToString())</tbody></table></div>
|
|
</section>
|
|
"@ })
|
|
</main>
|
|
<footer>Filer Manager · rapport autonome · $gen</footer>
|
|
<script>
|
|
function filterRows(inp, tableId){
|
|
var q = inp.value.toLowerCase(), rows = document.getElementById(tableId).tBodies[0].rows;
|
|
for (var i=0;i<rows.length;i++){ rows[i].style.display = rows[i].innerText.toLowerCase().indexOf(q)>-1?'':'none'; }
|
|
}
|
|
function applyPermFilters(){
|
|
var inhCb = document.getElementById('hideInh'), sysCb = document.getElementById('hideSys');
|
|
var hideInh = inhCb && inhCb.checked, hideSys = sysCb && sysCb.checked;
|
|
var anyHide = hideInh || hideSys;
|
|
// Hide each folder's own rows that match an active filter (inherited and/or system).
|
|
document.querySelectorAll('#permroot details.perm').forEach(function(g){
|
|
g.querySelectorAll(':scope > table.grid > tbody > tr').forEach(function(r){
|
|
var drop = (hideInh && r.getAttribute('data-inh') === 'True') ||
|
|
(hideSys && r.getAttribute('data-sys') === 'True');
|
|
r.style.display = drop ? 'none' : '';
|
|
});
|
|
});
|
|
// Walk the tree bottom-up: a folder stays visible if it keeps any own row or
|
|
// any visible child folder.
|
|
function visit(node){
|
|
var vis = false;
|
|
node.querySelectorAll(':scope > table.grid > tbody > tr').forEach(function(r){ if (r.style.display !== 'none') vis = true; });
|
|
node.querySelectorAll(':scope > .kids > details.perm').forEach(function(c){ if (visit(c)) vis = true; });
|
|
node.style.display = (anyHide && !vis) ? 'none' : '';
|
|
return vis;
|
|
}
|
|
document.querySelectorAll('#permroot > details.perm').forEach(function(top){ visit(top); });
|
|
}
|
|
// Apply the initial filter state (e.g. when the system toggle starts checked).
|
|
applyPermFilters();
|
|
</script>
|
|
</body></html>
|
|
"@
|
|
|
|
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() |