Category filter
PowerShell script to detect idle folders on Windows
In a corporate environment, employees frequently access, create, and manage large volumes of data, resulting in numerous open folders in their devices. If the folders remain open, it can create a significant security vulnerability, potentially exposing sensitive company data to unauthorized access. Moreover, open but idle folders may result in corporate devices suffering from decreased performance, higher cost of maintenance, and disrupted data management. Identifying the idle time or the time for which the system folders were left open without usage can help administrators archive such folders, increasing security.
This document provides PowerShell scripts to recognize open folders for all local user accounts and further archive folders that have been idle beyond a specified time limit. The Execute Custom Script remote action can help execute these PowerShell scripts on the target Windows devices.
Detect Open Folders for all User Accounts
The following PowerShell script recurrently checks the last activity time of all the subfolders in the root paths (the C:\ and D:\ drives) of the device and calculates the idle time. The function used for this purpose is the Get-FolderIdleTime function, which performs the scan. The scanning progress is logged and can be viewed in the Show Output button of the Action History. It lists a table containing the folder path, last activity, time and idle days.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
function Get-FolderIdleTime { param( [Parameter(Mandatory=$true)] [ValidateScript({Test-Path $_ -PathType Container})] [string]$TargetPath ) $LatestWriteTime = Get-ChildItem -Path $TargetPath -Recurse -File -Force -ErrorAction SilentlyContinue | Select-Object -ExpandProperty LastWriteTime | Sort-Object -Descending | Select-Object -First 1 if (-not $LatestWriteTime) { $LatestWriteTime = (Get-Item -Path $TargetPath -Force -ErrorAction SilentlyContinue).LastWriteTime } if (-not $LatestWriteTime) { $LatestWriteTime = Get-Date } $IdleTime = (Get-Date) - $LatestWriteTime [PSCustomObject]@{ FolderPath = $TargetPath LastActivity = $LatestWriteTime IdleDays = [math]::Round($IdleTime.TotalDays, 0) IdleTimeSpan = $IdleTime } } function Audit-IdleFolders { param( [Parameter(Mandatory=$true)] [string[]]$RootPaths ) $Report = @() Write-Host "Starting comprehensive audit of all subfolders..." -ForegroundColor Cyan foreach ($RootPath in $RootPaths) { Write-Host "`n-- Analyzing Root Path: $RootPath --" -ForegroundColor Yellow $FoldersToScan = Get-ChildItem -Path $RootPath -Directory -Force -ErrorAction SilentlyContinue foreach ($Folder in $FoldersToScan) { Write-Host " Checking Folder: $($Folder.Name)" -ForegroundColor Green $FolderStatus = Get-FolderIdleTime -TargetPath $Folder.FullName $Report += [PSCustomObject]@{ FolderPath = $FolderStatus.FolderPath LastActivity = $FolderStatus.LastActivity IdleDays = $FolderStatus.IdleDays } } } return $Report } $TargetAuditPaths = @( "C:\Users", "D:\SharedData" ) $ValidAuditPaths = @() foreach ($Path in $TargetAuditPaths) { if (Test-Path $Path -PathType Container) { $ValidAuditPaths += $Path } else { Write-Warning "Skipping invalid or inaccessible root path for audit: $Path." } } if ($ValidAuditPaths.Count -gt 0) { Audit-IdleFolders -RootPaths $ValidAuditPaths | Format-Table -AutoSize } else { Write-Error "No valid root paths were supplied for audit. Script aborted." } |
The output of the script listing the detected open folders can be viewed in the Show Output button from Manage > Your Windows device > Action History.
Archive Open Folders for all Users beyond a specified duration
The following PowerShell script recursively checks the last activity time of all the subfolders in the root paths and calculates the idle time, like the previous script. The threshold value is set at 3 days, beyond which the folder is considered idle. The idle folders are then archived to a designated archive location (C:\Data\Archive), preserving the internal folder structure (if subfolders are present) and the original user details associated with the folder (user who created the folder, user who last modified, etc.).
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
function Get-FolderIdleTime { param( [Parameter(Mandatory=$true)] [ValidateScript({Test-Path $_ -PathType Container})] [string]$TargetPath ) $LatestWriteTime = Get-ChildItem -Path $TargetPath -Recurse -File -Force -ErrorAction SilentlyContinue | Select-Object -ExpandProperty LastWriteTime | Sort-Object -Descending | Select-Object -First 1 if (-not $LatestWriteTime) { $LatestWriteTime = (Get-Item -Path $TargetPath -Force -ErrorAction SilentlyContinue).LastWriteTime } if (-not $LatestWriteTime) { $LatestWriteTime = Get-Date } $IdleTime = (Get-Date) - $LatestWriteTime [PSCustomObject]@{ FolderPath = $TargetPath LastActivity = $LatestWriteTime IdleDays = [math]::Round($IdleTime.TotalDays, 0) IdleTimeSpan = $IdleTime } } function Close-IdleFolders { param( [Parameter(Mandatory=$true)] [string[]]$RootPaths, [Parameter(Mandatory=$true)] [int]$ArchiveThresholdDays, [Parameter(Mandatory=$true)] [ValidateScript({Test-Path $_ -PathType Container})] [string]$ArchiveDestination ) # System-critical folders to exclude $ExcludedFolders = @( 'Public', 'Default', 'Default User', 'All Users', 'Administrator', 'defaultuser0', 'systemprofile', 'LocalService', 'NetworkService' ) $Report = @() $MaxDate = (Get-Date).AddDays(-$ArchiveThresholdDays) Write-Host "WARNING: This action targets ALL subfolders for ARCHIVING (moving)!" -ForegroundColor Red Write-Host "Folders not touched since $MaxDate will be moved to archive." -ForegroundColor Red Write-Host "Starting archive process with a threshold of $ArchiveThresholdDays days..." -ForegroundColor Cyan foreach ($RootPath in $RootPaths) { Write-Host "`n-- Scanning Root Path: $RootPath --" -ForegroundColor Yellow $Profiles = Get-ChildItem -Path $RootPath -Directory -Force -ErrorAction SilentlyContinue foreach ($Profile in $Profiles) { # Skip system-critical folders if ($ExcludedFolders -contains $Profile.Name) { Write-Host " SKIPPING System Folder: $($Profile.Name)" -ForegroundColor Magenta continue } Write-Host " Analyzing Root Folder: $($Profile.Name)" -ForegroundColor Green $SubFolders = Get-ChildItem -Path $Profile.FullName -Directory -Force -ErrorAction SilentlyContinue | Where-Object { $_.FullName -ne $Profile.FullName } foreach ($SubFolder in $SubFolders) { $FullSubPath = $SubFolder.FullName Write-Host " Checking Subfolder: $($SubFolder.Name)" $FolderStatus = Get-FolderIdleTime -TargetPath $FullSubPath if ($FolderStatus.LastActivity -lt $MaxDate) { Write-Host " ACTION REQUIRED: $($FolderStatus.FolderPath) is idle for $($FolderStatus.IdleDays) days. ARCHIVING..." -ForegroundColor Yellow $ArchiveProfilePath = Join-Path -Path $ArchiveDestination -ChildPath $Profile.Name $NewPath = Join-Path -Path $ArchiveProfilePath -ChildPath $SubFolder.Name try { if (-not (Test-Path $ArchiveProfilePath)) { New-Item -Path $ArchiveProfilePath -ItemType Directory -Force | Out-Null } Move-Item -Path $FullSubPath -Destination $NewPath -Force -Verbose $Status = "ARCHIVED" $MoveTarget = $NewPath } catch { Write-Host " ERROR: Failed to move folder to archive: $($_.Exception.Message)" -ForegroundColor Red $Status = "MOVE_FAILED" $MoveTarget = "" } } else { Write-Host " Skipping: $($FolderStatus.FolderPath) is active (last activity: $($FolderStatus.LastActivity))." $Status = "ACTIVE" $MoveTarget = "" } $Report += [PSCustomObject]@{ Folder = $FolderStatus.FolderPath IdleDays = $FolderStatus.IdleDays Threshold = $ArchiveThresholdDays Action = $Status NewPath = $MoveTarget } } } } Write-Host "`nArchive process completed." -ForegroundColor Green return $Report } $TargetScanPaths = @( "C:\Users" ) $ArchiveLocation = "C:\Data\Archive" $Threshold = 3 $ValidPaths = @() foreach ($Path in $TargetScanPaths) { if (Test-Path $Path -PathType Container) { $ValidPaths += $Path } else { Write-Warning "Skipping invalid or inaccessible root path for action: $Path." } } if (-not (Test-Path $ArchiveLocation)) { Write-Host "Creating archive directory: $ArchiveLocation" -ForegroundColor Cyan New-Item -Path $ArchiveLocation -ItemType Directory -Force } if ($ValidPaths.Count -gt 0) { Close-IdleFolders -RootPaths $ValidPaths -ArchiveThresholdDays $Threshold -ArchiveDestination $ArchiveLocation | Format-Table -AutoSize } else { Write-Error "No valid root paths were supplied for action. Script aborted." } |
A summary of the archived idle folders (open idly beyond 3 days) can be accessed from the Show Output button in the Hexnode UEM console. Navigate to Manage > Your device > Action History > Execute Custom Script action status.


