In the modern IT landscape, managing and maintaining file systems across numerous servers and workstations can be challenging. IT professionals, especially those working in Managed Service Providers (MSPs), often need to monitor specific files and directories to ensure compliance, security, and operational efficiency.
A robust way to accomplish this task is through PowerShell scripting, which provides automation and detailed control over Windows environments. The provided PowerShell script is designed to alert users if a specified file or folder is found within a given directory or subdirectory, making it a valuable tool for IT professionals.
Background
Monitoring file existence and location is crucial for several reasons. It helps in maintaining security by ensuring that no unauthorized files are present, supports compliance by verifying that necessary files are in place, and aids in operational efficiency by automating routine checks. This script is particularly useful for IT professionals who need to manage large networks or multiple client environments. By automating the search and alert process, it saves time and reduces the risk of human error.
The Script
#Requires -Version 5.1 <# .SYNOPSIS Alert if a specified file or folder is found in a directory or subdirectory you specify. .DESCRIPTION Alert if a specified file or folder is found in a directory or subdirectory you specify. .EXAMPLE -SearchPath "C:" -FileOrFolder "autounattend" WARNING: Backslash missing from the search path. Changing it to C:\. [Alert] File Found. C:\Users\Administrator\Desktop\ExampleFolder\Test Folder 1\autounattend.xml C:\Users\Administrator\Desktop\ExampleFolder\Test Folder 2\autounattend.xml C:\Users\Administrator\Desktop\ExampleFolder\TestFolder1\Test Folder 1\autounattend.xml C:\Users\Administrator\Desktop\ExampleFolder\TestFolder1\Test Folder 2\autounattend.xml C:\Users\Administrator\Desktop\ExampleFolder\TestFolder2\Test Folder 1\TestFolder1\autounattend.xml Attempting to set Custom Field 'multiline'. Successfully set Custom Field 'multiline'! PARAMETER: -SeachPath "C:\ReplaceMeWithAvalidPath" Enter the starting directories for the search, separated by commas. This will include all subdirectories as well. PARAMETER: -FileOrFolder "ReplaceMeWithNameToSearchFor" Specify the full or partial name of a file or folder to find. E.g., 'config' or '.exe'. PARAMETER: -SearchType "Files and Folders" Limit the search to either files, folders, or both. PARAMETER: -Timeout "30" Maximum search time in minutes, halts search if exceeded. PARAMETER: -CustomField "ReplaceMeWithNameOfMultilineCustomField" Optional multiline field to record search results. Leave blank if unused. .NOTES Minimum OS Architecture Supported: Windows 10, Windows Server 2016 Release Notes: Initial Release By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use. Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA). #> [CmdletBinding()] param ( [Parameter()] [String]$SearchPath = "C:\Windows,C:\Program Files", [Parameter()] [String]$FileOrFolder, [Parameter()] [String]$SearchType = "Files and Folders", [Parameter()] [Int]$Timeout = 30, [Parameter()] [String]$CustomField ) begin { # Set parameters using dynamic script variables. if ($env:searchPath -and $env:searchPath -notlike "null") { $SearchPath = $env:searchPath } if ($env:fileNameOrFolderNameToSearchFor -and $env:fileNameOrFolderNameToSearchFor -notlike "null") { $FileOrFolder = $env:fileNameOrFolderNameToSearchFor } if ($env:searchType -and $env:searchType -notlike "null") { $SearchType = $env:searchType } if ($env:timeoutInMinutes -and $env:timeoutInMinutes -notlike "null") { $Timeout = $env:timeoutInMinutes } if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } # Error out if no search path was given. if (-not $SearchPath) { Write-Host "[Error] No search path given!" exit 1 } # If given a comma-separated list, split the paths. $PathsToSearch = New-Object System.Collections.Generic.List[String] if ($SearchPath -match ",") { $SearchPath -split "," | ForEach-Object { $PathsToSearch.Add($_.Trim()) } } else { $PathsToSearch.Add($SearchPath) } # Initialize a generic list for paths to remove or replace. $ReplacementPaths = New-Object System.Collections.Generic.List[Object] $PathsToRemove = New-Object System.Collections.Generic.List[String] # If given a drive without the backslash add it in. $PathsToSearch | ForEach-Object { if ($_ -notmatch '^[A-Z]:\\$' -and $_ -match '^[A-Z]:$') { $NewPath = "$_\" $ReplacementPaths.Add( [PSCustomObject]@{ Index = $PathsToSearch.IndexOf("$_") NewPath = $NewPath } ) Write-Warning "Backslash missing from the search path. Changing it to $NewPath." } } # Apply replacements. $ReplacementPaths | ForEach-Object { $PathsToSearch[$_.index] = $_.NewPath } # Check if the search path is valid. $PathsToSearch | ForEach-Object { if (-not (Test-Path $_)) { Write-Host -Object "[Error] $_ does not exist!" $PathsToRemove.Add($_) $ExitCode = 1 } } # Remove Paths that do not exist. $PathsToRemove | ForEach-Object { $PathsToSearch.Remove($_) | Out-Null } # Error out if no valid paths to search. if ($($PathsToSearch).Count -eq 0) { Write-Host "[Error] No valid paths to search!" exit 1 } # If we're not given a file or folder error out. if (-not $FileOrFolder) { Write-Host -Object "[Error] No file or folder given to search for!" exit 1 } # Timeout must be within a given range in minutes. if ($Timeout -lt 1 -or $Timeout -gt 120) { Write-Host -Object "[Error] Timeout is greater than 120 minutes or less than 1 minute." exit 1 } # Scope the search to either files only or folders only. $ValidSearchTypes = "Files and Folders", "Files Only", "Folders Only" if ($ValidSearchTypes -notcontains $SearchType) { Write-Host -Object "[Error] Invalid search type." exit 1 } # Test for local administrator rights. function Test-IsElevated { $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object System.Security.Principal.WindowsPrincipal($id) $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } # Handy function to set a custom field. function Set-NinjaProperty { [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [String]$Name, [Parameter()] [String]$Type, [Parameter(Mandatory = $True, ValueFromPipeline = $True)] $Value, [Parameter()] [String]$DocumentName ) $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters if ($Characters -ge 10000) { throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") } # If we're requested to set the field value for a Ninja document we'll specify it here. $DocumentationParams = @{} if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # The field below requires additional information to be set $NeedsOptions = "Dropdown" if ($DocumentName) { if ($NeedsOptions -contains $Type) { # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 } } else { if ($NeedsOptions -contains $Type) { $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 } } # If an error is received it will have an exception property, the function will exit with that error information. if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. switch ($Type) { "Checkbox" { # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. $NinjaValue = [System.Convert]::ToBoolean($Value) } "Date or Date Time" { # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. $Date = (Get-Date $Value).ToUniversalTime() $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date $NinjaValue = $TimeSpan.TotalSeconds } "Dropdown" { # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID if (-not $Selection) { throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") } $NinjaValue = $Selection } default { # All the other types shouldn't require additional work on the input. $NinjaValue = $Value } } # We'll need to set the field differently depending on if its a field in a Ninja Document or not. if ($DocumentName) { $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 } else { $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 } if ($CustomField.Exception) { throw $CustomField } } $ExitCode = 0 } process { # Error out if local administrator rights are not present. if (-not (Test-IsElevated)) { Write-Host "[Error] Access Denied. Please run with Administrator privileges." exit 1 } # Initialize generic lists. $SearchJobs = New-Object System.Collections.Generic.List[object] $CustomFieldValue = New-Object System.Collections.Generic.List[string] # For each given path to search, create a PowerShell job with the provided parameters. $PathsToSearch | ForEach-Object { $SearchJobs.Add( ( Start-Job -ScriptBlock { param($SearchPath, $FileOrFolder, $SearchType) # We're going to wildcard search either files or folders depending on the parameters given. switch ($SearchType) { "Files and Folders" { Get-ChildItem -Path $SearchPath -Filter "*$FileOrFolder*" -Recurse | Select-Object -Property FullName, Attributes | ConvertTo-Csv } "Folders Only" { Get-ChildItem -Path $SearchPath -Filter "*$FileOrFolder*" -Recurse -Directory | Select-Object -Property FullName, Attributes | ConvertTo-Csv } "Files Only" { Get-ChildItem -Path $SearchPath -Filter "*$FileOrFolder*" -Recurse -File | Select-Object FullName, Attributes | ConvertTo-Csv } } } -ArgumentList $_, $FileOrFolder, $SearchType ) ) } # Convert timeout to seconds as Wait-Job requires seconds. $TimeoutInSeconds = $Timeout * 60 $StartTime = Get-Date # Wait for all jobs to complete or timeout. foreach ($SearchJob in $SearchJobs) { # Calculate the remaining time. $TimeElapsed = (Get-Date) - $StartTime $RemainingTime = $TimeoutInSeconds - $TimeElapsed.TotalSeconds # If there is no remaining time, break the loop. if ($RemainingTime -le 0) { break } # Wait for the current job with the remaining time as the timeout. $SearchJob | Wait-Job -Timeout $RemainingTime | Out-Null } # Output a warning if the job fails to complete. $IncompleteJobs = $SearchJobs | Get-Job | Where-Object { $_.State -eq "Running" } if ($IncompleteJobs) { Write-Host "[Error] The timeout period of $Timeout minutes has been reached, but not all files or directories have been searched!" $CustomFieldValue.Add("[Error] The timeout period of $Timeout minutes has been reached, but not all files or directories have been searched!") $ExitCode = 1 } # Our PowerShell Job outputs in CSV format; we'll convert it here. $MatchingItems = $SearchJobs | ForEach-Object { $_ | Get-Job | Receive-Job -ErrorAction SilentlyContinue -ErrorVariable JobErrors | ConvertFrom-Csv } # Identify whether or not we have a match for a file or folder here. $FileMatch = $MatchingItems | Where-Object { $_.Attributes -ne "Directory" } $FolderMatch = $MatchingItems | Where-Object { $_.Attributes -eq "Directory" } # If we have a match for a file we'll output that here. if ($FileMatch) { Write-Host -Object "[Alert] File Found." $CustomFieldValue.Add("[Alert] File Found.") } # If we have a match for a folder we'll output that here. if ($FolderMatch) { Write-Host -Object "[Alert] Folder Found." $CustomFieldValue.Add("[Alert] Folder Found.") } # If we have no matches we'll output that here. if (-not $FileMatch -and -not $FolderMatch) { Write-Host -Object "Unable to find $FileOrFolder." $CustomFieldValue.Add("Unable to find $FileOrFolder.") } # For each matching file we'll output their full path. $MatchingItems | ForEach-Object { Write-Host "$($_.FullName)" $CustomFieldValue.Add("$($_.FullName)") } # Output any failures or errors received. $FailedJobs = $SearchJobs | Get-Job | Where-Object { $_.State -ne "Completed" -and $_.State -ne "Running" } if ($FailedJobs -or $JobErrors) { Write-Host "" Write-Host "[Error] Failed to search certain files or directories due to an error." $CustomFieldValue.Add("") $CustomFieldValue.Add("[Error] Failed to search certain files or directories due to an error.") if ($JobErrors) { Write-Host "" $CustomFieldValue.Add("") $JobErrors | ForEach-Object { Write-Host "[Error] $($_.Exception.Message)" $CustomFieldValue.Add("[Error] $($_.Exception.Message)") } } $ExitCode = 1 } $SearchJobs | Get-Job | Remove-Job -Force # Attempt to set the custom field using the Set-NinjaProperty function, if provided. if ($CustomField) { try { Write-Host "Attempting to set Custom Field '$CustomField'." Set-NinjaProperty -Name $CustomField -Value ($CustomFieldValue | Out-String) Write-Host "Successfully set Custom Field '$CustomField'!" } catch { if (-not $_.Exception.Message) { Write-Host "[Error] $($_.Message)" } else { Write-Host "[Error] $($_.Exception.Message)" } $ExitCode = 1 } } exit $ExitCode } end { }
Access over 300+ scripts in the NinjaOne Dojo
Detailed Breakdown
The script operates in several stages, each contributing to its overall functionality. Here’s a step-by-step breakdown:
1. Parameter Initialization: The script begins by defining several parameters:
- SearchPath: The directories to be searched.
- FileOrFolder: The name of the file or folder to search for.
- SearchType: Whether to search for files, folders, or both.
- Timeout: The maximum duration for the search.
- CustomField: An optional field to record search results.
2. Environment Variable Overrides: The script checks if any environment variables are set that should override the input parameters. This allows for dynamic adjustments based on different environments.
3. Validation: The script performs several validation checks:
- Ensuring a search path is provided.
- Validating and formatting the search path.
- Checking the existence of the specified paths.
- Validating the file or folder name.
- Ensuring the timeout value is within an acceptable range.
- Confirming the search type is valid.
4. Administrator Rights Check: The script includes a function to verify if it is being run with administrator privileges, which is necessary for certain file operations.
5. Search Execution: The core functionality involves creating PowerShell jobs for each path to be searched. These jobs perform recursive searches based on the specified parameters and return the results in CSV format.
6. Results Handling: The script collects and processes the search results:
- It identifies matches for files and folders.
- Outputs the full paths of any matching items.
- Records errors or incomplete searches.
7. Custom Field Setting: If a custom field is specified, the script attempts to set it with the search results, leveraging a helper function to handle different field types.
8. Error Handling and Cleanup: The script ensures any errors are logged and all jobs are properly cleaned up before exiting with an appropriate exit code.
Potential Use Cases
Consider a scenario where an IT professional is managing a network of workstations for a corporate client. They need to ensure that no unauthorized executable files are present in user directories, as part of a security audit. By deploying this script, they can automate the search across all workstations, quickly identifying any instances of unauthorized files and taking action as needed. This automation not only enhances security but also frees up valuable time for the IT team to focus on more strategic tasks.
Comparisons
This script provides a comprehensive and automated approach to file and folder searches compared to manual methods or basic batch scripts. Traditional methods often involve manually navigating directories or using simple scripts that lack advanced features like timeout handling, custom field recording, and multi-threaded job execution. The PowerShell script’s ability to handle complex environments and provide detailed output makes it a superior choice for IT professionals.
FAQs
1) How do I specify multiple directories for the search?
Use a comma-separated list for the SearchPath parameter, e.g., “C:\Path1,C:\Path2”.
2) Can I limit the search to files only or folders only?
Yes, use the SearchType parameter with values “Files Only” or “Folders Only”.
3) What happens if the search exceeds the specified timeout?
The script will terminate any incomplete searches and log an error message.
4) Do I need administrator privileges to run this script?
Yes, the script checks for administrator rights and will exit if not run with sufficient privileges.
Implications
The results of this script have significant implications for IT security. By identifying unauthorized or misplaced files and folders, IT professionals can take immediate action to mitigate potential security risks. Regularly running this script as part of a maintenance routine can help ensure compliance with organizational policies and regulatory requirements, thereby enhancing overall security posture.
Recommendations
When using this script, consider the following best practices:
- Regularly update the script to accommodate changes in your environment.
- Integrate the script into automated maintenance routines.
- Review and act on the search results promptly to maintain security.
- Customize the script parameters to fit specific needs and environments.
Final Thoughts
This PowerShell script is a powerful tool for IT professionals, providing automated and efficient file and folder search capabilities. By leveraging this script, IT teams can enhance their operational efficiency, maintain security, and ensure compliance with organizational policies. For those using NinjaOne, integrating this script can further streamline management tasks, allowing for centralized control and monitoring across multiple endpoints.