Using -Confirm in Powershell - powershell

I am trying to understand what the relationship between the $? and $lastexitcode variables versus the -Confirm flag in Powershell cmdlets.
Say for example you run a command with -confirm it will prompt you accordingly for action:
PS C:\temp> rm .\foo.txt -confirm
Confirm
Are you sure you want to perform this action?
Performing the operation "Remove Directory" on target "C:\temp\foo.txt".
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help
(default is "Y"):n
PS C:\temp> $?
True
I understand that technically the command ran successfully, but if the user chose no then the command did not run.
My question is how to I obtain the user's answer to the -Confirm flag?

$?, $LastExitCode, and -Confirm are completely unrelated to each other.
$? is an automatic variable with a boolean value indicating whether or not the last (PowerShell) operation was executed successfully.
$LastExitCode is an automatic variable with the exit code of the external command that was last executed (an integer value).
-Confirm is a common parameter controlling whether or not a cmdlet prompts the user for confirmation of its action.
To my knowledge PowerShell does not store the answer given to a -Confirm prompt anywhere, so if you need that response for something else you'll have to prompt the user yourself, e.g. like this:
function Read-Confirmation {
Param(
[Parameter(Mandatory=$false)]
[string]$Prompt,
[Parameter(Mandatory=$false)]
[string]$Message
)
$choices = New-Object Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes'))
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&No'))
-not [bool]$Host.UI.PromptForChoice($Message, $Prompt, $choices, 1)
}
$doRemove = if ($PSBoundParameters['Confirm'].IsPresent) {
Read-Confirmation -Prompt 'Really delete'
} else {
$true
}
if ($doRemove) {
Remove-Item .\foo.txt -Force
}

AFAIK, it is not possible to capture the user's response to the confirmation prompt; it is not a part of PowerShell's command history, and while you might be able to get the information from the buffer somehow that would only be supported in the default PowerShell host as other hosts will use different buffers. In this case it is probably best to either do a separate confirmation within your script using an if statement.
$userAnswer = Read-Host "Are you sure you wish to proceed?"
if($userAnswer -eq "yes"){
rm .\foo.txt
}
Then just use the $userAnswer variable to know what your user responded with. Alternatively you could determine their answer by checking to see if the operation was completed. This would be my preferred method as this way you are SURE that the file has been deleted rather than assuming so because the cmdlet successfully executed and the user confirmed (the reliability is probably not any different here considering that remove-item is incredibly well tested but it could make a difference if you are using a third-party library of some kind) that would look something like the below.
rm .\foo.txt -Confirm
if(Test-Path .\foo.txt){
$success = $false
} else {
$success = $true
}
and if you really need to know whether it failed to delete due to an error or the user saying no you could do something like
rm .\foo.txt -Confirm
if(Test-Path .\foo.txt){
$success = $false
} else {
$success = $true
}
if(!($success) -and (!($?))){
$status = "Previous command failed"
} elseif (!($success) -and $?){
$status = "User cancelled operation"
}
Hope that helps.

Related

Check if given process is running with elevated right with powershell and Get-WmiObject

I have to following part of my script:
$active_processes = (Get-WmiObject -Class Win32_Process | where path -like $path | Select-Object -ExpandProperty Path | split-path -leaf | Select-Object -Unique)
It's working fine but I need to check if the process I get after all the script is running with elevated rights to launch another process with elevated rights if neccesary so it can interact with said process. I don't see any information about elevated rights with Get-WmiObject, I was wondering if I'm missing it or if there's another way to get that information
I don't need to run the powershell script as administrator. What I need is to find ff any executable requires elevated rights when launched and I need to find this information via powershell.
After some research on how windows knows if it needs admin to run an executable, I concluded that there are a couple ways but the most recommended and reliable is reading the executable manifest, so I wrote the following function:
function Get-ManifestFromExe{
Param(
[Parameter(Mandatory=$true,Position=0,ValueFromPipelineByPropertyName=$true)]
[Alias("Path")]
[ValidateScript({Test-Path $_ -IsValid})]
[String]$FullName
)
begin{
$stringStart = '<assembly'
$stringEnd = 'assembly>'
}
process{
$content = Get-Content $FullName -Raw
$indexStart = $content.IndexOf($stringStart)
$content = $content.Substring($indexStart)
$indexEnd = ($content.IndexOf($stringEnd)) + $stringEnd.Length
$content = $content.Substring(0,$indexEnd)
if($content -match "$stringStart(.|\s)+(?=$stringEnd)$stringEnd"){
return [XML]$Matches[0]
}
}
}
function Test-IsAdminRequired{
Param(
[Parameter(Mandatory=$true,Position=0)]
[XML]$xml
)
$value = $xml.assembly.trustInfo.security.requestedPrivileges.requestedExecutionLevel.level
if(-not [String]::IsNullOrEmpty($value)){
return ($value -eq "requireAdministrator" -or $value -eq "highestAvailable")
}else{
Write-Error "Provided xml does not contain requestedExecutionLevel node or level property"
}
}
$exe = '.\Firefox Installer.exe'
Get-ManifestFromExe -Path $exe
Test-IsAdminRequired -xml $exeManifest
It works by extracting the manifest XML from the executable and checking requestedExecutionLevel node's level property, the values accepted for this property are in this page, and quoted here:
asInvoker, requesting no additional permissions. This level requires
no additional trust prompts.
highestAvailable, requesting the highest permissions available to the
parent process.
requireAdministrator, requesting full administrator permissions.
So from this we can conclude that only highestAvailable and requireAdministrator would need admin privileges, so I check those, with that we will be done EXCEPT that some executables I tested (mostly installers) don't require admin to run but instead they prompt the UAC when they ruin their child executable, I don't really see a way to check this.. sorry.
BTW I really enjoyed this question (specially the research), hope this can help you.
SOURCES
What is the difference between "asInvoker" and "highestAvailable" execution levels?
reading an application's manifest file?
https://learn.microsoft.com/en-us/visualstudio/deployment/trustinfo-element-clickonce-application?view=vs-2019#requestedexecutionlevel
It's in the System.Security.Principal classes. This returns $true if the current user is elevated to local Administrator:
(New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)

Restrict multiple executions of the same script

I have tried to restrict multiple executions of the same script in PowerShell. I have tried following code. Now it is working, but a major drawback is that when I close the PowerShell window and try to run the same script again, it will execute once again.
Code:
$history = Get-History
Write-Host "history=" $history.Length
if ($history.Length -gt 0) {
Write-Host "this script already run using History"
return
} else {
Write-Host "First time using history"
}
How can I avoid this drawback?
I presume you want to make sure that a script is not running from different powershell processes, and not from the same one as some sort of self-call.
In either case there isn't anything in powershell for this, so you need to mimic a semaphore.
For the same process, you can leverage a global variable and wrap your script around a try/finally block
$variableName="Something unique"
try
{
if(Get-Variable -Name $variableName -Scope Global -ErrorAction SilentlyContinue)
{
Write-Warning "Script is already executing"
return
}
else
{
Set-Variable -Name $variableName -Value 1 -Scope Global
}
# The rest of the script
}
finally
{
Remove-Variable -Name $variableName -ErrorAction SilentlyContinue
}
Now if you want to do the same, then you need to store something outside of your process. A file would be a good idea with a similar mindset using Test-Path, New-Item and Remove-Item.
In either case, please note that this trick that mimics semaphores, is not as rigid as an actual semaphore and can leak.

Negate having to Press Enter twice on PowerShell script

I am trying to run a PowerShell script daily through task scheduler, however the script will not run. When I enter the code below manually into PowerShell (as an administrator), it makes me press enter twice. I believe since i have to press enter twice is the reason it will not run through the task scheduler.
Is there a way to adjust my code to get this to work with the task scheduler?
I am running Windows 2012 R2 and Version 5.1 of PowerShell.
Please note that i ran the exact same script on my computer, which is Windows 10 and running version 5.1 of PowerShell, and it worked the correct way (only had to press enter once)
I expect to only press enter once to run my PowerShell script, but the actual output from the first time i press enter brings another line with just ">>" and then i press enter the second time and the script executes.
Powershell Script:
# Load WinSCP .NET assembly
Add-Type -Path "WinSCPnet.dll"
# Set up session options
$sessionOptions = New-Object WinSCP.SessionOptions -Property #{
Protocol = [WinSCP.Protocol]::Sftp
HostName = ""
UserName = ""
Password = ""
SshHostKeyFingerprint = ""
}
$session = New-Object WinSCP.Session
try
{
# Connect
$session.Open($sessionOptions)
# Transfer files
$session.PutFiles("", "").Check()
}
finally
{
$session.Dispose()
}
If a script requires user interaction, then it really should not be a scheduled task.
If you write a script that requires confirmation, then you need to look using -Confirm parameter.
See Are you sure? Using the -WhatIf and -Confirm parameters in PowerShel
Remove-MailContact -Identity “$sourceEmail” -Confirm:$Y -WhatIf
The cmdlet or code you write has to support it. For code you write, that means using advanced functions.
How to write a PowerShell function to use Confirm, Verbose and WhatIf
function Set-FileContent
{
[cmdletbinding(SupportsShouldProcess)]
Param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$Content,
[Parameter(Mandatory = $true)]
[ValidateScript( {Test-Path $_ })]
[string]$File
)
if ($PSCmdlet.ShouldProcess("$File" , "Adding $Content to "))
{
Set-Content -Path $File -Value $Content
}
}
See also ConfirmPreference

Powershell Suppress Certificate Notification

I am writing a script to connect from a Windows 10 Client to a Terminal Server with the RDP-Protocoll.
The thought behind is: On these ThinClients we have about 20 RDP-Files. With about 10 of them, the password needs to be safed.
So it is quite a lot of work if you always have to save the password on every new ThinClient.
But I thought I could solve this problem with a powershell script. I just have to open the connection 1 time successfully and save the credentials and further on the credentials are saved.
I will show my code first:
$Server = "xx.yy.zz.xx"
$User = "DOMAIN\User"
$password = "password"
cmdkey /generic:"$Server" /user:"$User" /pass:"$password"
mstsc /v:"$Server"
This works so far.
But I always get the this Notification:
This is a Symbol-Picture from the Internet, as my Notification is in German. It is exactly the same, just easier to understand.
Even if I install the certificate, the notification keeps popping up.
How can I check the field with Powershell, where it says Don't ask me again for connections to this computer?
Ok i found a solution!
There is a registry-Key generated when you Tick "Dont ask me again..."
Now i just added the necessary Registry Keys with my Powershell Script..
function Test-RegistryValue {
param (
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]$Path,
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]$Value
)
try{
Get-ItemProperty -Path $Path | Select-Object -ExpandProperty $Value -ErrorAction Stop | Out-Null
return $true
}
catch{
return $false
}
}
#Certificate warning turn off
$exists = Test-RegistryValue -Path 'HKCU:\Software\Microsoft\Terminal Server Client' -Value 'AuthenticationLevelOverride'
if($exists -eq $False){
reg add "HKEY_CURRENT_USER\Software\Microsoft\Terminal Server Client" /v "AuthenticationLevelOverride" /t "REG_DWORD" /d 0 /f
}
Like this it works without this certificate notification!

Redirect Write-Host statements to a file

I have a PowerShell script that I am debugging and would like to redirect all Write-Host statements to a file. Is there an easy way to do that?
Until PowerShell 4.0, Write-Host sends the objects to the host. It does not return any objects.
Beginning with PowerShell 5.0 and newer, Write-Host is a wrapper for Write-Information, which allows to output to the information stream and redirect it with 6>> file_name.
http://technet.microsoft.com/en-us/library/hh849877.aspx
However, if you have a lot of Write-Host statements, replace them all with Write-Log, which lets you decide whether output to console, file or event log, or all three.
Check also:
Add-Content
redirection operators like >, >>, 2>, 2>, 2>&1
Write-Log
Tee-Object
Start-Transcript.
You can create a proxy function for Write-Host which sends objects to the standard output stream instead of merely printing them. I wrote the below cmdlet for just this purpose. It will create a proxy on the fly which lasts only for the duration of the current pipeline.
A full writeup is on my blog here, but I've included the code below. Use the -Quiet switch to suppress the console write.
Usage:
PS> .\SomeScriptWithWriteHost.ps1 | Select-WriteHost | out-file .\data.log # Pipeline usage
PS> Select-WriteHost { .\SomeScriptWithWriteHost.ps1 } | out-file .\data.log # Scriptblock usage (safer)
function Select-WriteHost
{
[CmdletBinding(DefaultParameterSetName = 'FromPipeline')]
param(
[Parameter(ValueFromPipeline = $true, ParameterSetName = 'FromPipeline')]
[object] $InputObject,
[Parameter(Mandatory = $true, ParameterSetName = 'FromScriptblock', Position = 0)]
[ScriptBlock] $ScriptBlock,
[switch] $Quiet
)
begin
{
function Cleanup
{
# Clear out our proxy version of write-host
remove-item function:\write-host -ea 0
}
function ReplaceWriteHost([switch] $Quiet, [string] $Scope)
{
# Create a proxy for write-host
$metaData = New-Object System.Management.Automation.CommandMetaData (Get-Command 'Microsoft.PowerShell.Utility\Write-Host')
$proxy = [System.Management.Automation.ProxyCommand]::create($metaData)
# Change its behavior
$content = if($quiet)
{
# In quiet mode, whack the entire function body,
# simply pass input directly to the pipeline
$proxy -replace '(?s)\bbegin\b.+', '$Object'
}
else
{
# In noisy mode, pass input to the pipeline, but allow
# real Write-Host to process as well
$proxy -replace '(\$steppablePipeline\.Process)', '$Object; $1'
}
# Load our version into the specified scope
Invoke-Expression "function ${scope}:Write-Host { $content }"
}
Cleanup
# If we are running at the end of a pipeline, we need
# to immediately inject our version into global
# scope, so that everybody else in the pipeline
# uses it. This works great, but it is dangerous
# if we don't clean up properly.
if($pscmdlet.ParameterSetName -eq 'FromPipeline')
{
ReplaceWriteHost -Quiet:$quiet -Scope 'global'
}
}
process
{
# If a scriptblock was passed to us, then we can declare
# our version as local scope and let the runtime take
# it out of scope for us. It is much safer, but it
# won't work in the pipeline scenario.
#
# The scriptblock will inherit our version automatically
# as it's in a child scope.
if($pscmdlet.ParameterSetName -eq 'FromScriptBlock')
{
. ReplaceWriteHost -Quiet:$quiet -Scope 'local'
& $scriptblock
}
else
{
# In a pipeline scenario, just pass input along
$InputObject
}
}
end
{
Cleanup
}
}
You can run your script in a secondary PowerShell shell and capture the output like this:
powershell -File 'Your-Script.ps1' > output.log
That worked for me.
Using redirection will cause Write-Host to hang. This is because Write-Host deals with various formatting issues that are specific to the current terminal being used. If you just want your script to have flexibility to output as normal (default to shell, with capability for >, 2>, etc.), use Write-Output.
Otherwise, if you really want to capture the peculiarities of the current terminal, Start-Transcript is a good place to start. Otherwise you'll have to hand-test or write some complicated test suites.
Try adding a asterisk * before the angle bracket > to redirect all streams:
powershell -File Your-Script.ps1 *> output.log
When stream redirection is requested, if no specific stream is indicated then by default only the Success Stream(1>) is redirected. Write-Host is an alias for Write-Information which writes to the Information Stream (6>). To redirect all streams use *>.
Powershell-7.1 supports redirection of multiple output streams:
Success Stream (#1): PowerShell 2.0 Write-Output
Error Stream (#2): PowerShell 2.0 Write-Error
Warning Stream (#3): PowerShell 3.0 Write-Warning
Verbose Stream (#4): PowerShell 3.0 Write-Verbose
Debug Stream (#5): PowerShell 3.0 Write-Debug
Information Stream (#6): PowerShell 5.0 Write-Information
All Streams (*): PowerShell 3.0
This worked for me in my first PowerShell script that I wrote few days back:
function logMsg($msg)
{
Write-Output $msg
Write-Host $msg
}
Usage in a script:
logMsg("My error message")
logMsg("My info message")
PowerShell script execution call:
ps> .\myFirstScript.ps1 >> testOutputFile.txt
It's not exactly answer to this question, but it might help someone trying to achieve both logging to the console and output to some log file, doing what I reached here :)
Define a function called Write-Host. Have it write to a file. You may have some trouble if some invocations use a weird set of arguments. Also, this will only work for invocations that are not Snapin qualified.
If you have just a few Write-Host statements, you can use the "6>>" redirector operator to a file:
Write-Host "Your message." 6>> file_path_or_file_name
This is the "Example 5: Suppress output from Write-Host" provided by Microsoft, modified accordingly to about_Operators.
I just added Start-Transcript at the top of the script and Stop-Transcript at the bottom.
The output file was intended to be named <folder where script resides>-<datestamp>.rtf, but for some reason the trace file was being put where I did not expect it — the desktop!
You should not use Write-Host if you wish to have the messages in a file. It is for writing to the host only.
Instead you should use a logging module, or Set/Add-Content.
I have found the best way to handle this is to have a logging function that will detect if there is a host UI and act accordingly. When the script is executed in interactive mode it will show the details in the host UI, but when it is run via WinRM or in a non-interactive mode it will fall back on the Write-Output so that you can capture it using the > or *> redirection operators
function Log-Info ($msg, $color = "Blue") {
if($host.UI.RawUI.ForegroundColor -ne $null) {
Write-Host "`n[$([datetime]::Now.ToLongTimeString())] $msg" -ForegroundColor $color -BackgroundColor "Gray"
} else {
Write-Output "`r`n[$([datetime]::Now.ToLongTimeString())] $msg"
}
}
In cases where you want to capture the full output with the Write-Host coloring, you can use the Get-ConsoleAsHtml.ps1 script to export the host's scrolling buffer to an HTML or RTF file.
Use Write-Output instead of Write-Host, and redirect it to a file like this:
Deploy.ps1 > mylog.log or Write-Output "Hello World!" > mylog.log
Try using Write-Output instead of Write-Host.
The output goes down the pipeline, but if this is the end of the pipe, it goes to the console.
> Write-Output "test"
test
> Write-Output "test" > foo.txt
> Get-Content foo.txt
test