In partnership with

THE PROBLEM

File share permissions are where good intentions go to die.

Somebody needs quick access to a folder. Adding them to the right group means a ticket, an approval, and a wait. Granting Everyone full control takes four seconds and the problem goes away. Nobody comes back to fix it, because from the user's perspective nothing is broken.

Multiply that across five years and a few staff changes, and you end up with shares that expose HR documents to the entire domain, finance folders inheriting permissions nobody intended, and at least one share where Everyone has Full Control on something genuinely sensitive.

The reason this never gets cleaned up is that checking it manually is brutal. Right-click, Properties, Sharing, Advanced Sharing, Permissions, then Security tab, then Advanced, then repeat for every share on every server. Nobody does that voluntarily.

This week we automate the whole thing.

THE SCRIPT

Save the script below to:

C:\Scripts\SharePermissionAudit.ps1

powershell

# File Share & NTFS Permission Auditor
# Automate & Operate — automateandoperate.com

# Email settings — update these
$from = "[email protected]"
$to = "[email protected]"
$smtpServer = "your.smtp.server"
$smtpPort = 587
$username = "[email protected]"
$password = ConvertTo-SecureString "yourpassword" -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($username, $password)

$logFile = "C:\Logs\SharePermissionAudit.log"
$reportFile = "C:\Logs\SharePermissionReport.csv"

# File servers to audit
$servers = @(
    "localhost",
    "FILESERVER01",
    "FILESERVER02"
)

# Shares to skip — administrative and print shares
$ignoreShares = @(
    "ADMIN$", "IPC$", "print$", "SYSVOL", "NETLOGON"
)

# Identities that should never have broad access to a data share
$riskyIdentities = @(
    "Everyone",
    "BUILTIN\Users",
    "NT AUTHORITY\Authenticated Users",
    "NT AUTHORITY\ANONYMOUS LOGON",
    "Domain Users"
)

# Rights considered dangerous when held by a risky identity
$riskyRights = @(
    "FullControl",
    "Modify",
    "Write",
    "ChangePermissions",
    "TakeOwnership"
)

# How many subfolder levels below the share root to check NTFS on
# 0 = share root only. 1 or 2 is usually plenty. Higher gets slow fast.
$subfolderDepth = 1

# Create log folder if missing
if (-not (Test-Path "C:\Logs")) {
    New-Item -ItemType Directory -Path "C:\Logs"
}

$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$report = @()
$findings = @()

foreach ($server in $servers) {
    try {
        $shareData = Invoke-Command -ComputerName $server -ArgumentList $ignoreShares, $subfolderDepth -ScriptBlock {
            param($ignoreShares, $subfolderDepth)

            $results = @()
            $shares = Get-SmbShare | Where-Object {
                $ignoreShares -notcontains $_.Name -and $_.Path -ne ""
            }

            foreach ($share in $shares) {

                # ---- Share-level permissions ----
                try {
                    $shareAccess = Get-SmbShareAccess -Name $share.Name -ErrorAction Stop
                    foreach ($access in $shareAccess) {
                        $results += [PSCustomObject]@{
                            ShareName  = $share.Name
                            SharePath  = $share.Path
                            Scope      = "SHARE"
                            TargetPath = $share.Path
                            Identity   = $access.AccountName
                            Rights     = "$($access.AccessRight)"
                            Type       = "$($access.AccessControlType)"
                            Inherited  = "N/A"
                        }
                    }
                } catch {
                    $results += [PSCustomObject]@{
                        ShareName  = $share.Name
                        SharePath  = $share.Path
                        Scope      = "SHARE"
                        TargetPath = $share.Path
                        Identity   = "ERROR READING SHARE ACL"
                        Rights     = "$_"
                        Type       = "N/A"
                        Inherited  = "N/A"
                    }
                }

                # ---- Build list of paths to check NTFS on ----
                $pathsToCheck = @($share.Path)

                if ($subfolderDepth -gt 0 -and (Test-Path $share.Path)) {
                    try {
                        $subFolders = Get-ChildItem -Path $share.Path -Directory -Depth ($subfolderDepth - 1) -ErrorAction SilentlyContinue
                        $pathsToCheck += $subFolders.FullName
                    } catch { }
                }

                # ---- NTFS permissions ----
                foreach ($path in $pathsToCheck) {
                    try {
                        $acl = Get-Acl -Path $path -ErrorAction Stop
                        foreach ($ace in $acl.Access) {
                            $results += [PSCustomObject]@{
                                ShareName  = $share.Name
                                SharePath  = $share.Path
                                Scope      = "NTFS"
                                TargetPath = $path
                                Identity   = $ace.IdentityReference.Value
                                Rights     = "$($ace.FileSystemRights)"
                                Type       = "$($ace.AccessControlType)"
                                Inherited  = "$($ace.IsInherited)"
                            }
                        }
                    } catch {
                        $results += [PSCustomObject]@{
                            ShareName  = $share.Name
                            SharePath  = $share.Path
                            Scope      = "NTFS"
                            TargetPath = $path
                            Identity   = "ERROR READING NTFS ACL"
                            Rights     = "$_"
                            Type       = "N/A"
                            Inherited  = "N/A"
                        }
                    }
                }
            }

            return $results

        } -ErrorAction Stop

        foreach ($entry in $shareData) {

            # Decide whether this entry is a finding
            $isRiskyIdentity = $false
            foreach ($risky in $riskyIdentities) {
                if ($entry.Identity -like "*$risky") { $isRiskyIdentity = $true; break }
            }

            $isRiskyRight = $false
            foreach ($right in $riskyRights) {
                if ($entry.Rights -like "*$right*") { $isRiskyRight = $true; break }
            }

            $isAllow = $entry.Type -eq "Allow"

            $severity = if ($isRiskyIdentity -and $isRiskyRight -and $isAllow) {
                if ($entry.Rights -like "*FullControl*") { "HIGH" } else { "MEDIUM" }
            } elseif ($isRiskyIdentity -and $isAllow) {
                "LOW"
            } else {
                "OK"
            }

            $record = [PSCustomObject]@{
                Server     = $server
                ShareName  = $entry.ShareName
                SharePath  = $entry.SharePath
                Scope      = $entry.Scope
                TargetPath = $entry.TargetPath
                Identity   = $entry.Identity
                Rights     = $entry.Rights
                Type       = $entry.Type
                Inherited  = $entry.Inherited
                Severity   = $severity
            }

            $report += $record

            if ($severity -eq "HIGH" -or $severity -eq "MEDIUM") {
                $findings += $record
            }
        }

        Add-Content -Path $logFile -Value "$timestamp$server$($shareData.Count) ACL entries reviewed"

    } catch {
        $report += [PSCustomObject]@{
            Server     = $server
            ShareName  = "UNREACHABLE"
            SharePath  = "N/A"
            Scope      = "N/A"
            TargetPath = "N/A"
            Identity   = "N/A"
            Rights     = "N/A"
            Type       = "N/A"
            Inherited  = "N/A"
            Severity   = "ERROR"
        }
        Add-Content -Path $logFile -Value "$timestamp$server — ERROR: $_"
    }
}

# Export full report
$report | Sort-Object Severity, Server, ShareName | Export-Csv -Path $reportFile -NoTypeInformation

$highFindings = $findings | Where-Object { $_.Severity -eq "HIGH" }
$medFindings  = $findings | Where-Object { $_.Severity -eq "MEDIUM" }

# ---------- BUILD EMAIL ----------
$emailBody = "File Share Permission Audit — $timestamp`n"
$emailBody += "Servers audited: $($servers.Count)`n"
$emailBody += "Subfolder depth checked: $subfolderDepth`n"
$emailBody += "Total ACL entries reviewed: $($report.Count)`n"
$emailBody += "HIGH severity findings: $($highFindings.Count)`n"
$emailBody += "MEDIUM severity findings: $($medFindings.Count)`n`n"

if ($highFindings.Count -gt 0) {
    $emailBody += "HIGH — BROAD IDENTITY WITH FULL CONTROL:`n"
    $emailBody += "=" * 60 + "`n"
    foreach ($f in $highFindings) {
        $emailBody += "Server:   $($f.Server)`n"
        $emailBody += "Share:    $($f.ShareName)`n"
        $emailBody += "Path:     $($f.TargetPath)`n"
        $emailBody += "Scope:    $($f.Scope)`n"
        $emailBody += "Identity: $($f.Identity)`n"
        $emailBody += "Rights:   $($f.Rights)`n"
        $emailBody += "-" * 40 + "`n"
    }
    $emailBody += "`n"
}

if ($medFindings.Count -gt 0) {
    $emailBody += "MEDIUM — BROAD IDENTITY WITH WRITE OR MODIFY:`n"
    $emailBody += "=" * 60 + "`n"
    foreach ($f in $medFindings) {
        $emailBody += "$($f.Server) | $($f.ShareName) | $($f.Scope) | $($f.Identity) | $($f.Rights)`n"
        $emailBody += "  Path: $($f.TargetPath)`n"
    }
    $emailBody += "`n"
}

if ($findings.Count -eq 0) {
    $emailBody += "No overly permissive share or NTFS entries detected.`n`n"
}

$emailBody += "SHARE INVENTORY:`n"
$emailBody += "=" * 60 + "`n"
$report | Where-Object { $_.Scope -eq "SHARE" } |
    Select-Object Server, ShareName, SharePath -Unique |
    ForEach-Object { $emailBody += "$($_.Server)$($_.ShareName)$($_.SharePath)`n" }

$emailBody += "`nFull report saved to: $reportFile`n"
$emailBody += "`nTo inspect a path in detail:`n"
$emailBody += "(Get-Acl 'PATH').Access | Format-Table IdentityReference, FileSystemRights, AccessControlType, IsInherited`n`n"
$emailBody += "Automate & Operate — automateandoperate.com"

$subject = if ($highFindings.Count -gt 0) {
    "SHARE ALERT: $($highFindings.Count) high severity permission finding(s)"
} elseif ($medFindings.Count -gt 0) {
    "Share Audit — $($medFindings.Count) medium severity finding(s) to review"
} else {
    "Share Permission Audit — No overly permissive entries found"
}

Send-MailMessage `
    -From $from -To $to `
    -Subject $subject `
    -Body $emailBody `
    -SmtpServer $smtpServer -Port $smtpPort `
    -UseSsl -Credential $credential

Update these lines:

  • $from / $to / $username / $password — your email details

  • $smtpServer — your SMTP server (e.g. smtp.office365.com)

  • $servers — your actual file servers

  • $subfolderDepth — start at 1, see the note below

  • $riskyIdentities — add YOURDOMAIN\Domain Users in the exact format your environment returns

SHARE PERMISSIONS VS NTFS PERMISSIONS

This trips people up constantly, so it's worth being precise. The script checks both because they're two separate gates.

Share permissions apply only over the network. Most shares are set to Everyone / Full Control at the share level deliberately, with the real restrictions applied via NTFS. That's the standard modern approach and it's not automatically wrong.

NTFS permissions apply everywhere — network and local — and they're the ones that actually enforce your access model.

The effective permission is the more restrictive of the two. So Everyone / Full Control at the share level with tight NTFS underneath is fine. The dangerous combination is broad access on both, or broad access on NTFS regardless of the share.

The Scope column in the CSV tells you which layer each finding came from. Read the NTFS ones first — those are the real exposures.

A WORD ON SUBFOLDER DEPTH

$subfolderDepth = 1 checks the share root and its immediate children. That's usually where broken inheritance and one-off grants live.

Be careful raising this. Depth 3 on a file server with a deep folder tree can mean hundreds of thousands of ACL reads and a script that runs for hours. Start at 1, see how long it takes, and raise it only if you have a specific reason.

If you need a genuinely deep audit of one problem share, run it as a one-off against a single server with a higher depth rather than raising it for the whole environment.

READING THE SEVERITY LEVELS

HIGH — a broad identity like Everyone or Domain Users has Full Control. This is the one to act on. Full Control includes the ability to change permissions, which means a user can grant themselves or others whatever access they want and remove your audit trail while doing it.

MEDIUM — a broad identity has Write or Modify. Often intentional on shared working folders. Verify it's deliberate rather than inherited by accident.

LOW — a broad identity has Read only. Frequently fine. Worth a glance if the share name suggests sensitive content.

The severity is mechanical — it doesn't know whether a folder holds payroll data or the office lunch menu. Context is yours to apply. But the HIGH list is short and specific, which is exactly what makes it actionable.

SETTING UP TASK SCHEDULER

  1. Press Windows key → type Task Scheduler → open it

  2. Click "Create Basic Task" → name it Share Permission Audit

  3. Trigger: Weekly → Sunday → 8:00pm

  4. Action: Start a program

  5. Program: powershell.exe

  6. Arguments:

-ExecutionPolicy Bypass -File "C:\Scripts\SharePermissionAudit.ps1"
  1. Click Next → Finish

Under Properties → General, check "Run with highest privileges" and use an account with admin rights on the target file servers.

Sunday evening is deliberate — ACL enumeration is I/O heavy and you don't want it competing with users on a Monday morning.

TEST IT

Open PowerShell as administrator and run:

powershell

powershell.exe -ExecutionPolicy Bypass -File "C:\Scripts\SharePermissionAudit.ps1"

Open C:\Logs\SharePermissionReport.csv and filter the Severity column to HIGH. That's your work list.

To see what the script sees on a single path before running the full audit:

powershell

Get-SmbShare | Where-Object { $_.Path -ne "" } | Format-Table Name, Path
(Get-Acl "C:\YourShare").Access | Format-Table IdentityReference, FileSystemRights, AccessControlType, IsInherited

If remote servers come back UNREACHABLE, enable PowerShell remoting on each target:

powershell

Enable-PSRemoting -Force

BEFORE YOU CHANGE ANYTHING

Permission changes break things faster than almost any other change you can make, and the breakage often shows up days later when somebody runs a monthly process.

Two rules that will save you:

Never remove an entry you don't understand. Find out what depends on it first. An Authenticated Users grant might be there because a service account needs it and nobody documented that.

Fix inheritance before fixing individual entries. If a subfolder has broken inheritance with a pile of one-off grants, restoring inheritance from a correctly configured parent is cleaner than editing twenty ACEs by hand.

Export the current ACL before you touch it:

powershell

Get-Acl "C:\YourShare" | Export-Clixml "C:\Logs\backup_share_acl.xml"

And to restore if you need to:

powershell

$acl = Import-Clixml "C:\Logs\backup_share_acl.xml"
Set-Acl -Path "C:\YourShare" -AclObject $acl

WHY THIS MATTERS

Overly permissive shares don't cause outages, which is exactly why they persist. Nothing breaks, no ticket gets raised, no alarm fires. The cost is invisible until it isn't — a ransomware event that encrypts far more than it should have, an insider who reads something they shouldn't, or an audit finding that turns into a remediation project with a deadline.

The operational benefit shows up sooner. Running this once usually reveals shares nobody knew still existed, folders where inheritance was broken years ago, and at least one path where a leaver's individual grant is still sitting in the ACL.

THIS WEEK'S ACTION

Run it against one file server today with depth 1. Filter the CSV to HIGH and count the rows. If that number is above zero — and for most environments running this for the first time, it is — you've found something worth fixing before anyone else finds it for you.

Reply to this email if you hit any issues — I read every reply.

Automate & Operate — automateandoperate.com

Granola Runs Revenue On Attio

"When I think of revenue, I think of Attio." - Shreman Shrestha, Head of Business at Granola

Here's what that adds up to:

  • Zero missed leads and 10x faster access to customer context

  • Lead triage 83% faster

  • Five hours saved per week with automated updates