Skip to content

Commit

Permalink
get runtime enrollment details before osq details (#1833)
Browse files Browse the repository at this point in the history
  • Loading branch information
James-Pickett authored Aug 14, 2024
1 parent a80efbf commit 1b40334
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 53 deletions.
86 changes: 43 additions & 43 deletions pkg/osquery/enrollment_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,62 @@ import (
"github.com/pkg/errors"
)

func getEnrollDetails(ctx context.Context, osquerydPath string) (service.EnrollmentDetails, error) {
// getEnrollDetails returns an EnrollmentDetails struct with populated with details it can fetch without osquery.
// To get the rest of the details, pass the struct to getOsqEnrollDetails.
func getRuntimeEnrollDetails() service.EnrollmentDetails {
details := service.EnrollmentDetails{
OSPlatform: runtime.GOOS,
OSPlatformLike: runtime.GOOS,
LauncherVersion: version.Version().Version,
GOOS: runtime.GOOS,
GOARCH: runtime.GOARCH,
}

// Pull in some launcher key info. These depend on the agent package, and we'll need to check for nils
if agent.LocalDbKeys().Public() != nil {
if key, err := x509.MarshalPKIXPublicKey(agent.LocalDbKeys().Public()); err == nil {
// der is a binary format, so convert to b64
details.LauncherLocalKey = base64.StdEncoding.EncodeToString(key)
}
}

if agent.HardwareKeys().Public() != nil {
if key, err := x509.MarshalPKIXPublicKey(agent.HardwareKeys().Public()); err == nil {
// der is a binary format, so convert to b64
details.LauncherHardwareKey = base64.StdEncoding.EncodeToString(key)
details.LauncherHardwareKeySource = agent.HardwareKeys().Type()
}
}

return details
}

// getOsqEnrollDetails queries osquery for enrollment details and populates the EnrollmentDetails struct.
// It's expected that the caller has initially populated the struct with runtimeEnrollDetails by calling getRuntimeEnrollDetails.
func getOsqEnrollDetails(ctx context.Context, osquerydPath string, details *service.EnrollmentDetails) error {
ctx, span := traces.StartSpan(ctx)
defer span.End()

var details service.EnrollmentDetails

// To facilitate manual testing around missing enrollment details,
// there is a environmental variable to trigger the failure condition
if os.Getenv("LAUNCHER_DEBUG_ENROLL_DETAILS_ERROR") == "true" {
return details, errors.New("Skipping enrollment details")
return errors.New("Skipping enrollment details")
}

// If the binary doesn't exist, bail out early.
if info, err := os.Stat(osquerydPath); os.IsNotExist(err) {
return details, fmt.Errorf("no binary at %s", osquerydPath)
return fmt.Errorf("no binary at %s", osquerydPath)
} else if info.IsDir() {
return details, fmt.Errorf("%s is a directory", osquerydPath)
return fmt.Errorf("%s is a directory", osquerydPath)
} else if err != nil {
return details, fmt.Errorf("statting %s: %w", osquerydPath, err)
return fmt.Errorf("statting %s: %w", osquerydPath, err)
}

query := `
SELECT
osquery_info.version as osquery_version,
os_version.build as os_build,
os_version.name as os_name,
os_version.platform as os_platform,
os_version.platform_like as os_platform_like,
os_version.version as os_version,
system_info.hardware_model,
system_info.hardware_serial,
Expand All @@ -57,7 +85,6 @@ func getEnrollDetails(ctx context.Context, osquerydPath string) (service.Enrollm
os_version,
system_info,
osquery_info;
`

var respBytes bytes.Buffer
Expand All @@ -69,25 +96,25 @@ func getEnrollDetails(ctx context.Context, osquerydPath string) (service.Enrollm
runsimple.WithStderr(&stderrBytes),
)
if err != nil {
return details, fmt.Errorf("create osquery for enrollment details: %w", err)
return fmt.Errorf("create osquery for enrollment details: %w", err)
}

osqCtx, osqCancel := context.WithTimeout(ctx, 5*time.Second)
defer osqCancel()

if sqlErr := osq.RunSql(osqCtx, []byte(query)); osqCtx.Err() != nil {
return details, fmt.Errorf("query enrollment details context error: %w: stderr: %s", osqCtx.Err(), stderrBytes.String())
return fmt.Errorf("query enrollment details context error: %w: stderr: %s", osqCtx.Err(), stderrBytes.String())
} else if sqlErr != nil {
return details, fmt.Errorf("query enrollment details: %w; stderr: %s", sqlErr, stderrBytes.String())
return fmt.Errorf("query enrollment details: %w; stderr: %s", sqlErr, stderrBytes.String())
}

var resp []map[string]string
if err := json.Unmarshal(respBytes.Bytes(), &resp); err != nil {
return details, fmt.Errorf("json decode enrollment details: %w; stderr: %s", err, stderrBytes.String())
return fmt.Errorf("json decode enrollment details: %w; stderr: %s", err, stderrBytes.String())
}

if len(resp) < 1 {
return details, fmt.Errorf("expected at least one row from the enrollment details query: stderr: %s", stderrBytes.String())
return fmt.Errorf("expected at least one row from the enrollment details query: stderr: %s", stderrBytes.String())
}

if val, ok := resp[0]["os_version"]; ok {
Expand All @@ -99,12 +126,6 @@ func getEnrollDetails(ctx context.Context, osquerydPath string) (service.Enrollm
if val, ok := resp[0]["os_name"]; ok {
details.OSName = val
}
if val, ok := resp[0]["os_platform"]; ok {
details.OSPlatform = val
}
if val, ok := resp[0]["os_platform_like"]; ok {
details.OSPlatformLike = val
}
if val, ok := resp[0]["osquery_version"]; ok {
details.OsqueryVersion = val
}
Expand All @@ -122,26 +143,5 @@ func getEnrollDetails(ctx context.Context, osquerydPath string) (service.Enrollm
details.HardwareUUID = val
}

// This runs before the extensions are registered. These mirror the
// underlying tables.
details.LauncherVersion = version.Version().Version
details.GOOS = runtime.GOOS
details.GOARCH = runtime.GOARCH

// Pull in some launcher key info. These depend on the agent package, and we'll need to check for nils
if agent.LocalDbKeys().Public() != nil {
if key, err := x509.MarshalPKIXPublicKey(agent.LocalDbKeys().Public()); err == nil {
// der is a binary format, so convert to b64
details.LauncherLocalKey = base64.StdEncoding.EncodeToString(key)
}
}
if agent.HardwareKeys().Public() != nil {
if key, err := x509.MarshalPKIXPublicKey(agent.HardwareKeys().Public()); err == nil {
// der is a binary format, so convert to b64
details.LauncherHardwareKey = base64.StdEncoding.EncodeToString(key)
details.LauncherHardwareKeySource = agent.HardwareKeys().Type()
}
}

return details, nil
return nil
}
11 changes: 8 additions & 3 deletions pkg/osquery/enrollment_details_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,31 @@ import (
"path/filepath"
"testing"

"github.com/kolide/launcher/pkg/service"
"github.com/stretchr/testify/require"
)

func Test_getEnrollDetails_binaryNotExist(t *testing.T) {
t.Parallel()

_, err1 := getEnrollDetails(context.TODO(), filepath.Join("some", "fake", "path", "to", "osqueryd"))
var details service.EnrollmentDetails

err1 := getOsqEnrollDetails(context.TODO(), filepath.Join("some", "fake", "path", "to", "osqueryd"), &details)
require.Error(t, err1, "expected error when path does not exist")

_, err2 := getEnrollDetails(context.TODO(), t.TempDir())
err2 := getOsqEnrollDetails(context.TODO(), t.TempDir(), &details)
require.Error(t, err2, "expected error when path is directory")
}

func Test_getEnrollDetails_executionError(t *testing.T) {
t.Parallel()

var details service.EnrollmentDetails

currentExecutable, err := os.Executable()
require.NoError(t, err, "could not get current executable for test")

// We expect getEnrollDetails to fail when called against an executable that is not osquery
_, err = getEnrollDetails(context.TODO(), currentExecutable)
err = getOsqEnrollDetails(context.TODO(), currentExecutable, &details)
require.Error(t, err, "should not have been able to get enroll details with non-osqueryd executable")
}
14 changes: 7 additions & 7 deletions pkg/osquery/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,32 +417,32 @@ func (e *Extension) Enroll(ctx context.Context) (string, bool, error) {

// We used to see the enrollment details fail, but now that we're running as an exec,
// it seems less likely. Try a couple times, but backoff fast.
var enrollDetails service.EnrollmentDetails
enrollDetails := getRuntimeEnrollDetails()
if osqPath := e.knapsack.LatestOsquerydPath(ctx); osqPath == "" {
e.slogger.Log(ctx, slog.LevelInfo,
"skipping enrollment details, no osqueryd path, this is probably CI",
"skipping enrollment osquery details, no osqueryd path, this is probably CI",
)
span.AddEvent("skipping_enrollment_details")
} else {
if err := backoff.WaitFor(func() error {
enrollDetails, err = getEnrollDetails(ctx, osqPath)
err = getOsqEnrollDetails(ctx, osqPath, &enrollDetails)
if err != nil {
e.slogger.Log(ctx, slog.LevelDebug,
"getEnrollDetails failed in backoff",
"getOsqEnrollDetails failed in backoff",
"err", err,
)
}
return err
}, 30*time.Second, 5*time.Second); err != nil {
if os.Getenv("LAUNCHER_DEBUG_ENROLL_DETAILS_REQUIRED") == "true" {
return "", true, fmt.Errorf("query enrollment details: %w", err)
return "", true, fmt.Errorf("query osq enrollment details: %w", err)
}

e.slogger.Log(ctx, slog.LevelError,
"failed to get enrollment details with retries, moving on",
"failed to get osq enrollment details with retries, moving on",
"err", err,
)
traces.SetError(span, fmt.Errorf("query enrollment details: %w", err))
traces.SetError(span, fmt.Errorf("query osq enrollment details: %w", err))
} else {
span.AddEvent("got_enrollment_details")
}
Expand Down

0 comments on commit 1b40334

Please sign in to comment.