
Detect and Quarantine Suspicious File Extensions in Shared Folders
Before you run this
This guide builds a PowerShell script that scans an SMB shared folder for files whose extensions are commonly used to carry malware (.exe, .scr, .js, .vbs, .bat, and so on), and moves any it finds into a locked-down quarantine folder outside the share, writing every action to a log. It is a crude, extension-based screen — a tripwire, not an antivirus engine. It does not inspect file contents and will not catch a malicious .docm or a renamed payload. Treat it as one layer, not the layer.
It moves files, and moving is disruptive. A user who dropped a legitimate installer.exe on the share will find it gone. Nothing is deleted, so the action is reversible (you can move files back from quarantine), but it will interrupt people mid-work if your extension list is too aggressive. Read the whole script, tune the extension list to your environment, and run it with -WhatIf first so it only reports what it would move.
Privileges: the script needs an account with read/write (modify) NTFS rights on both the scanned share and the quarantine folder. Registering it as a scheduled task requires an elevated PowerShell session (Run as Administrator). I assume the task runs under a dedicated service account, not a personal admin login.
Test first. Do not point this at a production share on the first run. Create a throwaway folder, drop a few harmless dummy files (rename a text file to test.exe), and run against that. Only widen the scope once you trust the output.
Assumptions for this guide: Windows Server 2019 or 2022, Windows PowerShell 5.1 (the version that ships in the box). The ScheduledTasks module used for automation is present on Server 2012 and later. If you run PowerShell 7 the script itself works unchanged; the scheduled-task registration is identical.
The plan
A scheduled sweep, not a live watcher. You can build a real-time trigger with the .NET FileSystemWatcher class, but it is fiddly to keep running reliably as a service and it drops events under load. A scheduled scan every few minutes is boring, predictable, and easy to reason about — the standard choice. I'll mention FileSystemWatcher by name and leave it there.
The script does four things: enumerate files matching a blocklist, move each to quarantine (preserving its relative path so nothing collides), log it, and exit. A -WhatIf switch makes the whole thing report-only.
The script
Save this as C:\Scripts\Quarantine-Extensions.ps1. Replace the paths and the extension list with your own.
[CmdletBinding(SupportsShouldProcess = $true)]
param(
# The share to scan. Replace with your real UNC or local path.
[string]$SharePath = '\\fileserver\shared',
# Where flagged files go. Keep this OFF the share, ACL'd to admins only.
[string]$QuarantinePath = 'D:\Quarantine',
# Log file for every move. Review this regularly.
[string]$LogPath = 'C:\Scripts\quarantine.log',
# The extensions to catch. Tune this to your environment.
[string[]]$BadExtensions = @(
'.exe','.scr','.pif','.com','.bat','.cmd',
'.vbs','.vbe','.js','.jse','.wsf','.wsh',
'.ps1','.hta','.cpl','.jar'
)
)
function Write-Log {
param([string]$Message)
$stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LogPath -Value "$stamp $Message"
}
# Fail early if paths are wrong, rather than half-processing.
if (-not (Test-Path -LiteralPath $SharePath)) {
Write-Log "ERROR: share path not found: $SharePath"
throw "Share path not found: $SharePath"
}
if (-not (Test-Path -LiteralPath $QuarantinePath)) {
New-Item -Path $QuarantinePath -ItemType Directory -Force | Out-Null
}
# -Include needs a wildcard on the path; -File keeps us to files only.
$candidates = Get-ChildItem -LiteralPath $SharePath -Recurse -File -Force -ErrorAction SilentlyContinue |
Where-Object { $BadExtensions -contains $_.Extension.ToLower() }
foreach ($file in $candidates) {
# Rebuild the file's path relative to the share so structure is preserved.
$relative = $file.FullName.Substring($SharePath.TrimEnd('\').Length).TrimStart('\')
$target = Join-Path $QuarantinePath $relative
$targetDir = Split-Path $target -Parent
if ($PSCmdlet.ShouldProcess($file.FullName, "Move to $target")) {
try {
if (-not (Test-Path -LiteralPath $targetDir)) {
New-Item -Path $targetDir -ItemType Directory -Force | Out-Null
}
Move-Item -LiteralPath $file.FullName -Destination $target -Force
Write-Log "MOVED: $($file.FullName) -> $target"
}
catch {
Write-Log "FAILED: $($file.FullName) ($($_.Exception.Message))"
}
}
else {
# -WhatIf path: report only.
Write-Log "WOULD MOVE: $($file.FullName) -> $target"
}
}
A few notes on the non-obvious parts:
SupportsShouldProcessis what gives you-WhatIfand-Confirmfor free, and it's what gates theMove-ItembehindShouldProcess. This is the standard PowerShell pattern for a script that changes things.-ForceonGet-ChildItemincludes hidden and system files, which is exactly where you want to look.- I match on
$_.Extensionrather than filtering with-Include, because it's easier to read and keeps the double-extension case (invoice.pdf.exe) working —.Extensionreturns only the final.exe. - Preserving the relative path under quarantine avoids two files named
update.exefrom different folders overwriting each other.
First run: report only
Run this by hand, elevated, and do not skip the -WhatIf:
C:\Scripts\Quarantine-Extensions.ps1 -SharePath '\\fileserver\shared' -WhatIf
Read C:\Scripts\quarantine.log. Every line will start WOULD MOVE:. Check it against reality. If it flags files people legitimately need, remove those extensions from $BadExtensions before you go live. When the report looks right, run it for real by dropping -WhatIf:
C:\Scripts\Quarantine-Extensions.ps1 -SharePath '\\fileserver\shared'
Automating it with a scheduled task
Register it to run every 15 minutes under your service account. Do this in an elevated session. You'll be prompted for the account password.
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\Quarantine-Extensions.ps1"'
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) `
-RepetitionInterval (New-TimeSpan -Minutes 15)
Register-ScheduledTask -TaskName 'Quarantine-Extensions' `
-Action $action -Trigger $trigger `
-User 'DOMAIN\svc-quarantine' `
-RunLevel Highest `
-Description 'Sweep share for dangerous file extensions'
Replace DOMAIN\svc-quarantine with your service account. -RunLevel Highest runs it elevated. For the exact parameter reference — including -Password if you want to supply it non-interactively — see Microsoft Learn for Register-ScheduledTask and New-ScheduledTaskTrigger; the repetition-interval syntax in particular is worth confirming there against your OS build.
Lock down the quarantine folder. It now holds live executables. Set NTFS permissions so only administrators can read it, and never re-share it:
icacls 'D:\Quarantine' /inheritance:r /grant:r 'BUILTIN\Administrators:(OI)(CI)F'
Verify it worked
Confirm the task exists and its last result:
Get-ScheduledTask -TaskName 'Quarantine-Extensions'
Get-ScheduledTaskInfo -TaskName 'Quarantine-Extensions' | Select-Object LastRunTime, LastTaskResult
LastTaskResult of 0 means the last run completed. Then drop a dummy test.exe on the share, wait for the next cycle, and confirm it landed in quarantine and was logged:
Get-Content C:\Scripts\quarantine.log -Tail 20
Get-ChildItem D:\Quarantine -Recurse -File
Undo
Because the script only moves files, recovery is a move back. To restore a single file, copy it from quarantine to its original relative location under the share. To pull the automation entirely:
Unregister-ScheduledTask -TaskName 'Quarantine-Extensions' -Confirm:$false
That removes the schedule but leaves quarantined files and the log where they are, so you can review them before deciding what to release or delete. Delete quarantined files only once you're certain — and, for anything you're unsure about, submit it to your antivirus or a sandbox first rather than trusting the extension alone.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
Related guides
Automate a Daily Failed-Logon (4625) Report with PowerShell
This guide builds a scheduled PowerShell job that reads Event ID 4625 (failed logon) from the Windows Security log for the last 24 hours and writes them to a dated HTML report. It is read-only — it queries the event log and creates a report file. It does not change auditing policy, delete events, or touch accounts.
Automate DHCP Lease Reporting on Windows Server
This guide builds a read-only PowerShell script that enumerates your DHCP scopes, pulls the current IPv4 leases from each, writes them to a timestamped CSV, and (optionally) emails the file. It does not change any DHCP configuration — no scopes, reservations, options, or leases are created, modified, or deleted. The worst it can do is fill a disk with CSVs if you never prune them.
Automate a Windows Server Hardening Baseline Check with PowerShell
This script is a read-only audit . It reports PASS/FAIL for a small set of well-known hardening settings — SMBv1 status, firewall profiles, Defender real-time protection, RDP Network Level Authentication, and the local Guest account — and writes the results to the screen and a CSV. It changes nothing. There is nothing to roll back, which is exactly why an audit script is the safe place to start before you touch any actual configuration.
Automate a Network Share Permissions Audit with PowerShell
This guide builds a read-only report . The script enumerates the SMB shares on a Windows file server, then lists two things for each one: the share-level permissions (the "who can connect" layer) and the NTFS permissions on the folder behind it (the "who can touch the files" layer). It writes both to CSV so you can review access in a spreadsheet instead of clicking through the Security tab share by share.




