How can a directory's encryption attribute be toggled from a Powershell script? - windows

The documentation states that the encryption attribute in a directory is just a flag that indicates that all its children should be encrypted.
For a file, you can toggle encryption with
(Get-Item -Path filename).Decrypt()
(Get-Item -Path filename).Encrypt()
These methods are defined in FileInfo and don't exist in DirectoryInfo. In neither case can you set the attribute directly, i.e. this does nothing:
(Get-Item -Path filename).Attributes -= 'Encrypted'
(This type of attribute setting will work with things like Archive and ReadOnly but not things like Compressed, Encrypted, Directory, etc.)
What I would like to do is:
Create a new directory inside a directory that is encrypted.
Set the encrypted attribute to false in this new directory.
Fill the new directory with files which will thus not be encrypted.
Is this possible from a script?
Note: I do not want to fill the directory first and then go and call Decrypt() on every file; this does not solve the problem of having all new files not be encrypted.

It isn't obvious (and you have to wonder why System.IO.DirectoryInfo instances don't expose .Encrypt() and .Decrypt() methods, as you have to wonder why attempts to remove the Encrypted attribute via .Attributes are quietly ignored), but the System.IO.File class has static .Encrypt() and .Decrypt() methods that also operate on directory paths.
Therefore:
# Create a new dir. inside an encrypted dir., which by default
# will have the Encrypted attribute set too.
$dirInfo = New-Item -Type Directory -Path $encryptedDir -Name NewUnencryptedDir
# Remove the Encrypted attribute, so that files and subdirs. created inside
# will be unencrypted.
# Note: Be sure to always pass a *full* path, because .NET's current dir.
# usually differs from PowerShell's.
[IO.File]::Decrypt($dirInfo)

Related

How can I reset the permissions of files via powershell that originated from a user's personal directory?

I've run into an issue where users scan PDFs via the copy machine into their personal user folder on a file server. (The copier has a domain user account that owns a subfolder within each users' personal folder (D:\FileShare\Users\JohnSmith\Scans) that it drops the PDFs into.) The PDFs gain the permissions of that folder. (Only the user, scanner and admins have access) The scanner retains ownership of the PDF. The user moves that PDF to another location on the server and no one but the original user can see the file exists because they don't have permission.
We have a workaround for new files. Rather than move or cut/paste a file within the server, the user can copy/paste it or move it first to their local computer before putting it back on the server. This effectively creates a new file that inherits permissions from the folder it's created in. Unfortunately, this doesn't help for those files that were already moved.
Hard mode:
I'm looking for a way to iterate through the server's directories (D:\FileShare) and have each file that is still owned by the scanner but isn't in a user directory (D:\FileShare\Users\JohnSmith) to have it's permissions replaced by those of the parent directory.
Psudo-code:
For each file
If owner == Scanners, replace current permissions with that of parent directory
next
Easy mode:
Iterate through all folders and set all files' permissions to that of their parent folder. Make no changes to folders themselves.
Psudo-code:
For each file, replace permissions with that of parent directory.
I'm not a powershell guy so the syntax scares me a bit.
If i'm interpreting correctly the below code should work.
This essentially:
Creates an array of PDF filepaths where the owner of the PDF is $scannerOwner
Loops through each PDF filepath, if it is in a user directory do nothing else apply the ACL from the parent folder to the file.
$ScannerOwner = "DOMAIN\UserA"
$ScanDir = "D:\Fileshare"
$userDir = "$ScanDir\Users"
#Get all PDF files in $scanDir that have the owner set to $scannerOwner
$Files = GCI $ScanDir -Filter *.pdf -Recurse | where-object {(get-acl $_.fullname).owner -eq $ScannerOwner}
foreach($file in $files){
if($file.FullName -like "$userDir*"){
##File exists in a user directory, nothing to do here
} else {
#File doesn't exist in user directory, need to apply parent directories permissions to the file
$parentDir = split-path -parent $file.FullName
$ParentDirSDDL = (get-acl $parentDir).sddl
$newPerms = get-acl -path $file.FullName
$newPerms.SetSecurityDescriptorSddlForm($parentDirSDDL)
set-acl -path $file.Fullname -AclObject $newPerms
}
}

Adding things to the user PATH | Powershell SETX Error [duplicate]

I followed this procedure in order to permanently add a path to SumatraPDF using powershell. The last few commands from the link are meant to check that the path has indeed been added.
When I access the path using the following command,
(get-itemproperty -path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).Path.split(';')
the result includes the path to SumatraPDF
C:\Windows\system32
C:\Windows
C:\Windows\System32\Wbem
C:\Windows\System32\WindowsPowerShell\v1.0\
C:\Windows\System32\OpenSSH\
C:\ProgramData\chocolatey\bin
C:\texlive\2021\bin\win32
C:\Users\921479\AppData\Local\SumatraPDF
However when I access it using the following command,
($env:path).split(';')
the result does not contain the path to SumatraPDF:
C:\Windows\system32
C:\Windows
C:\Windows\System32\Wbem
C:\Windows\System32\WindowsPowerShell\v1.0\
C:\Windows\System32\OpenSSH\
C:\ProgramData\chocolatey\bin
C:\texlive\2021\bin\win32
C:\Users\921479\AppData\Local\Microsoft\WindowsApps
Finally, actually passing sumatrapdf does not works, which indicates to me that the real path is the one accessed using the get-itemproperty command.
Why does the path set in the registry not correspond to the one set in $env:path? Is there a mistake in the procedure shown in the link I followed? How can I correct it?
I should mention I have already tried restarting the shell but it doesn't help.
Note:
See the middle section for helper function Add-Path
See the bottom section for why use of setx.exe should be avoided for updating the Path environment variable.
The procedure in the linked blog post is effective in principle, but is missing a crucial piece of information / additional step:
If you modify environment variables directly via the registry - which, unfortunately, is the right way to do it for REG_EXPAND_SZ-based environment variables such as Path - you need to broadcast a WM_SETTINGCHANGE message so that the Windows (GUI) shell (and its components, File Explorer, the taskbar, the desktop, the Start Menu, all provided via explorer.exe processes) is notified of the environment change and reloads its environment variables from the registry. Applications launched afterwards then inherit the updated environment.
If this message is not sent, future PowerShell sessions (and other applications) won't see the modification until the next logon / reboot.
Unfortunately, there's no direct way to do this from PowerShell, but there are workarounds:
Brute-force workaround - simple, but visually disruptive and closes all open File Explorer windows:
# Kills all explorer.exe processes, which restarts the Windows shell
# components, forcing a reload of the environment from the registry.
Stop-Process -Name explorer
Workaround via .NET APIs:
# Create a random name assumed to be unique
$dummyName = [guid]::NewGuid().ToString()
# Set an environment variable by that name, which makes .NET
# send a WM_SETTINGCHANGE broadcast
[Environment]::SetEnvironmentVariable($dummyName, 'foo', 'User')
# Now that the dummy variable has served its purpose, remove it again.
# (This will trigger another broadcast, but its performance impact is negligible.)
[Environment]::SetEnvironmentVariable($dummyName, [NullString]::value, 'User')
Workaround by calling the Windows API via an ad hoc-compiled P/Invoke call to SendMessageTimeout() in C#, via Add-Type:
While this is a proper solution, it invariably incurs a noticeable performance penalty due to the ad hoc-compilation the first time it is run in a session.
For details, see this blog post.
The approach in the blog post has another problematic aspect:
It retrieves the expanded environment-variable value from the registry, because that is what Get-ItemProperty and Get-ItemPropertyValue invariably do. That is, if directories in the value are defined in terms of other environment variables (e.g., %SystemRoot% or %JAVADIR%), the returned value no longer contains these variables, but their current values. See the bottom section for why this can be problematic.
The helper function discussed in the next section addresses all issues, while also ensuring that the modification takes effect for the current session too.
The following Add-Path helper function:
Adds (appends) a given, single directory path to the persistent user-level Path environment variable by default; use -Scope Machine to target the machine-level definition, which requires elevation (run as admin).
If the directory is already present in the target variable, no action is taken.
The relevant registry value is updated, which preserves its REG_EXPAND_SZ data type, based on the existing unexpanded value - that is, references to other environment variables are preserved as such (e.g., %SystemRoot%), and may also be used in the new entry being added.
Triggers a WM_SETTINGCHANGE message broadcast to inform the Windows shell of the change.
Also updates the current session's $env:Path variable value.
Note: By definition (due to use of the registry), this function is Windows-only.
With the function below defined, your desired Path addition could be performed as follows, modifying the current user's persistent Path definition:
Add-Path C:\Users\921479\AppData\Local\SumatraPDF
If you really want to update the machine-level definition (in the HKEY_LOCAL_MACHINE registry hive, which doesn't make sense with a user-specific path), add -Scope Machine, but not that you must then run with elevation (as admin).
Add-Path source code:
function Add-Path {
param(
[Parameter(Mandatory, Position=0)]
[string] $LiteralPath,
[ValidateSet('User', 'CurrentUser', 'Machine', 'LocalMachine')]
[string] $Scope
)
Set-StrictMode -Version 1; $ErrorActionPreference = 'Stop'
$isMachineLevel = $Scope -in 'Machine', 'LocalMachine'
if ($isMachineLevel -and -not $($ErrorActionPreference = 'Continue'; net session 2>$null)) { throw "You must run AS ADMIN to update the machine-level Path environment variable." }
$regPath = 'registry::' + ('HKEY_CURRENT_USER\Environment', 'HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment')[$isMachineLevel]
# Note the use of the .GetValue() method to ensure that the *unexpanded* value is returned.
$currDirs = (Get-Item -LiteralPath $regPath).GetValue('Path', '', 'DoNotExpandEnvironmentNames') -split ';' -ne ''
if ($LiteralPath -in $currDirs) {
Write-Verbose "Already present in the persistent $(('user', 'machine')[$isMachineLevel])-level Path: $LiteralPath"
return
}
$newValue = ($currDirs + $LiteralPath) -join ';'
# Update the registry.
Set-ItemProperty -Type ExpandString -LiteralPath $regPath Path $newValue
# Broadcast WM_SETTINGCHANGE to get the Windows shell to reload the
# updated environment, via a dummy [Environment]::SetEnvironmentVariable() operation.
$dummyName = [guid]::NewGuid().ToString()
[Environment]::SetEnvironmentVariable($dummyName, 'foo', 'User')
[Environment]::SetEnvironmentVariable($dummyName, [NullString]::value, 'User')
# Finally, also update the current session's `$env:Path` definition.
# Note: For simplicity, we always append to the in-process *composite* value,
# even though for a -Scope Machine update this isn't strictly the same.
$env:Path = ($env:Path -replace ';$') + ';' + $LiteralPath
Write-Verbose "`"$LiteralPath`" successfully appended to the persistent $(('user', 'machine')[$isMachineLevel])-level Path and also the current-process value."
}
The limitations of setx.exe and why it shouldn't be used to update the Path environment variable:
setx.exe has fundamental limitations that make it problematic, particularly for updating environment variables that are based on REG_EXPAND_SZ-typed registry values, such as Path:
Values are limited to 1024 characters, with additional ones getting truncated, albeit with a warning (as of at least Windows 10).
The environment variable that is (re)created is invariably of type REG_SZ, whereas Path is originally of type REG_EXPAND_SZ and contains directory paths based on other environment variables, such as %SystemRoot% and %JAVADIR%.
If the replacement value contains only literal paths (no environment-variable references) that may have no immediate ill effects, but, for an instance, an entry that originally depended on %JAVADIR% will stop working if the value of %JAVADIR% is later changed.
Additionally, if you base the updated value on the current session's $env:Path value, you'll end up duplicating entries, because the process-level $env:Path value is a composite of the machine-level and current-user-level values.
This increases the risk of running into the 1024-character limit, especially if the technique is used repeatedly. It also bears the risk of duplicate values lingering after the original entry is removed from the original scope.
While you can avoid this particular problem by retrieving the scope-specific value either directly from the registry or - invariably in expanded form - via [Environment]::GetEnvironmentVariable('Path', 'User') or [Environment]::GetEnvironmentVariable('Path', 'Machine'), that still doesn't solve the REG_EXPAND_SZ problem discussed above.
Use setx to permanently update an environment variable. Don't hack the registry.
After you invoke setx, just update the Path environment manually in the current session. Powershell: Reload the path in PowerShell

Archive all files of a certain type recursively from powershell

Is there a way to use Compress-Archive script, that when run from a path:
archives all files matching a wildcard filter (*.doc, for example)
archives such files in the current folder and all children folders
save the relative folder structure (the option to use relative or absolute would be good, though)
I am having trouble have it accomplish all three of these at once.
Edit:
The following filters and recurses, but does not maintain folder structure
Get-ChildItem -Path ".\" -Filter "*.docx" -Recurse |
Compress-Archive -CompressionLevel Optimal -DestinationPath "$pwd\doc.archive-$(Get-Date -f yyyyMMdd.hhmmss).zip"
This item does not recurse:
Compress-Archive -Path "$pwd\*.docx" -CompressionLevel Optimal -DestinationPath "$pwd\doc.archive-$(Get-Date -f yyyyMMdd.hhmmss).zip"
At some point I had a command that would recurse but not filter, but can't get back to it now.
Unfortunately, Compress-Archive is quite limited as of Windows PowerShell v5.1 / PowerShell Core 6.1.0:
The only way to preserve a subdirectory tree is pass a directory path to Compress-Archive.
Unfortunately, doing so provides no inclusion/exclusion mechanism to only select a subset of files.
Additionally, the resulting archive will internally contain a single root directory named for the input directory (e.g., if you pass C:\temp\foo to Compress-Archive, the resulting archive will contain a single foo directory containing the input directory's subtree - as opposed to containing C:\temp\foo's content at the top level).
There is no option to preserve absolute paths.
A cumbersome work around is to create a temporary copy of your directory tree with only the files of interest (Copy-Item -Recurse -Filter *.docx . $env:TEMP\tmpDir; Compress-Archive $env:TEMP\tmpDir out.zip - note that empty dirs. will be included)
Given that you'll still invariably end up with a single root directory named for the input directory inside the archive, even that may not work for you - see the alternatives at the bottom.
You may be better off with alternatives:
Use the .NET v4.5+ [System.IO.Compression.ZipFile] and [System.IO.Compression.ZipFileExtensions] types directly.
In Windows PowerShell, unlike in PowerShell Core (v6+), you most load the relevant assembly manually with Add-Type -AssemblyName System.IO.Compression.FileSystem - see below.
Use an external programs such as 7-Zip
Solving the problem with direct use of the .NET v4.5+ [System.IO.Compression.ZipFile] class:
Note:
In Windows PowerShell, unlike in PowerShell Core, you most load the relevant assembly manually with Add-Type -AssemblyName System.IO.Compression.FileSystem.
Because PowerShell doesn't support implicit use of extension methods as of Windows PowerShell v5.1 / PowerShell Core 6.1.0, you must make explicit use of the [System.IO.Compression.ZipFileExtensions] class as well.
# Windows PowerShell: must load assembly System.IO.Compression.FileSystem manually.
Add-Type -AssemblyName System.IO.Compression.FileSystem
# Create the target archive via .NET to provide more control over how files
# are added.
# Make sure that the target file doesn't already exist.
$archive = [System.IO.Compression.ZipFile]::Open(
"$pwd\doc.archive-$(Get-Date -f yyyyMMdd.hhmmss).zip",
'Create'
)
# Get the list of files to archive with their relative paths and
# add them to the target archive one by one.
$useAbsolutePaths = $False # Set this to true to use absolute paths instead.
Get-ChildItem -Recurse -Filter *.docx | ForEach-Object {
# Determine the entry path, i.e., the archive-internal path.
$entryPath = (
($_.FullName -replace ([regex]::Escape($PWD.ProviderPath) + '[/\\]'), ''),
$_.FullName
)[$useAbsolutePaths]
$null = [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
$archive,
$_.FullName,
$entryPath
)
}
# Close the archive.
$archive.Dispose()

Keeping track of current directory per user

I am currently creating a client/server application which is trying to keep track of multiple connected users current directories by way of pairing their unique identifier (username), and a new Dir object to an array of hashes like so:
users = []
user = {:user => "userN", :dir => Dir.new(".")}
users.push(user)
...
Although when accessing the dir key within the users hash, I can't seem to use the objects methods properly.
For example:
users[0][:dir].chdir("../")
Returns undefined methodchrdirfor #<Dir:.>
Likewise the method entries which is supposed to accept 1 argument for listing the contents of a directory, only accepts 0 arguments, and when called with 0 arguments it only lists the current directory initialized when Dir was created.
Is there a simple way to keep track of a user's pseudo location within the filesystem?
Edit:: I found the Pathname class and it sort of implements what I need. I am just wondering now if there is a cleaner way to implementing the cd and ls commands when using it.
#Simulate a single users default directory starting point
$dir = Pathname.pwd
#Create a backup of the current directory, change to new directory,
#test to see if the directory exists and if not return to the backup
def cd(dir)
backup = $dir
$dir += dir
$dir = backup if !($dir.directory?)
end
#Take the array of Pathname objects from entries and convert them
#to their string directory values and return the sorted array
def ls(dir)
$dir.entries.map { |pathobject| pathobject.to_s }.sort
end
Your problem actually isn't using a hash incorrectly, it's that Dir.chdir is a global method that changes the working directory of the current process. Dir.entries is similar.
If you're trying to keep track of a path on a per user basis, you could store it as a File, which can also be a directory. That is, directories are represented as a File, so even though it's called "file", it can still store a directory path.
The answer to my question as I've found out is to use the Pathname class: Pathname
It allows you to use the += operator to transverse the file system, although you will have to manually implement many checks to make sure where you are going to transverse to actually exists.
When I implemented my ls command I simply mapped the output of Pathname.entries, and sorted the results.
def ls(pathname)
pathname.entries.map { |pathobject| pathobject.to_s }.sort
end
This gave you an array of sorted strings of all the files in the current directory that Pathname is set to.
For cd you need to make sure the directory exists and if not revert to the previously good directory.
def cd(pathname, directory_to_move_to)
directory_backup = pathname
pathname += directory_to_move_to
pathname = directory_backup if !(pathname.directory?)
end
Example usage:
my_pathname = Pathname.pwd
cd(my_pathname, "../")
ls(my_pathname)

Recursive "touch" on fileserver

There's something I want to accomplish with administrating my Windows file server:
I want to change the "Last Modified" date of all the folders on my server (just the folders and subfolders, not the files within them) to be the same as the most recent "Created" (or maybe "Last Modified") date file within the folder. (In many cases, the date on the folder is MUCH newer than the newest file within it.)
I'd like to do this recursively, from the deepest subfolder to the root. I'd also like to do this without me manually entering any dates and times.
I'm sure with a combination of scripting and a Windows port of "touch" I could maybe accomplish this. Do you have any suggestions? I could maybe accomplish this. Do you have any suggestions?
This closed topic seems really close but I'm not sure how to only touch folders without touching the files inside, or how to get the most-recent-file's date. Recursive touch to fix syncing between computers
If it's for backup purposes, in Windows there is the Archive flag (instead of modifying the timestamp). You can set it recursively with ATTRIB /S (see ATTRIB /?)
If it is for other purposes you can use some touch.exe implementation and use a recursive for:
FOR /R (see FOR /?)
http://ss64.com/nt/for_r.html
http://ss64.com/nt/touch.html
I think that you can do this in PowerShell. I just tried throwing something together and it seems to work correctly. You could invoke this in PowerShell using Set-DirectoryMaxTime(".\Directory") and it will operate recursively on each directory under that.
function Set-DirectoryMaxTime([System.IO.DirectoryInfo]$directory)
{
# Grab a list of all the files in the directory
$files = Get-ChildItem -File $directory
# Get the current CreationTime of the directory we are looking at
$maxdate = Get-Date $directory.CreationTime
# Find the most recently edited file's LastWriteTime
foreach($file in $files)
{
if($file.LastWriteTime -gt $maxdate) { $maxdate = $file.LastWriteTime }
}
# This needs to be in a try/catch block because there is a reasonable chance of it failing
# if a folder is currently in use
try
{
# Give the directory a LastWriteTime equal to the newest file's LastWriteTime
$directory.LastWriteTime = $maxdate
} catch {
# One of the directories could not be updated
Write-Host "Could not update directory: $directory"
}
# Get all the subdirectories of this directory
$subdirectories = Get-ChildItem -Directory $directory
# Jump into each of the subdirectories and do the same thing to each of their CreationTimes
foreach($subdirectory in $subdirectories)
{
Set-DirectoryMaxTime($subdirectory)
}
}

Resources