11param (
22 [Parameter (Mandatory = $true ,
3- HelpMessage = " Root path to audit (e.g. \\fileserver\share or C:\Data)" )]
3+ HelpMessage = " Root path to audit (e.g. \\fileserver\\ share or C:\ \Data)" )]
44 [string ]$RootPath ,
55
66 [Parameter (Mandatory = $false ,
99
1010 [Parameter (Mandatory = $false ,
1111 HelpMessage = " Path to log file" )]
12- [string ]$LogFilePath = $ (Join-Path - Path (Get-Location ) - ChildPath (" FolderAclAudit_{0:yyyyMMdd_HHmmss}.log" -f (Get-Date )))
12+ [string ]$LogFilePath = $ (Join-Path - Path (Get-Location ) - ChildPath (" FolderAclAudit_{0:yyyyMMdd_HHmmss}.log" -f (Get-Date ))),
13+
14+ [Parameter (Mandatory = $true ,
15+ HelpMessage = " Max depth: 0=root only, 1=root+children, 2=root+children+grandchildren, etc. Press ENTER for unlimited." )]
16+ [string ]$MaxDepth
1317)
1418
19+ # --- Normalize & validate MaxDepth ---
20+
21+ [int ]$MaxDepthInt = [int ]::MaxValue
22+
23+ if ([string ]::IsNullOrWhiteSpace($MaxDepth )) {
24+ # User hit ENTER -> unlimited depth
25+ $MaxDepthInt = [int ]::MaxValue
26+ } else {
27+ $parsed = 0
28+ if (-not [int ]::TryParse($MaxDepth , [ref ]$parsed )) {
29+ Write-Error " MaxDepth must be a valid non-negative integer."
30+ exit 1
31+ }
32+
33+ if ($parsed -lt 0 ) {
34+ Write-Warning " MaxDepth cannot be negative. Using 0 (root only)."
35+ $MaxDepthInt = 0
36+ } else {
37+ $MaxDepthInt = $parsed
38+ }
39+ }
40+
41+ # --- Core script starts here ---
42+
1543# Ensure root path exists
1644if (-not (Test-Path - LiteralPath $RootPath )) {
1745 Write-Error " Root path '$RootPath ' does not exist or is not reachable."
2553 Write-Warning " Failed to start transcript logging: $ ( $_.Exception.Message ) "
2654}
2755
56+ $maxDepthDisplay = if ($MaxDepthInt -eq [int ]::MaxValue) { " Unlimited" } else { $MaxDepthInt }
57+
2858Write-Host " Starting FOLDER-ONLY ACL audit (NTFS + Share)..."
2959Write-Host " Root path : $RootPath "
60+ Write-Host " Max depth : $maxDepthDisplay "
3061Write-Host " Output CSV : $OutputCsvPath "
3162Write-Host " Log file : $LogFilePath "
3263Write-Host " Start time : $ ( Get-Date ) "
@@ -79,7 +110,7 @@ function Get-ShareInfo {
79110 # UNC path: \\Server\Share\...
80111 if ($RootPath.StartsWith (" \\" )) {
81112 if ($RootPath -match " ^\\\\([^\\]+)\\([^\\]+)" ) {
82- $server = $matches [1 ]
113+ $server = $matches [1 ]
83114 $shareName = $matches [2 ]
84115
85116 $shareProps.ShareServer = $server
@@ -136,7 +167,6 @@ function Get-ShareInfo {
136167 return [pscustomobject ]$shareProps
137168}
138169
139- $results = New-Object System.Collections.Generic.List[psobject ]
140170$errors = New-Object System.Collections.Generic.List[psobject ]
141171
142172# Normalize root path for depth calculations
@@ -150,56 +180,35 @@ if ($shareInfo.ShareName) {
150180 Write-Log " No matching share information could be resolved for root path '$RootPath '." " WARN"
151181}
152182
153- # Get list of all FOLDERS, including the root itself
154- Write-Log " Enumerating folders under '$RootPath '..."
183+ # Global counters & CSV state
184+ $idCounter = 0
185+ $script :CsvInitialized = $false
155186
156- $allFolders = @ ()
187+ # Helper: write one ACE row to CSV (streaming, header once)
188+ function Write-AceRow {
189+ param (
190+ [pscustomobject ]$Row ,
191+ [string ]$CsvPath
192+ )
157193
158- try {
159- # Root folder
160- $rootItem = Get-Item - LiteralPath $RootPath - ErrorAction Stop
161- if (-not $rootItem.PSIsContainer ) {
162- Write-Error " Root path '$RootPath ' is not a folder."
163- exit 1
194+ if (-not $script :CsvInitialized ) {
195+ $Row | Export-Csv - Path $CsvPath - NoTypeInformation - Encoding UTF8
196+ $script :CsvInitialized = $true
197+ } else {
198+ $Row | Export-Csv - Path $CsvPath - NoTypeInformation - Encoding UTF8 - Append
164199 }
165- $allFolders += $rootItem
166-
167- # Subfolders only
168- $children = Get-ChildItem - LiteralPath $RootPath - Directory - Recurse - Force - ErrorAction SilentlyContinue
169- $allFolders += $children
170- } catch {
171- Write-Log " Failed to enumerate folders under '$RootPath ': $ ( $_.Exception.Message ) " " ERROR"
172200}
173201
174- $total = $allFolders.Count
175- Write-Log " Total folders found: $total "
176-
177- $index = 0
178- $idCounter = 0 # Global row ID
179-
180- foreach ($folder in $allFolders ) {
181- $index ++
182- $percent = [int ](($index / [math ]::Max($total , 1 )) * 100 )
183-
184- Write-Progress - Activity " Auditing folder ACLs" - Status $folder.FullName - PercentComplete $percent
185-
186- try {
187- $acl = Get-Acl - LiteralPath $folder.FullName - ErrorAction Stop
188- } catch {
189- $errObj = [pscustomobject ]@ {
190- Path = $folder.FullName
191- Error = $_.Exception.Message
192- TimeStamp = Get-Date
193- }
194- $errors.Add ($errObj ) | Out-Null
195- Write-Log " Failed to get ACL for '$ ( $folder.FullName ) ': $ ( $_.Exception.Message ) " " ERROR"
196- continue
197- }
202+ # Helper: compute depth for a folder relative to root
203+ function Get-FolderDepth {
204+ param (
205+ [string ]$FolderPath ,
206+ [string ]$RootPath
207+ )
198208
199- # Calculate parent folder and depth (relative to root )
200- $parentFolder = Split-Path - LiteralPath $folder .FullName - Parent
209+ $normalizedFolder = $FolderPath .TrimEnd ( ' \ ' )
210+ $normalizedRoot = $RootPath .TrimEnd ( ' \ ' )
201211
202- $normalizedFolder = $folder.FullName.TrimEnd (' \' )
203212 $folderDepth = 0
204213 if ($normalizedFolder.Length -gt $normalizedRoot.Length -and
205214 $normalizedFolder.StartsWith ($normalizedRoot , [System.StringComparison ]::OrdinalIgnoreCase)) {
@@ -210,52 +219,105 @@ foreach ($folder in $allFolders) {
210219 }
211220 }
212221
213- # Keep ACE order per folder
222+ return $folderDepth
223+ }
224+
225+ # Helper: process a single folder (get ACL, emit rows)
226+ function Process-Folder {
227+ param (
228+ [System.IO.DirectoryInfo ]$Folder ,
229+ [int ]$Depth ,
230+ [pscustomobject ]$ShareInfo ,
231+ [string ]$CsvPath ,
232+ [ref ]$IdCounterRef ,
233+ [System.Collections.Generic.List [psobject ]]$ErrorsList
234+ )
235+
236+ Write-Progress - Activity " Auditing folder ACLs" - Status $Folder.FullName
237+
238+ try {
239+ $acl = Get-Acl - LiteralPath $Folder.FullName - ErrorAction Stop
240+ } catch {
241+ $errObj = [pscustomobject ]@ {
242+ Path = $Folder.FullName
243+ Error = $_.Exception.Message
244+ TimeStamp = Get-Date
245+ }
246+ $ErrorsList.Add ($errObj ) | Out-Null
247+ Write-Log " Failed to get ACL for '$ ( $Folder.FullName ) ': $ ( $_.Exception.Message ) " " ERROR"
248+ return
249+ }
250+
251+ $parentFolder = Split-Path - LiteralPath $Folder.FullName - Parent
214252 $aceOrder = 0
215253
216254 foreach ($ace in $acl.Access ) {
217255 $aceOrder ++
218- $idCounter ++
256+ $IdCounterRef .Value ++
219257
220258 $permissionLevel = Get-PermissionLevel - Rights $ace.FileSystemRights
221259 $aceType = if ($ace.IsInherited ) { " Inherited" } else { " Explicit" }
222260
223- $obj = [pscustomobject ]@ {
224- ID = $idCounter
225- Path = $folder .FullName
226- ItemType = " Folder"
227- ParentFolder = $parentFolder
228- FolderDepth = $folderDepth
229- ShareServer = $shareInfo .ShareServer
230- ShareName = $shareInfo .ShareName
231- ShareLocalPath = $shareInfo .ShareLocalPath
232- ShareAccessSummary = $shareInfo .ShareAccessSummary
233- ACEOrder = $aceOrder
234- ACEType = $aceType
235- Identity = $ace.IdentityReference.Value
236- FileSystemRights = $ace.FileSystemRights.ToString ()
237- PermissionLevel = $permissionLevel
238- AccessControlType = $ace.AccessControlType.ToString () # Allow / Deny
239- InheritanceFlags = $ace.InheritanceFlags.ToString ()
240- PropagationFlags = $ace.PropagationFlags.ToString ()
241- IsInherited = $ace.IsInherited
242- Owner = $acl.Owner
243- LastWriteTime = $folder .LastWriteTime
244- CreationTime = $folder .CreationTime
261+ $row = [pscustomobject ]@ {
262+ ID = $IdCounterRef .Value
263+ Path = $Folder .FullName
264+ ItemType = " Folder"
265+ ParentFolder = $parentFolder
266+ FolderDepth = $Depth
267+ ShareServer = $ShareInfo .ShareServer
268+ ShareName = $ShareInfo .ShareName
269+ ShareLocalPath = $ShareInfo .ShareLocalPath
270+ ShareAccessSummary = $ShareInfo .ShareAccessSummary
271+ ACEOrder = $aceOrder
272+ ACEType = $aceType
273+ Identity = $ace.IdentityReference.Value
274+ FileSystemRights = $ace.FileSystemRights.ToString ()
275+ PermissionLevel = $permissionLevel
276+ AccessControlType = $ace.AccessControlType.ToString ()
277+ InheritanceFlags = $ace.InheritanceFlags.ToString ()
278+ PropagationFlags = $ace.PropagationFlags.ToString ()
279+ IsInherited = $ace.IsInherited
280+ Owner = $acl.Owner
281+ LastWriteTime = $Folder .LastWriteTime
282+ CreationTime = $Folder .CreationTime
245283 }
246- $results.Add ($obj ) | Out-Null
284+
285+ Write-AceRow - Row $row - CsvPath $CsvPath
247286 }
248287}
249288
250- Write-Log " Finished collecting ACLs. Exporting to CSV ..."
289+ Write-Log " Enumerating and auditing folders under ' $RootPath ' with MaxDepth = $maxDepthDisplay ..."
251290
291+ # Process root folder
252292try {
253- $results | Export-Csv - Path $OutputCsvPath - NoTypeInformation - Encoding UTF8
254- Write-Log " ACL data exported to '$OutputCsvPath '"
293+ $rootItem = Get-Item - LiteralPath $RootPath - ErrorAction Stop
294+ if (-not $rootItem.PSIsContainer ) {
295+ Write-Error " Root path '$RootPath ' is not a folder."
296+ exit 1
297+ }
255298} catch {
256- Write-Log " Failed to export ACL data: $ ( $_.Exception.Message ) " " ERROR"
299+ Write-Log " Failed to access root path '$RootPath ': $ ( $_.Exception.Message ) " " ERROR"
300+ exit 1
257301}
258302
303+ # Root depth is always 0
304+ Process - Folder - Folder $rootItem - Depth 0 - ShareInfo $shareInfo - CsvPath $OutputCsvPath - IdCounterRef ([ref ]$idCounter ) - ErrorsList $errors
305+
306+ # If MaxDepthInt is 0, we stop at the root
307+ if ($MaxDepthInt -gt 0 ) {
308+ # Stream all subfolders and filter by depth
309+ Get-ChildItem - LiteralPath $RootPath - Directory - Recurse - Force - ErrorAction SilentlyContinue |
310+ ForEach-Object {
311+ $folderDepth = Get-FolderDepth - FolderPath $_.FullName - RootPath $RootPath
312+
313+ if ($folderDepth -le $MaxDepthInt ) {
314+ Process - Folder - Folder $_ - Depth $folderDepth - ShareInfo $shareInfo - CsvPath $OutputCsvPath - IdCounterRef ([ref ]$idCounter ) - ErrorsList $errors
315+ }
316+ }
317+ }
318+
319+ Write-Log " Finished collecting ACLs. CSV written to '$OutputCsvPath '"
320+
259321if ($errors.Count -gt 0 ) {
260322 $errorCsvPath = [System.IO.Path ]::ChangeExtension($OutputCsvPath , " .errors.csv" )
261323 try {
@@ -269,10 +331,11 @@ if ($errors.Count -gt 0) {
269331}
270332
271333Write-Host " End time : $ ( Get-Date ) "
334+ Write-Host " Total ACE rows: $idCounter "
272335Write-Host " Audit complete."
273336
274337try {
275338 Stop-Transcript | Out-Null
276339} catch {
277340 Write-Warning " Failed to stop transcript: $ ( $_.Exception.Message ) "
278- }
341+ }
0 commit comments