I'm trying to remove a bunch of OSX alternate data streams on an NTFS volume. However no matter what I try I cannot get Powershell to do it. Yes, I admit that my powershell is not great. Is anyone able to help?
Objective: Remove the ADS "AFP_AfpInfo" from any directory in the volume.
Current Code:
Get-ChildItem E:\ -Directory -Recurse | ForEach-Object {
$streams = Get-Content -Path $_ -Stream AFP_AfpInfo -ErrorAction SilentlyContinue
if ($streams) {
$streams | ForEach-Object {
try {
Remove-Item -Path "$($_.PSPath)" -Stream AFP_AfpInfo -Recurse -Force -ErrorAction Silentlycontinue
}
catch {
Write-Host "An error occurred: $($_.Exception.Message)"
}
}
}
}
Current error:
An error occurred: A parameter cannot be found that matches parameter name 'Stream'.
Note: Running Powershell 7.3
-Recurse and -Stream don't seem to go together even though in the documentation they appear in the same Parameter Sets. In this case -Recurse should be removed. GitHub Issue #9822 was submitted to add clarification to the Remove-Item doc.
Also, you're seeking for an exact stream, AFP_AfpInfo, so I don't see a need to enumerate $streams. Lastly, checking if a file or folder has an alternative stream should be done with Get-Item instead of Get-Content for efficiency.
As a final aside, the code must use the .Remove method from EngineIntrinsics to work, Remove-Item -Confirm:$false -Force will always ask for confirmation on folders, arguably a bug. Remove-Item should skip confirmation checks if -Stream is in use and -Confirm:$false -Force. GitHub issue #19154 was submitted to follow up on this.
$removeFunc = $ExecutionContext.InvokeProvider.Item.Remove
$targetStream = 'AFP_AfpInfo'
Get-ChildItem E:\ -Recurse -Directory | ForEach-Object {
if ($stream = $_ | Get-Item -Stream $targetStream -ErrorAction SilentlyContinue) {
try {
$removeFunc.Invoke($stream.PSPath, $false, $true, $true)
}
catch {
Write-Host "An error occurred: $($_.Exception.Message)"
}
}
}
Why are you not just using the Unblock-File cmdlet to remove ADS?
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/unblock-file?view=powershell-7.3
Description This cmdlet only works on the Windows and macOS platforms.
The Unblock-File cmdlet lets you open files that were downloaded from
the internet. It unblocks PowerShell script files that were downloaded
from the internet so you can run them, even when the PowerShell
execution policy is RemoteSigned. By default, these files are blocked
to protect the computer from untrusted files.
Before using the Unblock-File cmdlet, review the file and its source
and verify that it is safe to open.
Internally, the Unblock-File cmdlet removes the Zone.Identifier
alternate data stream, which has a value of 3 to indicate that it was
downloaded from the internet.
Get-Help -Name Unblock-FIle -Examples
NAME
Unblock-File
SYNOPSIS
Unblocks files that were downloaded from the internet.
------------------ Example 1: Unblock a file ------------------
PS C:\> Unblock-File -Path C:\Users\User01\Documents\Downloads\PowerShellTips.chm
-------------- Example 2: Unblock multiple files --------------
PS C:\> dir C:\Downloads\*PowerShell* | Unblock-File
------------- Example 3: Find and unblock scripts -------------
PS C:\> Get-Item * -Stream "Zone.Identifier" -ErrorAction SilentlyContinue
FileName: C:\ps-test\Start-ActivityTracker.ps1
See also Get-Item, Clear-Content and Remove-Item cmdlets use case:
Friday Fun with PowerShell and Alternate Data Streams
https://jdhitsolutions.com/blog/scripting/8888/friday-fun-with-powershell-and-alternate-data-streams
You could also just use the MSSysinternals tool to remove ADS as well in your PS code.
https://learn.microsoft.com/en-us/sysinternals/downloads/streams
Related
I have a script that I feel that I am close to being ready to run but need some help fine tuning things.
My primary objective is this:
From each text file (named after the computer it was generated from), run each script using the data that exists within the .txt file. Each file is output from the C:\Users folder on the computer, listing each user profile that exists on that machine. I need to be able to run the script so that it deletes the specified folders/files for each user profile on that machine.
# Name: CacheCleanup
# Description: Deletes cache files per user on each computer
# Syntax: .\CacheCleanup.ps1
# Author: Nicholas Nedrow
# Created: 06/15/2021
#Text file contains list of all machines that have recently pinged and are online
$Computers = Get-Content "C:\Temp\CacheCleanUp\ComputerUp.txt"
#Users are listed in individual text files assigned with the name of their PC.
$Users = Get-Content "C:\Temp\CacheCleanUp\Computer Users\*.txt"
#Base path for deletion paths
$Path = "\\$PC\c$\users\$user\appdata\local"
#Delete User\Temp files
Remote-Item -Path "$Path\temp\*" -Recurse -Force -EA SilentlyContinue -Verbose
#Delete Teams files
Remove-Item -Path "$Path\Microsoft\Teams" -Recurse -Force -EA SilentlyContinue -Verbose
Remove-ITem -Path "$Path\Microosft\TeamsMeetingAddin" -Recurse -Force -EA SilentlyContinue -Verbose
Remove-Item -Path "$Path\Microsoft\TeamsPresenceAddin" -Recurse -Force -EA SilentlyContinue -Verbose
#Delete Chrome Cache
Remove-Item -Path "$Path\Google\Chrome\User Data\Default\Cache\*" -Recurse -Force -EA SilentlyContinue -Verbose
#Delete IE Cache
Remove-Item -Path "$Path\Microsoft\Windows\INetCache\*" -Recurse -Force -EA SilentlyContinue -Verbose
#Delete Firefox cache
Remove-Item -Path "$Path\Mozilla\Firefox\Profiles\*.default\cache\*" -Recurse -Force -EA SilentlyContinue -Verbose
Remove-Item -Path "$Path\Mozilla\Firefox\Profiles\*.default\cache\*.*" -Recurse -Force -EA SilentlyContinue -Verbose
Remove-Item -Path "$Path\Mozilla\Firefox\Profiles\*.default\cache\cache2\entries\*.*" -Recurse -Force -EA SilentlyContinue -Verbose
Remove-Item -Path "$Path\Mozilla\Firefox\Profiles\*.default\cache\thumbnails\*" -Recurse -Force -EA SilentlyContinue -Verbose
Remove-Item -Path "$Path\Mozilla\Firefox\Profiles\*.default\cache\cookies.sqlite" -Recurse -Force -EA SilentlyContinue -Verbose
Remove-Item -Path "$Path\Mozilla\Firefox\Profiles\*.default\cache\webappstore.sqlite" -Recurse -Force -EA SilentlyContinue -Verbose
Remove-Item -Path "$Path\Mozilla\Firefox\Profiles\*.default\cache\chromeapstore.sqlite" -Recurse -Force -EA SilentlyContinue -Verbose
#How to Run each script for each user on each machine
#How to generate detailed log with results of deletion for each section
I will state right away that I am still learning scripting and am unfamiliar with functions, even though I am pretty sure that is what I need to develop here. This is a domain network so the appropriate path for the computer name has been taken into consideration. Each script does run independently, with the computer name specified but I run into issues when it comes to trying to call out each user profile on that computer.
If possible, it would be nice to have some sort of generated report with the outcome of each user profile and what was ran successfully. I don't need to necessarily know every file that was deleted but maybe a list of those files that were unable to be deleted due to conflicts with running programs or permission issues.
You need to use loops. Consider the following code:
$configFiles = "C:\Temp\CacheCleanUp";
Get-Content "$configFiles\TESTComputers.txt" | % {
$PC = $_;
Write-Host "Attempting to clean cache on computer: $PC";
Get-Content "$configFiles\TESTusers.txt" | % {
$user = $_;
$Path = "\\$PC\c$\users\$user\appdata\local"
Write-Host "`tCleaning $Path"
<# Your code goes here #>
}
}
TESTusers.txt contains:
dave
bob
amy
TESTComputers.txt contains:
10.0.0.1
10.0.0.2
10.0.0.3
10.0.0.4
10.0.0.5
This is the output of the above code and computer/user files:
Attempting to clean cache on computer: 10.0.0.1
Cleaning \\10.0.0.1\c$\users\dave\appdata\local
Cleaning \\10.0.0.1\c$\users\bob\appdata\local
Cleaning \\10.0.0.1\c$\users\amy\appdata\local
Attempting to clean cache on computer: 10.0.0.2
Cleaning \\10.0.0.2\c$\users\dave\appdata\local
Cleaning \\10.0.0.2\c$\users\bob\appdata\local
Cleaning \\10.0.0.2\c$\users\amy\appdata\local
Attempting to clean cache on computer: 10.0.0.3
Cleaning \\10.0.0.3\c$\users\dave\appdata\local
Cleaning \\10.0.0.3\c$\users\bob\appdata\local
Cleaning \\10.0.0.3\c$\users\amy\appdata\local
Attempting to clean cache on computer: 10.0.0.4
Cleaning \\10.0.0.4\c$\users\dave\appdata\local
Cleaning \\10.0.0.4\c$\users\bob\appdata\local
Cleaning \\10.0.0.4\c$\users\amy\appdata\local
Attempting to clean cache on computer: 10.0.0.5
Cleaning \\10.0.0.5\c$\users\dave\appdata\local
Cleaning \\10.0.0.5\c$\users\bob\appdata\local
Cleaning \\10.0.0.5\c$\users\amy\appdata\local
Few things to note about the code:
Get-Content "filename" | % - this is going to loop through the contents of the file one line at a time. % is a shortcut for ForEach-Object.
$_ when inside a foreach loop is an automatic variable created by PowerShell that contains the current item in the loop.
If you have a loop inside a loop and you need to access both $_ values from the inner and outer loop, you can create a new variable (eg $PC = $_;) in the outer loop that can be used within the inner loop (eg $Path = "\\$PC\c$\users\$user\appdata\local").
You should definitely learn to use functions, and then in the future you can combine functions into modules. This is a big help in organising your code, and you can avoid duplication by sharing functions between different scripts - but your current script doesnt need functions (but theyre a good idea).
Depending on your network, you might be able to use PowerShell remoting instead of the Administrative shares to achieve the same effect. This is a more advanced topic, there is some configuration required on the machines you want to connect but the advantage is your computer sends the script to each target, and the target computer runs the script and reports its results.
Another possible change i would suggest is only using a list of computers - then on each computer use get-childitem -path c:\users to actually get the list of each profile currently on that target computer.
In a script, I use Get-ChildItem -File -Recurse in order to check my permissions on subfolders. Then, I try to read the first character of every files. My goal is to catch Permission Denied type errors using $Error. It works well locally. But when I execute the script on a remote server with a UNC long path, the errors aren't generated. If I run manually the Get-ChildItem command just after the execution of the script, which is supposed to generate some errors, it display the files but does not generate errors. If I wait a few minutes and I run it again, I finally get the errors displayed.
Is there a way to wait for the errors to be generated?
Here is the specific part of my code which doesn't generate any error over the network:
# Check if the current item is a folder or a file
If($elem.Attributes -eq 'Directory')
{
# Get all child items of File type
$subElem = Get-ChildItem -LiteralPath $elem.FullName -File -Recurse -ErrorAction SilentlyContinue
# Parse subfolders and files to check permissions integrity. To generate an Permission Denied error, a file must be open
ForEach($subItem in $subElem)
{
# Read the first character of the current sub-item
Get-Content -LiteralPath $subItem.FullName -Encoding byte -TotalCount 1 -ErrorAction SilentlyContinue | Out-Null
}
}
Else
{
# Read the first character of the current element
Get-Content -LiteralPath $elem.FullName -Encoding byte -TotalCount 1 -ErrorAction SilentlyContinue | Out-Null
}
I finally found the solution myself.
In this script, I use the module NTFSSecurity (https://github.com/raandree/NTFSSecurity) in order to manage ACLs and inheritance. Over the network, it seems to be a bit slow.
Before the bit of code I shared above, I have a few lines which check and updates a bunch of ACLs over the network. As it takes some time, errors are only generated some time after. In this case, if the command encounter an error, it just continues but doesn't display or catch the error at the same time.
I used errors to detect items on which I had to recover the access. Now, I use another cmdlet coming with the NTFSSecurity module, Get-NTFSEffectiveAccess.
I wrote a little function which does perfectly the trick:
Function Check-MyAccess([String]$Path)
{
# Get effective permissions
$effectiveAccess = Get-NTFSEffectiveAccess -Path $Path -ErrorAction SilentlyContinue
# Check to be, at least, able to read the item
If(($effectiveAccess -eq $Null) -or ($effectiveAccess.AccessRights -Match 'Synchronize') -or ((($effectiveAccess.AccessRights -Like '*Read*') -or ($effectiveAccess.AccessRights -Like '*Modify*') -and ($effectiveAccess.AccessControlType -Match 'Deny'))))
{
Return $False
}
Else
{
Return $True
}
}
I have two separate PowerShell (.ps1) files that I'd like to run one after the other when a user logs on to a PC. They're fairly straightforward tasks. The first copies a shortcut from a network location to all users AppData folder.
Copy-Item -Path "\\Server\Share\*.lnk" -Destination "$env:APPDATA\Microsoft\Windows\Start Menu\Programs"
The second .ps1 file removes a load of bloatware from Windows 10, I won't put all the code in here as it's quite repetitive but it basically lists a load of apps and finally removes them.
$AppList = #(
"*Microsoft.3dbuilder*"
"*AdobeSystemsIncorporated.AdobePhotoshopExpress*"
"*Microsoft.WindowsAlarms*"
"*Microsoft.Asphalt8Airborne*"
)
foreach ($App in $AppList) {
Get-AppxPackage -Name $App | Remove-AppxPackage -ErrorAction SilentlyContinue
}
If I place the two files into the same logon policy, the first script will run but the second one doesn't until the user logs off and back on again (I'd like them both to run at the same time).
I've tried placing them both in the same file and separating then with a ;, this didn't work so tried and, again no joy. I've also tried creating a master file (with the two .ps1 files in the same location) and running the following, again this didn't work.
&"$PSScriptroot\Copy Devices and Printers Shortcut.ps1" &"$PSScriptroot\BloatwareRemoval.ps1"
I've also tried separating the above with ; and and with no joy.
Edit I've resolved this with the following pd1 file:
Get-ChildItem \\File\Location | ForEach-Object {
& $_.FullName
}
As per the comments, this script you should save as a .ps1 file and call it however you want. It will do both the operations together. I have added error handling but ideally you should parse them in a file so that you can refer in case of any failure.
If( (Test-Path -Path "\\Server\Share\*.lnk") -and (Test-Path -Path "$env:APPDATA\Microsoft\Windows\Start Menu\Programs"))
{
Copy-Item -Path "\\Server\Share\*.lnk" -Destination "$env:APPDATA\Microsoft\Windows\Start Menu\Programs"
}
else
{
"Invalid path. Kindly validate. "
}
#("*Microsoft.3dbuilder*","*AdobeSystemsIncorporated.AdobePhotoshopExpress*","*Microsoft.WindowsAlarms*","*Microsoft.Asphalt8Airborne*")|% {
try{
Get-AppxPackage -Name $_ | Remove-AppxPackage -ErrorAction stop
}
catch
{
$_.Exception.Message
}
}
Hope it helps.
I've resolved this with the following pd1 file:
Get-ChildItem \\File\Location | ForEach-Object {
& $_.FullName
}
I currently have this script working but only able to get it to run locally, I would like to have it read a text file that would be stored on c:\List_of_PCs.txt that would have computer names that it would also run the same script on. That way I can update the text file instead of modify the code.
Set-ExecutionPolicy RemoteSigned
# Get all users
$users = Get-ChildItem -Path "C:\Users"
# Loop through users and delete the Teams file
$users | ForEach-Object {
Remove-Item -Path "C:\Users\$($_.Name)\AppData\Roaming\Microsoft\Teams\Cache\f*" -Force
Remove-Item -Path "C:\Users\$($_.Name)\AppData\Roaming\Microsoft\Teams\Application Cache\Cache\f*" -Force
}
Any help on this I've tried multiple things every which way, I'm sure this is something simple but I'm still very new to PowerShell.
Try something like this...
Requires PowerShell remoting to be enabled and using an account that is an admin on the remote computer
$ComputerList = Import-Csv -Path 'c:\List_of_PCs.txt'
$ComputerList | % {
Invoke-Command -ComputerName $_ -ScriptBlock {
# Set-ExecutionPolicy RemoteSigned # this is something that should be set via GPO for all systems, not your script, so that it is centrally controlled and monitored.
# Get all users
$users = Get-ChildItem -Path "C:\Users"
# Loop through users and delete the Teams file
$users | ForEach-Object {
Remove-Item -Path "C:\Users\$($_.Name)\AppData\Roaming\Microsoft\Teams\Cache\f*" -Force
Remove-Item -Path "C:\Users\$($_.Name)\AppData\Roaming\Microsoft\Teams\Application Cache\Cache\f*" -Force
}
}
}
I am working on a DPM powershell script to get the list of files/folders available for backup in a particular directory. More precisely, i need to get the list of folders under the directory D:\inetpub\vhosts\ (i.e all vhosts). I have been trying to write a script using DPM powershell cmdlets and this is what i have come up with.
$searchpath = 'D:\inetpub\vhosts'
$so=New-SearchOption -FromRecoveryPoint $today -ToRecoveryPoint $tomorrow -SearchDetail filesfolders -SearchType contains -Location $searchpath -SearchString "*" -ErrorAction SilentlyContinue
$ri=Get-RecoverableItem -Datasource $datasource -SearchOption $so -ErrorAction SilentlyContinue
foreach($file in $ri)
{
echo $file.userFriendlyName
}
But i was not able to get all the directories. After some research i found out that New-SearchOption can at maximum return 250 searches. In my use the number of folders is minimum 1500. Is there any way of getting all the files. Any help would really be appreciated.
Have you tried using the pipeline. There might be paging features built in the cmdlet only available using the pipeline. Try the code below:
$searchpath = 'D:\inetpub\vhosts'
New-SearchOption -FromRecoveryPoint $today -ToRecoveryPoint $tomorrow -SearchDetail filesfolders -SearchType contains -Location $searchpath -SearchString "*" -ErrorAction SilentlyContinue |
Get-RecoverableItem -Datasource $datasource -ErrorAction SilentlyContinue | For-EachObject {
$_.userFriendlyName
}