diff --git a/cmd/launcher/console_other.go b/cmd/launcher/console_other.go new file mode 100644 index 000000000..52bae0824 --- /dev/null +++ b/cmd/launcher/console_other.go @@ -0,0 +1,12 @@ +//go:build !windows +// +build !windows + +package main + +func attachConsole() error { + return nil +} + +func detachConsole() error { + return nil +} diff --git a/cmd/launcher/console_windows.go b/cmd/launcher/console_windows.go new file mode 100644 index 000000000..8c89edcfb --- /dev/null +++ b/cmd/launcher/console_windows.go @@ -0,0 +1,64 @@ +//go:build windows +// +build windows + +package main + +import ( + "fmt" + "os" + "syscall" +) + +// attachConsole ensures that subsequent output from the process will be +// printed to the user's terminal. +func attachConsole() error { + kernel32 := syscall.NewLazyDLL("kernel32.dll") + attachConsoleProc := kernel32.NewProc("AttachConsole") + + // Call AttachConsole, using the console of the parent of the current process + // See: https://learn.microsoft.com/en-us/windows/console/attachconsole + r1, _, err := attachConsoleProc.Call(^uintptr(0)) + if r1 == 0 { + return fmt.Errorf("could not call AttachConsole: %w", err) + } + + // Set stdout for newly attached console + stdout, err := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE) + if err != nil { + return fmt.Errorf("getting stdout handle: %w", err) + } + os.Stdout = os.NewFile(uintptr(stdout), "stdout") + + // Set stderr for newly attached console + stderr, err := syscall.GetStdHandle(syscall.STD_ERROR_HANDLE) + if err != nil { + return fmt.Errorf("getting stderr handle: %w", err) + } + os.Stderr = os.NewFile(uintptr(stderr), "stderr") + + // Print an empty line so that our first line of actual output doesn't occur on the same line + // as the command prompt + fmt.Println("") + + return nil +} + +// detachConsole undos a previous call to attachConsole. It will leave the window +// appearing to hang, so it notifies the user to press enter in order to get +// their command prompt back. +func detachConsole() error { + // Let the user know they have to press enter to get their prompt back + fmt.Println("Press enter to return to your terminal") + + // Now, free the console + kernel32 := syscall.NewLazyDLL("kernel32.dll") + freeConsoleProc := kernel32.NewProc("FreeConsole") + + // See: https://learn.microsoft.com/en-us/windows/console/freeconsole + r1, _, err := freeConsoleProc.Call() + if r1 == 0 { + return fmt.Errorf("could not call FreeConsole: %w", err) + } + + return nil +} diff --git a/cmd/launcher/doctor.go b/cmd/launcher/doctor.go index f8fe0dd49..a028a9eb6 100644 --- a/cmd/launcher/doctor.go +++ b/cmd/launcher/doctor.go @@ -13,6 +13,9 @@ import ( ) func runDoctor(args []string) error { + attachConsole() + defer detachConsole() + // Doctor assumes a launcher installation (at least partially) exists // Overriding some of the default values allows options to be parsed making this assumption launcher.DefaultAutoupdate = true diff --git a/cmd/launcher/flare.go b/cmd/launcher/flare.go index 48586c73d..4e29eae12 100644 --- a/cmd/launcher/flare.go +++ b/cmd/launcher/flare.go @@ -23,6 +23,9 @@ import ( // runFlare is a command that runs the flare checkup and saves the results locally or uploads them to a server. func runFlare(args []string) error { + attachConsole() + defer detachConsole() + // Flare assumes a launcher installation (at least partially) exists // Overriding some of the default values allows options to be parsed making this assumption // TODO this stuff needs some deeper thinking diff --git a/cmd/launcher/main.go b/cmd/launcher/main.go index 47a592c7f..df7222fec 100644 --- a/cmd/launcher/main.go +++ b/cmd/launcher/main.go @@ -261,7 +261,10 @@ func commandUsage(fs *flag.FlagSet, short string) func() { } func runVersion(args []string) error { + attachConsole() version.PrintFull() + detachConsole() + os.Exit(0) return nil } diff --git a/cmd/launcher/svc_windows.go b/cmd/launcher/svc_windows.go index eb9f0afbc..1fa4a6b96 100644 --- a/cmd/launcher/svc_windows.go +++ b/cmd/launcher/svc_windows.go @@ -144,11 +144,25 @@ func runWindowsSvc(args []string) error { } func runWindowsSvcForeground(args []string) error { + attachConsole() + defer detachConsole() + // Foreground mode is inherently a debug mode. So we start the // logger in debugging mode, instead of looking at opts.debug logger := logutil.NewCLILogger(true) level.Debug(logger).Log("msg", "foreground service start requested (debug mode)") + // Use new logger to write logs to stdout + systemSlogger := new(multislogger.MultiSlogger) + localSlogger := new(multislogger.MultiSlogger) + + handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelDebug, + }) + localSlogger.AddHandler(handler) + systemSlogger.AddHandler(handler) + opts, err := launcher.ParseOptions("", os.Args[2:]) if err != nil { level.Info(logger).Log("err", err) @@ -161,7 +175,7 @@ func runWindowsSvcForeground(args []string) error { run := debug.Run - return run(serviceName, &winSvc{logger: logger, opts: opts}) + return run(serviceName, &winSvc{logger: logger, slogger: localSlogger, systemSlogger: systemSlogger, opts: opts}) } type winSvc struct { diff --git a/pkg/dataflatten/jwt.go b/pkg/dataflatten/jwt.go new file mode 100644 index 000000000..2125d93bc --- /dev/null +++ b/pkg/dataflatten/jwt.go @@ -0,0 +1,55 @@ +package dataflatten + +import ( + "fmt" + "io" + "os" + + "github.com/golang-jwt/jwt/v5" +) + +// JWTFile adds support for the kolide_jwt table, which allows parsing +// a file containing a JWT. Note that the kolide_jwt table does not handle +// verification - this is a utility table for convenience. +func JWTFile(file string, opts ...FlattenOpts) ([]Row, error) { + return flattenJWT(file, opts...) +} + +func flattenJWT(path string, opts ...FlattenOpts) ([]Row, error) { + // for now, make it clear that any data we parse is unverified + results := map[string]interface{}{"verified": false} + + jwtFH, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("unable to access file: %w", err) + } + + defer jwtFH.Close() + + tokenRaw, err := io.ReadAll(jwtFH) + if err != nil { + return nil, fmt.Errorf("unable to read JWT: %w", err) + } + + // attempt decode into the generic (default) MapClaims struct to ensure we capture + // any claims data that might be useful + token, _, err := new(jwt.Parser).ParseUnverified(string(tokenRaw), jwt.MapClaims{}) + if err != nil { + return nil, fmt.Errorf("unable to parse JWT: %w", err) + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, fmt.Errorf("JWT has no parseable claims") + } + + parsedClaims := map[string]interface{}{} + for k, v := range claims { + parsedClaims[k] = v + } + + results["claims"] = parsedClaims + results["header"] = token.Header + + return Flatten(results, opts...) +} diff --git a/pkg/debug/shipper/shipper.go b/pkg/debug/shipper/shipper.go index 334bd8846..0c39e6f7f 100644 --- a/pkg/debug/shipper/shipper.go +++ b/pkg/debug/shipper/shipper.go @@ -201,8 +201,8 @@ func signHttpRequest(req *http.Request, body []byte) { return } - request.Header.Set(control.HeaderKey, string(pub)) - request.Header.Set(control.HeaderSignature, base64.StdEncoding.EncodeToString(sig)) + request.Header.Set(headerKey, string(pub)) + request.Header.Set(signatureKey, base64.StdEncoding.EncodeToString(sig)) } sign(agent.LocalDbKeys(), control.HeaderKey, control.HeaderSignature, req) diff --git a/pkg/osquery/tables/dataflattentable/tables.go b/pkg/osquery/tables/dataflattentable/tables.go index 5b0ca9415..a2249bce5 100644 --- a/pkg/osquery/tables/dataflattentable/tables.go +++ b/pkg/osquery/tables/dataflattentable/tables.go @@ -20,6 +20,7 @@ const ( PlistType DataSourceType = iota + 1 JsonType JsonlType + JWTType ExecType XmlType IniType @@ -47,6 +48,7 @@ func AllTablePlugins(logger log.Logger) []osquery.OsqueryPlugin { TablePlugin(logger, IniType), TablePlugin(logger, PlistType), TablePlugin(logger, JsonlType), + TablePlugin(logger, JWTType), } } @@ -73,6 +75,9 @@ func TablePlugin(logger log.Logger, dataSourceType DataSourceType) osquery.Osque case IniType: t.flattenFileFunc = dataflatten.IniFile t.tableName = "kolide_ini" + case JWTType: + t.flattenFileFunc = dataflatten.JWTFile + t.tableName = "kolide_jwt" default: panic("Unknown data source type") }