Invoke-command and running ps1 with parameters - powershell

I'm trying to run a script using invoke-command to install defender for endpoint with some associated parameters.
If I run a standard ps1 using invoke-command it works with no issues. However, if I run the following:
Invoke-Command -ComputerName NAME -FilePath \\srv\share\install.ps1 -OnboardingScript \\srv\share\WindowsDefenderATPonboardingscript.cmd -Passive
I receive "A parameter cannot be found that matches parameter name 'OnboardingScript'". Can someone please help me understand how I invoke a command and run a script with parameters?
Parameters already defined in the install.Ps1 file
https://github.com/microsoft/mdefordownlevelserver/blob/main/Install.ps1
Many thanks in advance

Your Invoke-Command call has a syntax problem, as Santiago Squarzon points out:
Any pass-through arguments - those to be seen by the script whose path is passed to -FilePath - must be specified via the -ArgumentList (-Args) parameter, as an array.
# Simplified example with - of necessity - *positional* arguments only.
# See below.
Invoke-Command -ComputerName NAME -FilePath .\foo.ps1 -Args 'bar', 'another arg'
The same applies to the more common invocation form that uses a script block ({ ... }), via the (potentially positionally implied) -ScriptBlock parameter.
However, there's a catch: Only positional arguments can be passed that way, which:
(a) requires that the target script support positional argument binding for all arguments of interest...
(b) ... which notably precludes passing switch parameters (type [switch]), such as -Passive in your call.
(c) requires you to pass the invariably positional arguments in the correct order.
Workaround:
Use a -ScriptBlock-based invocation, which allows for regular argument-passing with the usual support for named arguments (including switches):
If, as in your case, the script file is accessible by a UNC path visible to the remote session as well, you can simply call it from inside the remote script block.
Note: It isn't needed in your case, but you generally may need $using: references in order to incorporate values from the local session into the arguments - see further below for an example.
Invoke-Command -ComputerName NAME {
& \\srv\share\install.ps1 -OnboardingScript \\srv\share\WindowsDefenderATPonboardingscript.cmd -Passive
}
Otherwise (typically, a script file local to the caller):
Use a $using: reference to pass the content (source code) of your script file to the remote session, parse it into a script block there, and execute that script block with the arguments of interest :
$scriptContent = Get-Content -Raw \\srv\share\install.ps1
Invoke-Command -ComputerName NAME {
& ([scriptblock]::Create($using:scriptContent)) -OnboardingScript \\srv\share\WindowsDefenderATPonboardingscript.cmd -Passive
}
Small caveat: Since the original script file's source code is executed in memory in the remote session, file-related reflection information won't be available, such as the automatic variables that report a script file's full path and directory path ($PSCommandPath and $PSScriptRoot).
That said, the same applies to use of the -FilePath parameter, which essentially uses the same technique of copying the source code rather than a file to the remote session, behind the scenes.

thanks for your reply. I have managed to get this working by adding -ScriptBlock {. "\srv\share etc}

Related

Provide parameter to PS script stored as a string

This is the command that I provided to the customer before:
iex ((New-Object System.Net.WebClient).DownloadString('URL'))
It downloads PS script as a string from URL location and executes the script.
Now I need to pass parameter to this script
iex (((New-Object System.Net.WebClient).DownloadString('URL')) -Parameter 'parameter')
And it is not working because string not accepts parameters
I need to create the simple command (it is important) that will download the script from URL and accept parameters. Otherwise I would save this string to ps1 file and execute it passing the parameter
Could you please help me with that? I am new to PS
Try the following approach:
& ([scriptblock]::Create(
(New-Object System.Net.WebClient).DownloadString('URL')
)) 'Parameter'
Note:
The above runs the script text in a child scope, due to use of &, the call operator, as would happen with local invocation of a script file, whereas Invoke-Expression (iex) runs it in the current scope, i.e. as if it were called with . , the dot-sourcing operator. Change & to . if you want this behavior.
To download the script, you could alternatively use PowerShell's Invoke-RestMethod (irm) cmdlet: Invoke-RestMethod 'URL' or irm 'URL'
[scriptblock]::Create() creates a PowerShell script block, which can then be invoked with & (or .), while also accepting arguments.
As for what you tried:
Invoke-Expression (iex) - which should generally be avoided - has several drawbacks when it comes to executing a script file's content downloaded from the web:
It doesn't support passing arguments, as you've discovered.
An exit statement in the script text would cause the current PowerShell session to exit as a whole.
As noted, the code executes directly in the caller's scope, so that its variables, functions, ... linger in the session after execution (however, it's easy to avoid that with & { iex '...' }).
GitHub issue #5909 discusses enhancing the Invoke-Command cmdlet to robustly support download and execution of scripts directly from the web.

PowerShell Invoke-Command with FilePath on local computer - vague parameters error?

I want to run a script in file on the local machine using Invoke-Command so I can pass in parameters with -ArgumentList. I've been getting an error I don't understand, so I simplified my command. When I do this:
Invoke-Command -FilePath 'getprocess.ps1'
The content of getprocess.ps1 is:
Get-Process
The error message I get is:
Invoke-Command : Parameter set cannot be resolved using the specified named parameters.
At line:1 char:1
+ Invoke-Command -FilePath 'getprocess.ps1'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Invoke-Command], ParameterBindingException
+ FullyQualifiedErrorId : AmbiguousParameterSet,Microsoft.PowerShell.Commands.InvokeCommandCommand
I'm baffled by this error message. What does it mean? How do I get this to work?
tl;dr:
Generally, do not use Invoke-Command for local invocations - while technically possible, there's only one specific use case where doing so is called for (see below).
Instead, invoke scripts directly:
.\getprocess.ps1
Note: Unlike cmd.exe, PowerShell by design requires .\ in order to execute an executable located in the current directory. That is, to avoid accidental execution of executables in the current directory rather than from a directory listed in $env:Path, PowerShell, as a security feature, requires you to signal the intent to execute something in the current directory (.) explicitly.
For script blocks ({ ... }), use &, the call operator (e.g., & { Get-Date }).
For syntactic reasons alone, you situationally also need & for script-file paths if they're specified either as a quoted path (e.g., & '.\getprocess.ps1') and/or if the path involves variable references (e.g.,
& $HOME\getprocess.ps1).
(Separately, . , the dot-sourcing operator is needed in both cases in order to execute a script [block] directly in the caller's scope rather in a child scope).
Note that you can technically combine passing a script block to Invoke-Command (parameter -ScriptBlock) with invoking a local script:
# The script block positionally binds to the -ScriptBlock parameter.
# This is essentially the more expensive equivalent of:
# & .\getprocess.ps1
Invoke-Command { .\getprocess.ps1 }
This is slower and offers no advantage over direct invocation.
However, there is one conceivable use case:
If the script isn't an advanced script and you wanted to take advantage of Invoke-Command's stream-output-collecting
common parameters, such as -ErrorVariable (if the script or function being invoked is advanced, it supports these common parameters itself).
# Invoke locally and collect errors in $errs
Invoke-Command { .\getprocess.ps1 } -ErrorVariable errs
Caveat: At least as of PowerShell 7.2, Invoke-Command does not apply the common -ErrorAction parameter to errors that occur in the script block, so it cannot be used to control error handling; e.g., -ErrorAction Stop has no effect on the commands in the script block.
As for what you tried:
Indeed, as you point out in your own answer, -FilePath must be combined with the
-ComputerName parameter (that the error message is so generic is unfortunate).
More generally, -FilePath must be combined with any of the parameters that request remote execution, which includes -Session, -ConnectionUri, -VmId / -VmName, and, on Unix-like platforms, -HostName, and -SSHConnection.
The purpose of parameter -FilePath is to copy the content of a local script (*.ps1 file) to a remote computer for execution there. That is, it is a convenient mechanism of executing the code of a script that is (only) available locally on a remote machine.
While you can technically target the local computer via -ComputerName localhost (or, more succinctly, via -ComputerName . / -cn .), this does not amount to a local call:
Whenever -ComputerName is specified - even with -ComputerName localhost - PowerShell's remoting infrastructure is used, which has major implications:
The target computer - even if it is the local one - must be set up for PowerShell remoting - see about_Remote_Requirements.
If you target the local machine specifically, you must be running in an elevated session (running as administrator).
Execution will be much slower than direct (local) invocation.
Type fidelity can be lost for both input and output data, given that cross-process marshaling via PowerShell's XML-based serialization infrastructure is involved - see this answer.
That said, if the intent is to locally test remote execution of your script, and your local machine is set up as a remoting target, then use of -ComputerName localhost (-ComputerName . / -cn .) makes perfect sense, given that PowerShell's remoting infrastructure is then involved in the same way it would be in a truly remote call.
Note, however, that such "loopback remoting" calls require elevation (running as admin).
Although the error message doesn't make it clear, the -FilePath parameter makes the -ComputerName parameter required. To explicitly target the local computer, use -ComputerName localhost.
Invoke-Command -FilePath 'getprocess.ps1' -ComputerName localhost
I can't really use invoke-command locally. You use that when you're trying to run a command on a remote PC.
For example, you would want to run something like:
invoke-Command -ComputerName REMOTE-PC -Credentials $credential -Scriptblock {Get-Process}
That error is basically telling you that you need to fill out more parameters that are tied to that command.
try running Get-Help Invoke-Command to see some info on the command and how to run it.
You must have a computername.
$parameters = #{
ComputerName = '255.255.255.255'
FilePath = 'getprocess.ps1'
Credential = 'Domain01\User01'
}
invoke-command #parameters
Use your IP :) To allow that you must also include -credential
If that doesn't do it... invoke-expression may be a semi-suitable replacement for testing until you are ready to invoke-command on the remote machine.
You could run it locally like this, but you'd have to be at the administrator (elevated) prompt. It's nice to be able to run it as a test.
invoke-command localhost getprocess.ps1
You can actually do a strange form of parallelism too:
invoke-command localhost,localhost,localhost getprocess.ps1

Can't pass a script block as a parameter to powershell.exe via -Command

I'm trying this
$Global:commandBlock={
Start-Transcript -path $projectFolder\gruntLog.txt;
grunt $argList;
Stop-Transcript
}
$cmdProc=start-process powershell -ArgumentList ('-command `$Global:commandBlock') -WorkingDirectory $fwd -PassThru -NoNewWindow:$NoNewWindow
And keep getting $commandBlock : The term '$Global:commandBlock' is not recognized as the name of a cmdlet, function, script file, or operable program.
My guess was it has to do with scope. But making variable global didn't help. Adding -args $commandBlock like that:
-ArgumentList ('-command `$Global:commandBlock -args "-commandBlock:$commandBlock"')
-ArgumentList ('-command `$Global:commandBlock -args $commandBlock"')
didn't help
And I'm not sure that I escape variables correctly in the block, read this, but not sure how to apply to my script.
There's a few things which I think are keeping this from working. First, when you're using single quotes, ' you're instructing PowerShell to operate literally. This means that it won't expand variables. Not what you're looking for.
A better way to do this is to do it with an subexpression like this.
$Global:commandBlock={
'ham' >> C:\temp\test.txt
}
$cmdProc=start-process powershell -ArgumentList ("-command $($Global:commandBlock)") -PassThru -NoNewWindow:$NoNewWindow
This will give you the desired results.
Subexpressions are pretty sweet. It lets you embed a mini-scriptblock within a string, and it's then expanded out in the parent string.
"today's date is $(get-date), on system: $($env:COMPUTERNAME)"
today's date is 02/14/2017 11:50:49, on system: BEHEMOTH
There are two major issues (leaving the obvious mistake of attempting to reference a variable inside a single-quoted string aside):
Any argument you want to pass to a new powershell instance via -Command must be escaped in non-obvious ways if it contains " and/or \ chars, which is especially likely if you're passing a piece of PowerShell source code.
The escaping issue can generally be solved by Base64-encoding the source-code string and passing it via the -EncodedCommand parameter - see this answer of mine to a related question for how to do that, but a more concise alternative is presented below.
If the source code being passed references any variables that only exist in the calling session, the new instance won't see them.
The solution is not to reference session-specific variables in the source code being passed, but to pass their values as parameter values instead.
To solve the local-variable-not-seen-by-the-new-instance problem, we must rewrite the script block to accept parameters:
$scriptBlock={
param($projectFolder, $argList)
# For demonstration, simply *output* the parameter values.
"folder: [$projectFolder]; arguments: [$argList]"
}
Now we can apply the necessary escaping, using PetSerAl's sophisticated -replace expression from his comment on the question.
We can then invoke the resulting string with & {...} while passing it parameter values (I'm omitting the -WorkingDirectory and -PassThru parameters for brevity):
# Parameter values to pass.
$projectFolder = 'c:\temp'
$argList='-v -f'
Start-Process -NoNewWindow powershell -ArgumentList '-noprofile', '-command',
(('& {' + $scriptBlock.ToString() + '}') -replace '\"|\\(?=\\*("|$))', '\$&'),
"'$projectFolder'",
"'$argList'"
For an explanation of the regular expression, again see this answer.
Note how the variable values passed as parameters to the script block are enclosed in '...' inside a "..."-enclosed string in order to:
pass the values as a single parameter value.
protect them from another round of interpretation by PowerShell.
Note: If your variable values have embedded ' instances, you'll have to escape them as ''.
The above yields:
folder: [c:\temp]; arguments: [-v -f]
Alternative with a temporary, self-deleting script file:
Using -File with a script file has the advantage of being able to pass parameter values as literals, with no concern over additional interpretation of their contents.
Caveat: As of PowerShell Core v6-beta.3, there is a problem when passing parameter values that start with -: they are not bound as expected; see this GitHub issue.
To work around this problem, the sample script block below accesses only the first parameter by name, and relies on all remaining ones binding via the automatic $Args variable.
# Define the script block to be executed by the new PowerShell instance.
$scriptBlock={
param($projectFolder)
# For demonstration, simply *output* the parameter values.
"folder: [$projectFolder]; arguments: [$Args]"
}
# Parameter values to pass.
$projectFolder = 'c:\temp'
$argList='-v -f'
# Determine the temporary script path.
$tempScript = "$env:TEMP\temp-$PID.ps1"
# Create the script from the script block and append the self-removal command.
# Note that simply referencing the script-block variable inside `"..."`
# expands to the script block's *literal* content (excluding the enclosing {...})
"$scriptBlock; Remove-Item `$PSCommandPath" > $tempScript
# Now invoke the temporary script file, passing the arguments as literals.
Start-Process -NoNewWindow powershell -ArgumentList '-NoProfile', '-File', $tempScript,
$projectFolder,
$argList
Again, the above yields:
folder: [c:\temp]; arguments: [-v -f]
I've messed around with the syntax for passing args to a new powershell instance and have found the following works. So many variations fail without a good error message. Maybe it would work in your case?
$arg = "HAM"
$command = {param($ham) write-host $ham}
#please not its important to wrap your command
#in a further script block to stop it being processed to a string at execution
#The following would normally suffice "& $command $arg"
Start-Process powershell -ArgumentList "-noexit -command & {$command} $arg"
Also simply using the Invoke-Command gives you the -ArgumentList parameter to opperate against the given Command that you are missing with the standard powershell.exe parameters. This is probably a bit cleaner looking.
Start-Process powershell -ArgumentList "-noexit -command invoke-command -scriptblock {$command} -argumentlist $arg"
No need for any extra complex escaping or unwanted persisted variables. Just keep the script block in curly braces so it remains a script block on arrival in the new session. At least in this simple case...
If you have several string parameters that contain spaces. I found popping the string in a single parenthesis and separating with commas works well. You could also probably pass a predefined array as a single argument.
Start-Process powershell -ArgumentList "-noexit -command invoke-command -scriptblock {$command} -argumentlist '$arg1', '$arg2', '$arg3'"
Will this work:
$Global:commandBlock={
Start-Transcript -path $projectFolder\gruntLog.txt;
grunt $argList;
Stop-Transcript
}
& $Global:commandBlock

Run Powershell script that uses -List (parameter) with alternate credentials that

I was struggling to get this simple (?) function working today.
I have a PowerShell script that reads computer names from a txt-based file. It works fine when run from a PowerShell session by the following one-liner:
./"Server Health Check.ps1" -List One-off.txt
As you can see, it's got a long file name, so it's wrapped with quotes.
However, I'm building a PowerShell GUI form with radio boxes that will pass on a choice for a text file that will be used to a call the script. Trick is, the script needs to be run with alternate admin account, and i'm not clear how to make that work.
For another script I've got that doesn't use I know i can use something along the lines of the following, this uses the old DOS "runas", however, it doesn't work with the -list function.
invoke-command -scriptblock {runas.exe /user:domain\$Env:Username"admin" "powershell.exe -file \"\\Server\c$\LONG FOLDER\Server Health Check.PS1""}
So, in a nutshell, how do get a script to launch with alternate credentials that reads a parameter (-List) from the command line? I'm also keen to preserve my directory structure, which includes folders with spaces. The script is titled: "Server health check.ps1"
The last thing I tried was the following
$ScriptPath = "C:\SCRIPTS FOLDER\Server Health Check.ps1"
$ArgList = "-List C:\SCRIPTS FOLDER\One-off.txt"
Invoke-Command -filepath $ScriptPath -Credential DragonBallDomain\$Env:UserName"Admin" -ArgumentList $ArgList
The result was the following message:
Invoke-Command : Parameter set cannot be resolved using the specified named parameters.
I'm almost certain this is do-able by invoke-command or start-process, it's just a matter of getting the correct formatting? I'm probably missing a / or a ' or "" somewhere in my trials with start-process or invoke-command.
Any help appreciated!
Update for April 30:
I've tried some more to make this work, i'm close, but still not quite there.
$LongScriptPath = resolve-path Script.ps1
$LongFolderPath = \\UNC\PATH TO FOLDER\WITH LONG NAME\
start-process -filepath powershell.exe -argumentlist " -file``"$($FilePath.path)`"" -cred DOMAIN\USERID -WorkingDirectory "$LongFolderPath"
Adding the -credential is what causes an error that states that the -file parameter is invalid. I'm sure there's a way to do this.
Note: Completely rewritten after the requirements became clearer.
To run a command as a different user locally, use Start-Process -Credential ...
That is what you've attempted in your update in principle, but there are problems with how you're passing parameters; try this instead:
$LongScriptPath = resolve-path Script.ps1
$LongFolderPath = '\\UNC\PATH TO FOLDER\WITH LONG NAME\'
start-process `
powershell.exe `
-ArgumentList '-file', $LongScriptPath, '-List', 'One-off.txt' `
-Credential DOMAIN\USERID `
-WorkingDirectory $LongFolderPath
The key to making this work is to pass all parameters to pass to powershell.exe as an array via Start-Process's -ArgumentList parameter, which means that the parameters must be ,-separated.
Note how an array is always parsed in expression mode, which means that literal string elements such as -file and -List must be quoted.
It is important in general to understand the difference between PowerShell's two fundamental parsing modes, argument mode and expression mode, and which is applied when - see https://technet.microsoft.com/en-us/library/hh847892.aspx
Add -Wait to wait for the script to finish; Start-Process is asynchronous by default (all PS cmdlets named Start-* are).
Caveat: For commands invoked as a different user, you can only wait from an elevated prompt.
If it isn't, the command will still execute, but will do so asynchronously, and you'll get an Access denied error message in the current console; in effect, -Wait is ignored.
Only if not running as a different user: Add -NoNewWindow -Wait if you want to run the script in the current console window; Start-Process opens a new window by default for console applications such as powershell.exe and cmd.exe.
If you do run the command as a different user, -NoNewWindow is quietly ignored.
As for the original symptom and why using Invoke-Command to run a command locally as a different user is ill-advised:
Invoke-Command -Credential ... requires that the -ComputerName parameter be specified too.
Run Get-Help Invoke-Command to see all parameter sets that involve the -Credential parameter. The OP's original command had only -Credential, but not -ComputerName, which caused PS to complain that no parameter set could be unambiguously identified.
Once you use -ComputerName, PowerShell remoting is invariably used, even if you specify . - the local computer - as the only computer to target.
Using remoting has two implications:
Remoting is not available by default, and must be configured on the target computer (the local computer, in this case).
Using remoting requires invocation with admin privileges.
In short:
While you can perform purely local invocations with Invoke-Command, you cannot do so as another user, because that invariably involves remoting.
Start-Process, by contrast, solely exists to run commands locally, optionally as a different user.

Is it possible to call a powershell script within another script as a variable?

I have a Powershell script where the user passes in a script as a parameter. After that is passed in, I cannot call the script by using $scriptvariable. Is there any way to call a Powershell script from within another Powershell script, when the one script needs to be called from a variable.
param(
[string]hostval,
[string]$scriptpath
)
Invoke-Command -Computer $hostval -Scriptblock { $scriptpath } -credential $cred
This does not work, and I'm not sure if what I want is possible. Is there a parameter type (ex: [script]$scriptpath) that I can use so the script can be called from $scriptpath?
It sounds like you need to use the -FilePath parameter, instead of -Scriptblock:
-FilePath <String>
Runs the specified local script on one or more remote computers. Enter the path and file name of the script, or pipe a script path to Invoke-Command. The script must reside on the local computer or in a directory that the local computer can access. Use the ArgumentList parameter to specify the values of parameters in the script.