1+ param (
2+ [Parameter (Mandatory = $true ,
3+ HelpMessage = " Root path to audit (e.g. \\fileserver\share or C:\Data)" )]
4+ [string ]$RootPath ,
5+
6+ [Parameter (Mandatory = $false ,
7+ HelpMessage = " Path to output CSV file" )]
8+ [string ]$OutputCsvPath = $ (Join-Path - Path (Get-Location ) - ChildPath (" FolderAclAudit_{0:yyyyMMdd_HHmmss}.csv" -f (Get-Date ))),
9+
10+ [Parameter (Mandatory = $false ,
11+ HelpMessage = " Path to log file" )]
12+ [string ]$LogFilePath = $ (Join-Path - Path (Get-Location ) - ChildPath (" FolderAclAudit_{0:yyyyMMdd_HHmmss}.log" -f (Get-Date )))
13+ )
14+
15+ # Ensure root path exists
16+ if (-not (Test-Path - LiteralPath $RootPath )) {
17+ Write-Error " Root path '$RootPath ' does not exist or is not reachable."
18+ exit 1
19+ }
20+
21+ # Start transcript logging
22+ try {
23+ Start-Transcript - Path $LogFilePath - Append - ErrorAction Stop
24+ } catch {
25+ Write-Warning " Failed to start transcript logging: $ ( $_.Exception.Message ) "
26+ }
27+
28+ Write-Host " Starting FOLDER-ONLY ACL audit (NTFS + Share)..."
29+ Write-Host " Root path : $RootPath "
30+ Write-Host " Output CSV : $OutputCsvPath "
31+ Write-Host " Log file : $LogFilePath "
32+ Write-Host " Start time : $ ( Get-Date ) "
33+
34+ function Write-Log {
35+ param (
36+ [string ]$Message ,
37+ [string ]$Level = " INFO"
38+ )
39+ $timestamp = Get-Date - Format " yyyy-MM-dd HH:mm:ss"
40+ $line = " [$timestamp ] [$Level ] $Message "
41+ Write-Host $line
42+ }
43+
44+ # Map FileSystemRights to a simpler permission level
45+ function Get-PermissionLevel {
46+ param (
47+ [System.Security.AccessControl.FileSystemRights ]$Rights
48+ )
49+
50+ if ($Rights -band [System.Security.AccessControl.FileSystemRights ]::FullControl) {
51+ return " FullControl"
52+ }
53+
54+ if ($Rights -band [System.Security.AccessControl.FileSystemRights ]::Modify) {
55+ return " Modify"
56+ }
57+
58+ if ($Rights -band [System.Security.AccessControl.FileSystemRights ]::ReadAndExecute -or
59+ $Rights -band [System.Security.AccessControl.FileSystemRights ]::Read) {
60+ return " Read"
61+ }
62+
63+ return " Other"
64+ }
65+
66+ # Get share information (server, share name, path, and ACE summary)
67+ function Get-ShareInfo {
68+ param (
69+ [string ]$RootPath
70+ )
71+
72+ $shareProps = [ordered ]@ {
73+ ShareServer = $null
74+ ShareName = $null
75+ ShareLocalPath = $null
76+ ShareAccessSummary = $null
77+ }
78+
79+ # UNC path: \\Server\Share\...
80+ if ($RootPath.StartsWith (" \\" )) {
81+ if ($RootPath -match " ^\\\\([^\\]+)\\([^\\]+)" ) {
82+ $server = $matches [1 ]
83+ $shareName = $matches [2 ]
84+
85+ $shareProps.ShareServer = $server
86+ $shareProps.ShareName = $shareName
87+
88+ try {
89+ $session = New-CimSession - ComputerName $server - ErrorAction Stop
90+
91+ $share = Get-SmbShare - CimSession $session - Name $shareName - ErrorAction Stop
92+ $shareProps.ShareLocalPath = $share.Path
93+
94+ $access = Get-SmbShareAccess - CimSession $session - Name $shareName - ErrorAction Stop
95+ if ($access ) {
96+ $summary = $access | ForEach-Object {
97+ " $ ( $_.AccountName ) :$ ( $_.AccessControlType ) :$ ( $_.AccessRight ) "
98+ }
99+ $shareProps.ShareAccessSummary = ($summary -join " ; " )
100+ }
101+
102+ Remove-CimSession $session
103+ } catch {
104+ Write-Log " Could not retrieve share information for '\\$server \$shareName ': $ ( $_.Exception.Message ) " " WARN"
105+ }
106+ }
107+ }
108+ else {
109+ # Local path: try to find a local share whose path is a prefix of RootPath
110+ try {
111+ $shares = Get-SmbShare - ErrorAction SilentlyContinue | Where-Object {
112+ $_.Path -and ($RootPath -like " $ ( $_.Path ) *" )
113+ }
114+
115+ if ($shares ) {
116+ # Pick the most specific (longest path)
117+ $share = $shares | Sort-Object Path - Descending | Select-Object - First 1
118+
119+ $shareProps.ShareServer = $env: COMPUTERNAME
120+ $shareProps.ShareName = $share.Name
121+ $shareProps.ShareLocalPath = $share.Path
122+
123+ $access = Get-SmbShareAccess - Name $share.Name - ErrorAction SilentlyContinue
124+ if ($access ) {
125+ $summary = $access | ForEach-Object {
126+ " $ ( $_.AccountName ) :$ ( $_.AccessControlType ) :$ ( $_.AccessRight ) "
127+ }
128+ $shareProps.ShareAccessSummary = ($summary -join " ; " )
129+ }
130+ }
131+ } catch {
132+ Write-Log " Could not retrieve local share information for path '$RootPath ': $ ( $_.Exception.Message ) " " WARN"
133+ }
134+ }
135+
136+ return [pscustomobject ]$shareProps
137+ }
138+
139+ $results = New-Object System.Collections.Generic.List[psobject ]
140+ $errors = New-Object System.Collections.Generic.List[psobject ]
141+
142+ # Normalize root path for depth calculations
143+ $normalizedRoot = $RootPath.TrimEnd (' \' )
144+
145+ # Resolve share information once (same for all folders under this root)
146+ $shareInfo = Get-ShareInfo - RootPath $RootPath
147+ if ($shareInfo.ShareName ) {
148+ Write-Log " Share detected: $ ( $shareInfo.ShareServer ) \$ ( $shareInfo.ShareName ) (Path: $ ( $shareInfo.ShareLocalPath ) )"
149+ } else {
150+ Write-Log " No matching share information could be resolved for root path '$RootPath '." " WARN"
151+ }
152+
153+ # Get list of all FOLDERS, including the root itself
154+ Write-Log " Enumerating folders under '$RootPath '..."
155+
156+ $allFolders = @ ()
157+
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
164+ }
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"
172+ }
173+
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+ }
198+
199+ # Calculate parent folder and depth (relative to root)
200+ $parentFolder = Split-Path - LiteralPath $folder.FullName - Parent
201+
202+ $normalizedFolder = $folder.FullName.TrimEnd (' \' )
203+ $folderDepth = 0
204+ if ($normalizedFolder.Length -gt $normalizedRoot.Length -and
205+ $normalizedFolder.StartsWith ($normalizedRoot , [System.StringComparison ]::OrdinalIgnoreCase)) {
206+
207+ $relative = $normalizedFolder.Substring ($normalizedRoot.Length ).TrimStart(' \' )
208+ if ($relative ) {
209+ $folderDepth = $relative.Split (' \' ).Count
210+ }
211+ }
212+
213+ # Keep ACE order per folder
214+ $aceOrder = 0
215+
216+ foreach ($ace in $acl.Access ) {
217+ $aceOrder ++
218+ $idCounter ++
219+
220+ $permissionLevel = Get-PermissionLevel - Rights $ace.FileSystemRights
221+ $aceType = if ($ace.IsInherited ) { " Inherited" } else { " Explicit" }
222+
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
245+ }
246+ $results.Add ($obj ) | Out-Null
247+ }
248+ }
249+
250+ Write-Log " Finished collecting ACLs. Exporting to CSV..."
251+
252+ try {
253+ $results | Export-Csv - Path $OutputCsvPath - NoTypeInformation - Encoding UTF8
254+ Write-Log " ACL data exported to '$OutputCsvPath '"
255+ } catch {
256+ Write-Log " Failed to export ACL data: $ ( $_.Exception.Message ) " " ERROR"
257+ }
258+
259+ if ($errors.Count -gt 0 ) {
260+ $errorCsvPath = [System.IO.Path ]::ChangeExtension($OutputCsvPath , " .errors.csv" )
261+ try {
262+ $errors | Export-Csv - Path $errorCsvPath - NoTypeInformation - Encoding UTF8
263+ Write-Log " Encountered $ ( $errors.Count ) errors. Details saved to '$errorCsvPath '" " WARN"
264+ } catch {
265+ Write-Log " Failed to export error details: $ ( $_.Exception.Message ) " " ERROR"
266+ }
267+ } else {
268+ Write-Log " No ACL errors encountered."
269+ }
270+
271+ Write-Host " End time : $ ( Get-Date ) "
272+ Write-Host " Audit complete."
273+
274+ try {
275+ Stop-Transcript | Out-Null
276+ } catch {
277+ Write-Warning " Failed to stop transcript: $ ( $_.Exception.Message ) "
278+ }
0 commit comments