Skip to content

Commit de3c7d2

Browse files
authored
Create FolderAclAudit.ps1
1 parent 5e2f553 commit de3c7d2

File tree

1 file changed

+278
-0
lines changed

1 file changed

+278
-0
lines changed
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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

Comments
 (0)