Skip to content

Commit b5820db

Browse files
authored
Enhance FolderAclAudit with MaxDepth parameter
Added MaxDepth parameter to control folder depth for ACL audit. Updated logic for processing folders and exporting results.
1 parent f70114b commit b5820db

1 file changed

Lines changed: 140 additions & 77 deletions

File tree

file-server-audit/FolderAclAudit.ps1

Lines changed: 140 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
param(
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,
@@ -9,9 +9,37 @@ param(
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
1644
if (-not (Test-Path -LiteralPath $RootPath)) {
1745
Write-Error "Root path '$RootPath' does not exist or is not reachable."
@@ -25,8 +53,11 @@ try {
2553
Write-Warning "Failed to start transcript logging: $($_.Exception.Message)"
2654
}
2755

56+
$maxDepthDisplay = if ($MaxDepthInt -eq [int]::MaxValue) { "Unlimited" } else { $MaxDepthInt }
57+
2858
Write-Host "Starting FOLDER-ONLY ACL audit (NTFS + Share)..."
2959
Write-Host "Root path : $RootPath"
60+
Write-Host "Max depth : $maxDepthDisplay"
3061
Write-Host "Output CSV : $OutputCsvPath"
3162
Write-Host "Log file : $LogFilePath"
3263
Write-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
252292
try {
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+
259321
if ($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

271333
Write-Host "End time : $(Get-Date)"
334+
Write-Host "Total ACE rows: $idCounter"
272335
Write-Host "Audit complete."
273336

274337
try {
275338
Stop-Transcript | Out-Null
276339
} catch {
277340
Write-Warning "Failed to stop transcript: $($_.Exception.Message)"
278-
}
341+
}

0 commit comments

Comments
 (0)