Iniciando o script .ps1 do PowerShell com parâmetros e credenciais e obtendo a saída usando a variável

10

Comunidade Hello Stack :)

Eu tenho um objetivo simples. Gostaria de iniciar um script do PowerShell a partir de outro script do PowerShell, mas existem três condições:

  1. Eu tenho que passar credenciais (a execução se conecta a um banco de dados que possui usuário específico)
  2. Tem que levar alguns parâmetros
  3. Eu gostaria de passar a saída para uma variável

Existe uma pergunta semelhante Link . Mas a resposta é usar arquivos como uma maneira de se comunicar entre scripts de 2 PS. Eu só gostaria de evitar conflitos de acesso. @ Update: O script principal vai iniciar alguns outros scripts. portanto, a solução com arquivos pode ser complicada, se a execução for executada por vários usuários ao mesmo tempo.

Script1.ps1 é o script que deve ter string como saída. (Só para deixar claro, é um script fictício, o real tem 150 linhas, então eu só queria dar um exemplo)

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

ExecuteScripts.ps1 deve chamar aquele com as três condições mencionadas acima

Eu tentei várias soluções. Este por exemplo:

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

Eu não recebo nenhuma saída disso e não posso simplesmente chamar o script com

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

Porque eu não posso passar -Credentialparâmetro para ele ..

Agradeço antecipadamente!

Dmytro
fonte
Se se trata apenas de conflitos de acesso: criar nomes de arquivos exclusivos para cada chamada resolveria seu problema, certo?
mklement0 27/03
11
@ mklement0 se for a única maneira, eu empilharia com essa solução. Apenas gerando nomes de arquivos aleatórios, verificando se esse arquivo existe ... Eu executarei de 6 a 10 scripts no meu Código Java e precisaria de 6 a 10 arquivos toda vez que estiver usando ou alguém usar meu aplicativo. Então é sobre desempenho também
Dmytro

Respostas:

2

Nota:

  • A solução a seguir funciona com qualquer programa externo e captura a saída invariavelmente como texto .

  • Para invocar outra instância do PowerShell e capturar sua saída como objetos ricos (com limitações), consulte a solução variante na seção inferior ou considere a resposta útil de Mathias R. Jessen , que usa o PowerShell SDK .

A seguir, uma prova de conceito baseada no uso direto dos tipos .NET System.Diagnostics.Processe System.Diagnostics.ProcessStartInfopara capturar a saída do processo na memória (conforme declarado na sua pergunta, Start-Processnão é uma opção, pois suporta apenas a captura de saída nos arquivos , conforme mostrado nesta resposta ) :

Nota:

  • Devido à execução como um usuário diferente, isso é suportado apenas no Windows (a partir do .NET Core 3.1), mas nas duas edições do PowerShell.

  • Devido à necessidade de executar como um usuário diferente e à captura de saída, .WindowStylenão pode ser usado para executar o comando oculto (porque o uso .WindowStyleexige .UseShellExecuteque seja $true, o que é incompatível com esses requisitos); No entanto, uma vez que toda a saída está sendo capturado , definição .CreateNoNewWindowde $trueresultados de forma eficaz na execução escondido.

# 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

O exemplo acima produz algo como o seguinte, mostrando que o processo foi executado com sucesso com a identidade de usuário fornecida:

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

Como você está chamando outra instância do PowerShell , convém aproveitar a capacidade da CLI do PowerShell para representar a saída no formato CLIXML, que permite desserializar a saída em objetos ricos , embora com fidelidade de tipo limitada , conforme explicado nesta resposta 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

O resultado acima é semelhante ao seguinte, mostrando que a [datetime]instância ( System.DateTime) gerada por Get-Datefoi desserializada da seguinte maneira:

Running `Get-Date` as user jdoe yielded:

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

as data type:
System.DateTime
mklement0
fonte
5

Start-Processseria minha última opção de recurso para chamar o PowerShell do PowerShell - especialmente porque todas as E / S se tornam cadeias de caracteres e não objetos (desserializados).

Duas alternativas:

1. Se o usuário for um administrador local e o PSRemoting estiver configurado

Se uma sessão remota na máquina local (infelizmente restrita a administradores locais) for uma opção, eu definitivamente aceitaria Invoke-Command:

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

$strings conterá os resultados.


2. Se o usuário não for um administrador no sistema de destino

Você pode escrever seu próprio "somente local Invoke-Command" girando um espaço de execução fora de processo:

  1. Criando um PowerShellProcessInstance, com um login diferente
  2. Criando um espaço de execução no referido processo
  3. Execute seu código no espaço de execução fora de processo

Eu montei essa função abaixo, veja comentários embutidos para uma explicação passo a passo:

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

Então use assim:

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

rcv.ps1

param(
    $username,
    $password
)

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

execução de outro script:

.\rcv.ps1 'user' 'supersecretpassword'

resultado:

The user is:  user
My super secret password is:  supersecretpassword
thepip3r
fonte
11
Eu tenho que passar credentails para esta expressão ...
Dmytro
atualizou as partes relevantes.
thepip3r 27/03
Para esclarecer: a intenção não é apenas passar credenciais, mas executar como o usuário identificado pelas credenciais.
mklement0 28/03
11
@ mklement0, obrigado pelo esclarecimento, porque isso não ficou claro para mim pelas diferentes iterações da pergunta que está sendo feita.
thepip3r 31/03
0

O que você pode fazer da seguinte maneira para passar um parâmetro para um script ps1.

O primeiro script pode ser um origin.ps1 onde escrevemos:

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

O script de destino dest.ps1 pode ter o seguinte código para capturar as variáveis

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

E o resultado será

my args Pa$$w0rd, parameter_a, parameter_n
Andy McRae
fonte
11
O objetivo principal é combinar todas as condições em 1 execução. Eu tenho que passar parâmetros e credenciais!
Dmytro
O que você quer dizer com "combinar todas as condições em 1 execução". Eu não acho que você pode adicionar um parâmetro com o símbolo "-" como você fez. Acho que você precisa reformatar as strings no script de destino
Andy McRae
Devo executar algum arquivo PS1 com parâmetros e passar -Credential $credentialsparâmetros para esta execução e obter a saída dele em uma variável. O ps1. O script em execução está lançando uma sequência de palavras simples no final. Basta olhar para o jeito que eu fiz isso, Start-processmas essa função não gera saída
Dmytro
Eu acho que o PowerShell não permite que você passe um parâmetro como este $ argumentos = "C: \ .. \ script1.ps1" + "-ClientName" + $ DeviceName. Você provavelmente deve pensar em excluir o "-"
Andy McRae
11
dito isso. O Start-Process executa o script com parâmetros e credenciais, mas não salva essa saída em uma variável. Se estou tentando acessar a $outputvariável, é NULL. A outra idéia que veio de @ mklement0 é salvar a saída em um arquivo. Mas no meu caso, isso causará uma enorme quantidade de arquivos em um só lugar. Tudo criado a partir de diferentes usuários com diferentes scripts
Dmytro