feat: Add winexec helper for running PowerShell from the agent
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
// Package winexec runs PowerShell on Windows hosts.
|
||||
//
|
||||
// It exists because three subsystems — updates, workload collection and
|
||||
// workload logs — all need the same invocation, and because getting a
|
||||
// multi-line script past Go quoting, cmd.exe quoting and PowerShell's own
|
||||
// parser is a problem worth solving once.
|
||||
package winexec
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// EncodeCommand renders a script for powershell.exe -EncodedCommand: UTF-16LE,
|
||||
// no byte-order mark, base64. This is deliberately free of build tags so it is
|
||||
// tested on a Linux development machine like every other pure function here.
|
||||
func EncodeCommand(script string) string {
|
||||
units := utf16.Encode([]rune(script))
|
||||
b := make([]byte, 0, len(units)*2)
|
||||
for _, u := range units {
|
||||
b = append(b, byte(u), byte(u>>8))
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package winexec
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEncodeCommand(t *testing.T) {
|
||||
// "hi" as UTF-16LE is 68 00 69 00, which base64-encodes to aABpAA==.
|
||||
if got := EncodeCommand("hi"); got != "aABpAA==" {
|
||||
t.Fatalf("EncodeCommand(hi) = %q, want aABpAA==", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeCommandMultiline(t *testing.T) {
|
||||
// Only that it round-trips through the same encoding PowerShell expects:
|
||||
// every ASCII byte followed by a zero byte, no BOM.
|
||||
got := EncodeCommand("a\nb")
|
||||
want := "YQAKAGIA"
|
||||
if got != want {
|
||||
t.Fatalf("EncodeCommand = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package winexec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Run executes a PowerShell script and returns its stdout.
|
||||
//
|
||||
// powershell.exe rather than pwsh: everything this agent runs through here
|
||||
// touches Windows Update COM or CIM, both of which are most reliable under
|
||||
// Windows PowerShell 5.1, and 5.1 is present on every supported Windows while
|
||||
// pwsh is an optional install.
|
||||
func Run(ctx context.Context, script string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, "powershell.exe",
|
||||
"-NoProfile", "-NonInteractive", "-EncodedCommand", EncodeCommand(script))
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 {
|
||||
return "", fmt.Errorf("powershell: %s", strings.TrimSpace(string(ee.Stderr)))
|
||||
}
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return "", fmt.Errorf("powershell: timed out")
|
||||
}
|
||||
return "", fmt.Errorf("powershell: %w", err)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
Reference in New Issue
Block a user