Inicio del script .ps1 desde PowerShell con parámetros y credenciales y obtención de resultados mediante variables

10

Hola comunidad de pila :)

Tengo un objetivo simple. Me gustaría iniciar un script de PowerShell desde otro script de Powershell, pero hay 3 condiciones:

  1. Tengo que pasar las credenciales (la ejecución se conecta a una base de datos que tiene un usuario específico)
  2. Tiene que tomar algunos parámetros
  3. Me gustaría pasar la salida a una variable

Hay una pregunta similar Enlace . Pero la respuesta es usar archivos como una forma de comunicarse entre 2 secuencias de comandos PS. Solo me gustaría evitar conflictos de acceso. @Update: el script principal va a iniciar algunos otros scripts. Por lo tanto, la solución con archivos puede ser complicada si la ejecución se realiza desde varios usuarios al mismo tiempo.

Script1.ps1 es el script que debe tener una cadena como salida. (Para ser claros, es un guión ficticio, el real tiene 150 filas, así que solo quería dar un ejemplo)

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

ExecuteScripts.ps1 debería invocar a aquel con las 3 condiciones mencionadas anteriormente

Intenté múltiples soluciones. Este por ejemplo:

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

No obtengo ningún resultado de eso y no puedo simplemente llamar al script con

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

Porque no puedo pasarle -Credentialparámetros ...

¡Gracias de antemano!

Dmytro
fuente
Si se trata solo de conflictos de acceso: crear nombres de archivo únicos para cada invocación resolvería su problema, ¿verdad?
mklement0
1
@ mklement0 si es la única forma, apilaría esa solución. Simplemente generando nombres de archivo aleatorios, verificando si dicho archivo existe ... Ejecutaré de 6 a 10 scripts de mi código Java y necesitaría de 6 a 10 archivos cada vez que esté usando o alguien más use mi aplicación. Así que también se trata de rendimiento
Dmytro

Respuestas:

2

Nota:

  • La siguiente solución funciona con cualquier programa externo y captura la salida invariablemente como texto .

  • Para invocar otra instancia de PowerShell y capturar su salida como objetos enriquecidos (con limitaciones), vea la solución variante en la sección inferior o considere la útil respuesta de Mathias R. Jessen , que usa el SDK de PowerShell .

Aquí hay una prueba de concepto basada en el uso directo de los tipos System.Diagnostics.Processy System.Diagnostics.ProcessStartInfo.NET para capturar la salida del proceso en la memoria (como se indicó en su pregunta, Start-Processno es una opción, ya que solo admite la captura de salida en archivos , como se muestra en esta respuesta ) :

Nota:

  • Debido a que se ejecuta como un usuario diferente, esto solo es compatible con Windows (a partir de .NET Core 3.1), pero en ambas ediciones de PowerShell.

  • Debido a la necesidad de ejecutarse como un usuario diferente y la necesidad de capturar la salida, .WindowStyleno se puede utilizar para ejecutar el comando oculto (porque el uso .WindowStyledebe .UseShellExecuteser $true, lo que es incompatible con estos requisitos); sin embargo, ya está siendo toda la salida capturado , el establecimiento .CreateNoNewWindowde $trueresultados de eficacia en la ejecución oculto.

# 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

Lo anterior produce algo como lo siguiente, mostrando que el proceso se ejecutó con éxito con la identidad de usuario dada:

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

Como está llamando a otra instancia de PowerShell , es posible que desee aprovechar la capacidad de PowerShell CLI para representar la salida en formato CLIXML, que permite deserializar la salida en objetos ricos , aunque con una fidelidad de tipo limitada , como se explica en esta respuesta relacionada .

# 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

Lo anterior genera algo como lo siguiente, mostrando que la salida de [datetime]instancia ( System.DateTime) Get-Datefue deserializada como tal:

Running `Get-Date` as user jdoe yielded:

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

as data type:
System.DateTime
mklement0
fuente
5

Start-Processsería mi último recurso para invocar PowerShell desde PowerShell, especialmente porque todas las E / S se convierten en cadenas y no en objetos (deserializados).

Dos alternativas:

1. Si el usuario es un administrador local y PSRemoting está configurado

Si una sesión remota contra la máquina local (desafortunadamente restringida a administradores locales) es una opción, definitivamente elegiría Invoke-Command:

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

$strings contendrá los resultados.


2. Si el usuario no es administrador en el sistema de destino

Puede escribir su propio "solo local Invoke-Command" girando un espacio de ejecución fuera de proceso de la siguiente manera:

  1. Crear un PowerShellProcessInstance, bajo un inicio de sesión diferente
  2. Crear un espacio de ejecución en dicho proceso
  3. Ejecute su código en dicho espacio de ejecución fuera de proceso

Reuní dicha función a continuación, vea los comentarios en línea para un recorrido:

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()
            }
        }
    }
}

Luego use así:

$output = Invoke-RunAs -FilePath C:\path\to\script1.ps1 -Credential $targetUser -NamedArguments @{ClientDevice = "ClientName"}
Mathias R. Jessen
fuente
0

rcv.ps1

param(
    $username,
    $password
)

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

ejecución desde otro script:

.\rcv.ps1 'user' 'supersecretpassword'

salida:

The user is:  user
My super secret password is:  supersecretpassword
thepip3r
fuente
1
Tengo que pasar credenciales a esta ejecución ...
Dmytro
actualizado las porciones relevantes.
thepip3r
Para aclarar: la intención no es solo pasar las credenciales, sino ejecutarse como el usuario identificado por las credenciales.
mklement0
1
@ mklement0, gracias por la aclaración porque eso no estaba claro para mí por las diferentes iteraciones de la pregunta que se hizo.
thepip3r
0

Lo que puede hacer es lo siguiente para pasar un parámetro a un script ps1.

El primer script puede ser origin.ps1 donde escribimos:

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

El script de destino dest.ps1 puede tener el siguiente código para capturar las variables

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

Y el resultado será

my args Pa$$w0rd, parameter_a, parameter_n
Andy McRae
fuente
1
El objetivo principal es combinar todas las condiciones en 1 ejecución. ¡Tengo que pasar parámetros y pasar credenciales!
Dmytro
¿Qué quieres decir con "combinar todas las condiciones en 1 ejecución". No creo que pueda agregar un parámetro con el símbolo "-" como lo hizo. Creo que debe volver a formatear las cadenas en el script de destino
Andy McRae
Debo ejecutar algún archivo PS1 con parámetros y pasar -Credential $credentialsparámetros a esta ejecución y obtener resultados de él en una variable. La ps1. La secuencia de comandos que estoy ejecutando es arrojar una cadena de palabra singe al final. Solo mira la forma en que lo hice Start-processpero esta función no genera salida
Dmytro
Creo que powershell no le permite pasar un parámetro como este $ argumentos = "C: \ .. \ script1.ps1" + "-ClientName" + $ DeviceName. Probablemente deberías pensar en eliminar el "-"
Andy McRae
1
dijo ese beeing. Start-Process ejecuta el script con parámetros y credenciales, pero no guarda esta salida en una variable. Si estoy tratando de acceder a la $outputvariable, es NULL. La otra idea que surgió de @ mklement0 es guardar la salida en un archivo. Pero en mi caso causará una gran cantidad de archivos en un solo lugar. Todo creado a partir de diferentes usuarios con diferentes scripts
Dmytro