Starting .ps1 Script from PowerShell with Parameters and Credentials and getting output using variable
Asked Answered
C

4

11

Hello Stack Community :)

I have a simple goal. I'd like to start some PowerShell Script from an another Powershell Script, but there are 3 conditions:

  1. I have to pass credentials (the execution connects to a database that has specific user)
  2. It has to take some Parameters
  3. I'd like to pass the output into a variable

There is a similar question Link. But the answer is to use files as a way to communicate between 2 PS Scripts. I just would like to avoid access conflicts. @Update: The Main Script is going to start few other scripts. so the solution with files can be tricky, if the execution will be performed from multiple user at the same time.

Script1.ps1 is the script that should have string as an output. (Just to be clear, it's a fictive script, the real one has 150 Rows, so I just wanted to make an example)

param(  
[String]$DeviceName
)
#Some code that needs special credentials
$a = "Device is: " + $DeviceName
$a

ExecuteScripts.ps1 should invoke that one with those 3 conditions mentioned above

I tried multiple solutions. This one for examplte:

$arguments = "C:\..\script1.ps1" + " -ClientName" + $DeviceName
$output = Start-Process powershell -ArgumentList $arguments -Credential $credentials
$output 

I don't get any output from that and I can't just call the script with

&C:\..\script1.ps1 -ClientName PCPC

Because I can't pass -Credential parameter to it..

Thank you in Advance!

Cole answered 27/3, 2020 at 15:9 Comment(2)
If this is just about access conflicts: creating unique filenames for every invocation would solve your problem, right?Applicant
@Applicant if it's the only way, I would stack with that solution. Just generating random file names, checking if such file exists...I will be executing 6 to 10 scripts from my Java Code and it would need 6 to 10 files every time i'm using or someone else uses my application. So its about performance alsoCole
A
2

Note:

  • The following solution works with any external program, and captures output invariably as text.

  • To invoke another PowerShell instance and capture its output as rich objects (with limitations), see the variant solution in the bottom section or consider Mathias R. Jessen's helpful answer, which uses the PowerShell SDK.

Here's a proof-of-concept based on direct use of the System.Diagnostics.Process and System.Diagnostics.ProcessStartInfo .NET types to capture process output in memory (as stated in your question, Start-Process is not an option, because it only supports capturing output in files, as shown in this answer):

Note:

  • Due to running as a different user, this is supported on Windows only (as of .NET Core 3.1), but in both PowerShell editions there.

  • Due to needing to run as a different user and needing to capture output, .WindowStyle cannot be used to run the command hidden (because using .WindowStyle requires .UseShellExecute to be $true, which is incompatible with these requirements); however, since all output is being captured, setting .CreateNoNewWindow to $true effectively results in hidden execution.

  • Only stdout output is captured below. If you want to capture stderr output too, you'll need to capture it via events, because use of $ps.StandardError.ReadToEnd() alongside $ps.StandardOutput.ReadToEnd() could lead to deadlocks.

# Get the target user's name and password.
$cred = Get-Credential

# Create a ProcessStartInfo instance
# with the relevant properties.
$psi = [System.Diagnostics.ProcessStartInfo] @{
  # For demo purposes, use a simple `cmd.exe` command that echoes the username. 
  # See the bottom section for a call to `powershell.exe`.
  FileName = 'cmd.exe'
  Arguments = '/c echo %USERNAME%'
  # Set this to a directory that the target user
  # is permitted to access.
  WorkingDirectory = 'C:\'                                                                   #'
  # Ask that output be captured in the
  # .StandardOutput / .StandardError properties of
  # the Process object created later.
  UseShellExecute = $false # must be $false
  RedirectStandardOutput = $true
  RedirectStandardError = $true
  # Uncomment this line if you want the process to run effectively hidden.
  #   CreateNoNewWindow = $true
  # Specify the user identity.
  # Note: If you specify a UPN in .UserName
  # ([email protected]), set .Domain to $null
  Domain = $env:USERDOMAIN
  UserName = $cred.UserName
  Password = $cred.Password
}

# Create (launch) the process...
$ps = [System.Diagnostics.Process]::Start($psi)

# Read the captured standard output.
# By reading to the *end*, this implicitly waits for (near) termination
# of the process.
# Do NOT use $ps.WaitForExit() first, as that can result in a deadlock.
$stdout = $ps.StandardOutput.ReadToEnd()

# Uncomment the following lines to report the process' exit code.
#   $ps.WaitForExit()
#   "Process exit code: $($ps.ExitCode)"

"Running ``cmd /c echo %USERNAME%`` as user $($cred.UserName) yielded:"
$stdout

The above yields something like the following, showing that the process successfully ran with the given user identity:

Running `cmd /c echo %USERNAME%` as user jdoe yielded:
jdoe

Since you're calling another PowerShell instance, you may want to take advantage of the PowerShell CLI's ability to represent output in CLIXML format, which allows deserializing the output into rich objects, albeit with limited type fidelity, as explained in this related answer.

# Get the target user's name and password.
$cred = Get-Credential

# Create a ProcessStartInfo instance
# with the relevant properties.
$psi = [System.Diagnostics.ProcessStartInfo] @{
  # Invoke the PowerShell CLI with a simple sample command
  # that calls `Get-Date` to output the current date as a [datetime] instance.
  FileName = 'powershell.exe'
  # `-of xml` asks that the output be returned as CLIXML,
  # a serialization format that allows deserialization into
  # rich objects.
  Arguments = '-of xml -noprofile -c Get-Date'
  # Set this to a directory that the target user
  # is permitted to access.
  WorkingDirectory = 'C:\'                                                                   #'
  # Ask that output be captured in the
  # .StandardOutput / .StandardError properties of
  # the Process object created later.
  UseShellExecute = $false # must be $false
  RedirectStandardOutput = $true
  RedirectStandardError = $true
  # Uncomment this line if you want the process to run effectively hidden.
  #   CreateNoNewWindow = $true
  # Specify the user identity.
  # Note: If you specify a UPN in .UserName
  # ([email protected]), set .Domain to $null
  Domain = $env:USERDOMAIN
  UserName = $cred.UserName
  Password = $cred.Password
}

# Create (launch) the process...
$ps = [System.Diagnostics.Process]::Start($psi)

# Read the captured standard output, in CLIXML format,
# stripping the `#` comment line at the top (`#< CLIXML`)
# which the deserializer doesn't know how to handle.
$stdoutCliXml = $ps.StandardOutput.ReadToEnd() -replace '^#.*\r?\n'

# Uncomment the following lines to report the process' exit code.
#   $ps.WaitForExit()
#   "Process exit code: $($ps.ExitCode)"

# Use PowerShell's deserialization API to 
# "rehydrate" the objects.
$stdoutObjects = [Management.Automation.PSSerializer]::Deserialize($stdoutCliXml)

"Running ``Get-Date`` as user $($cred.UserName) yielded:"
$stdoutObjects
"`nas data type:"
$stdoutObjects.GetType().FullName

The above outputs something like the following, showing that the [datetime] instance (System.DateTime) output by Get-Date was deserialized as such:

Running `Get-Date` as user jdoe yielded:

Friday, March 27, 2020 6:26:49 PM

as data type:
System.DateTime
Applicant answered 27/3, 2020 at 16:14 Comment(0)
S
5

Start-Process would be my last resort choice for invoking PowerShell from PowerShell - especially because all I/O becomes strings and not (deserialized) objects.

Two alternatives:

1. If the user is a local admin and PSRemoting is configured

If a remote session against the local machine (unfortunately restricted to local admins) is a option, I'd definitely go with Invoke-Command:

$strings = Invoke-Command -FilePath C:\...\script1.ps1 -ComputerName localhost -Credential $credential

$strings will contain the results.


2. If the user is not an admin on the target system

You can write your own "local-only Invoke-Command" by spinning up an out-of-process runspace by:

  1. Creating a PowerShellProcessInstance, under a different login
  2. Creating a runspace in said process
  3. Execute your code in said out-of-process runspace

I put together such a function below, see inline comments for a walk-through:

function Invoke-RunAs
{
    [CmdletBinding()]
    param(
        [Alias('PSPath')]
        [ValidateScript({Test-Path $_ -PathType Leaf})]
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        ${FilePath},

        [Parameter(Mandatory = $true)]
        [pscredential]
        [System.Management.Automation.CredentialAttribute()]
        ${Credential},

        [Alias('Args')]
        [Parameter(ValueFromRemainingArguments = $true)]
        [System.Object[]]
        ${ArgumentList},

        [Parameter(Position = 1)]
        [System.Collections.IDictionary]
        $NamedArguments
    )

    begin
    {
        # First we set up a separate managed powershell process
        Write-Verbose "Creating PowerShellProcessInstance and runspace"
        $ProcessInstance = [System.Management.Automation.Runspaces.PowerShellProcessInstance]::new($PSVersionTable.PSVersion, $Credential, $null, $false)

        # And then we create a new runspace in said process
        $Runspace = [runspacefactory]::CreateOutOfProcessRunspace($null, $ProcessInstance)
        $Runspace.Open()
        Write-Verbose "Runspace state is $($Runspace.RunspaceStateInfo)"
    }

    process
    {
        foreach($path in $FilePath){
            Write-Verbose "In process block, Path:'$path'"
            try{
                # Add script file to the code we'll be running
                $powershell = [powershell]::Create([initialsessionstate]::CreateDefault2()).AddCommand((Resolve-Path $path).ProviderPath, $true)

                # Add named param args, if any
                if($PSBoundParameters.ContainsKey('NamedArguments')){
                    Write-Verbose "Adding named arguments to script"
                    $powershell = $powershell.AddParameters($NamedArguments)
                }

                # Add argument list values if present
                if($PSBoundParameters.ContainsKey('ArgumentList')){
                    Write-Verbose "Adding unnamed arguments to script"
                    foreach($arg in $ArgumentList){
                        $powershell = $powershell.AddArgument($arg)
                    }
                }

                # Attach to out-of-process runspace
                $powershell.Runspace = $Runspace

                # Invoke, let output bubble up to caller
                $powershell.Invoke()

                if($powershell.HadErrors){
                    foreach($e in $powershell.Streams.Error){
                        Write-Error $e
                    }
                }
            }
            finally{
                # clean up
                if($powershell -is [IDisposable]){
                    $powershell.Dispose()
                }
            }
        }
    }

    end
    {
        foreach($target in $ProcessInstance,$Runspace){
            # clean up
            if($target -is [IDisposable]){
                $target.Dispose()
            }
        }
    }
}

Then use like so:

$output = Invoke-RunAs -FilePath C:\path\to\script1.ps1 -Credential $targetUser -NamedArguments @{ClientDevice = "ClientName"}
Stonebroke answered 27/3, 2020 at 16:36 Comment(0)
T
2

rcv.ps1

param(
    $username,
    $password
)

"The user is:  $username"
"My super secret password is:  $password"

execution from another script:

.\rcv.ps1 'user' 'supersecretpassword'

output:

The user is:  user
My super secret password is:  supersecretpassword
Tenter answered 27/3, 2020 at 15:48 Comment(2)
To clarify: the intent is not just to pass credentials, but to run as the user identified by the credentials.Applicant
@mklement0, thanks for the clarification because that wasn't clear at all to me by the different iterations of the question being asked.Tenter
A
2

Note:

  • The following solution works with any external program, and captures output invariably as text.

  • To invoke another PowerShell instance and capture its output as rich objects (with limitations), see the variant solution in the bottom section or consider Mathias R. Jessen's helpful answer, which uses the PowerShell SDK.

Here's a proof-of-concept based on direct use of the System.Diagnostics.Process and System.Diagnostics.ProcessStartInfo .NET types to capture process output in memory (as stated in your question, Start-Process is not an option, because it only supports capturing output in files, as shown in this answer):

Note:

  • Due to running as a different user, this is supported on Windows only (as of .NET Core 3.1), but in both PowerShell editions there.

  • Due to needing to run as a different user and needing to capture output, .WindowStyle cannot be used to run the command hidden (because using .WindowStyle requires .UseShellExecute to be $true, which is incompatible with these requirements); however, since all output is being captured, setting .CreateNoNewWindow to $true effectively results in hidden execution.

  • Only stdout output is captured below. If you want to capture stderr output too, you'll need to capture it via events, because use of $ps.StandardError.ReadToEnd() alongside $ps.StandardOutput.ReadToEnd() could lead to deadlocks.

# Get the target user's name and password.
$cred = Get-Credential

# Create a ProcessStartInfo instance
# with the relevant properties.
$psi = [System.Diagnostics.ProcessStartInfo] @{
  # For demo purposes, use a simple `cmd.exe` command that echoes the username. 
  # See the bottom section for a call to `powershell.exe`.
  FileName = 'cmd.exe'
  Arguments = '/c echo %USERNAME%'
  # Set this to a directory that the target user
  # is permitted to access.
  WorkingDirectory = 'C:\'                                                                   #'
  # Ask that output be captured in the
  # .StandardOutput / .StandardError properties of
  # the Process object created later.
  UseShellExecute = $false # must be $false
  RedirectStandardOutput = $true
  RedirectStandardError = $true
  # Uncomment this line if you want the process to run effectively hidden.
  #   CreateNoNewWindow = $true
  # Specify the user identity.
  # Note: If you specify a UPN in .UserName
  # ([email protected]), set .Domain to $null
  Domain = $env:USERDOMAIN
  UserName = $cred.UserName
  Password = $cred.Password
}

# Create (launch) the process...
$ps = [System.Diagnostics.Process]::Start($psi)

# Read the captured standard output.
# By reading to the *end*, this implicitly waits for (near) termination
# of the process.
# Do NOT use $ps.WaitForExit() first, as that can result in a deadlock.
$stdout = $ps.StandardOutput.ReadToEnd()

# Uncomment the following lines to report the process' exit code.
#   $ps.WaitForExit()
#   "Process exit code: $($ps.ExitCode)"

"Running ``cmd /c echo %USERNAME%`` as user $($cred.UserName) yielded:"
$stdout

The above yields something like the following, showing that the process successfully ran with the given user identity:

Running `cmd /c echo %USERNAME%` as user jdoe yielded:
jdoe

Since you're calling another PowerShell instance, you may want to take advantage of the PowerShell CLI's ability to represent output in CLIXML format, which allows deserializing the output into rich objects, albeit with limited type fidelity, as explained in this related answer.

# Get the target user's name and password.
$cred = Get-Credential

# Create a ProcessStartInfo instance
# with the relevant properties.
$psi = [System.Diagnostics.ProcessStartInfo] @{
  # Invoke the PowerShell CLI with a simple sample command
  # that calls `Get-Date` to output the current date as a [datetime] instance.
  FileName = 'powershell.exe'
  # `-of xml` asks that the output be returned as CLIXML,
  # a serialization format that allows deserialization into
  # rich objects.
  Arguments = '-of xml -noprofile -c Get-Date'
  # Set this to a directory that the target user
  # is permitted to access.
  WorkingDirectory = 'C:\'                                                                   #'
  # Ask that output be captured in the
  # .StandardOutput / .StandardError properties of
  # the Process object created later.
  UseShellExecute = $false # must be $false
  RedirectStandardOutput = $true
  RedirectStandardError = $true
  # Uncomment this line if you want the process to run effectively hidden.
  #   CreateNoNewWindow = $true
  # Specify the user identity.
  # Note: If you specify a UPN in .UserName
  # ([email protected]), set .Domain to $null
  Domain = $env:USERDOMAIN
  UserName = $cred.UserName
  Password = $cred.Password
}

# Create (launch) the process...
$ps = [System.Diagnostics.Process]::Start($psi)

# Read the captured standard output, in CLIXML format,
# stripping the `#` comment line at the top (`#< CLIXML`)
# which the deserializer doesn't know how to handle.
$stdoutCliXml = $ps.StandardOutput.ReadToEnd() -replace '^#.*\r?\n'

# Uncomment the following lines to report the process' exit code.
#   $ps.WaitForExit()
#   "Process exit code: $($ps.ExitCode)"

# Use PowerShell's deserialization API to 
# "rehydrate" the objects.
$stdoutObjects = [Management.Automation.PSSerializer]::Deserialize($stdoutCliXml)

"Running ``Get-Date`` as user $($cred.UserName) yielded:"
$stdoutObjects
"`nas data type:"
$stdoutObjects.GetType().FullName

The above outputs something like the following, showing that the [datetime] instance (System.DateTime) output by Get-Date was deserialized as such:

Running `Get-Date` as user jdoe yielded:

Friday, March 27, 2020 6:26:49 PM

as data type:
System.DateTime
Applicant answered 27/3, 2020 at 16:14 Comment(0)
P
0

What you can do it the following to pass a parameter to a ps1 script.

The first script can be a origin.ps1 where we write:

& C:\scripts\dest.ps1 Pa$$w0rd parameter_a parameter_n

Th destination script dest.ps1 can have the following code to capture the variables

$var0 = $args[0]
$var1 = $args[1]
$var2 = $args[2]
Write-Host "my args",$var0,",",$var1,",",$var2

And the result will be

my args Pa$$w0rd, parameter_a, parameter_n
Paella answered 27/3, 2020 at 15:57 Comment(7)
The main goal is to combine all conditions into 1 execution. I have to pass parameters and to pass credentials!Cole
What do you mean with "combine all conditions into 1 execution". I dont think you can add a parameter with the "-" symbol like you did.. I think you have to reformat the strings on the destination scriptPaella
I must execute some PS1 file with parameters and pass -Credential $credentials parameter to this execution and get output from it into a variable. The ps1. Script im executing is throwing a singe word string at the end. Just look at the way I did it with Start-process but this function doesn't generate outputCole
I think powershell doesn't allow you to pass a parameter like this $arguments = "C:\..\script1.ps1" + " -ClientName" + $DeviceName. You should probably think of deleting the "-"Paella
that beeing said. Start-Process does execute the script with parameters and credentials, bit it doesn't save this output into a variable. If im trying to access $output variable, it's NULL. The other Idea that came from @Applicant is to save output to a file. But in my case it will cause a huge amount of files in one place. All created from different users with different scriptsCole
@AndyMcRae, "C:\..\script1.ps1" + " -ClientName " + $DeviceName constructs a string representation of a PowerShell command that is passed as the only value to the -ArgumentList parameter of Start-Process, which passes it to powershell.exe in the new process, which works just fine (assuming the script path has no embedded spaces). Dmytro's challenge is to run a script_as a different user_ - hence Start-Process -Credential - while collecting output from that script in memory rather than in files (which -RedirectStandardOutput and -RedirectStandardError would allow you to do).Applicant
Ok now I get the point of that question.. I guess it has been answered though.Paella

© 2022 - 2024 — McMap. All rights reserved.