diff --git a/pkg/osquery/table/platform_tables_darwin.go b/pkg/osquery/table/platform_tables_darwin.go index fc2a74100..7071157eb 100644 --- a/pkg/osquery/table/platform_tables_darwin.go +++ b/pkg/osquery/table/platform_tables_darwin.go @@ -10,6 +10,7 @@ import ( "github.com/kolide/launcher/pkg/osquery/tables/firmwarepasswd" "github.com/kolide/launcher/pkg/osquery/tables/ioreg" "github.com/kolide/launcher/pkg/osquery/tables/munki" + "github.com/kolide/launcher/pkg/osquery/tables/profiles" "github.com/kolide/launcher/pkg/osquery/tables/pwpolicy" "github.com/kolide/launcher/pkg/osquery/tables/screenlock" "github.com/kolide/launcher/pkg/osquery/tables/systemprofiler" @@ -37,6 +38,7 @@ func platformTables(client *osquery.ExtensionManagerClient, logger log.Logger, c TouchIDSystemConfig(client, logger), UserAvatar(logger), ioreg.TablePlugin(client, logger), + profiles.TablePlugin(client, logger), kextpolicy.TablePlugin(), legacyexec.TablePlugin(), dataflattentable.TablePlugin(client, logger, dataflattentable.PlistType), diff --git a/pkg/osquery/tables/profiles/profiles.go b/pkg/osquery/tables/profiles/profiles.go new file mode 100644 index 000000000..aeed0ba95 --- /dev/null +++ b/pkg/osquery/tables/profiles/profiles.go @@ -0,0 +1,168 @@ +//+build darwin + +// Package profiles provides a table wrapper around the various +// profiles options. +// +// As the returned data is a complex nested plist, this uses the +// dataflatten tooling. (See +// https://godoc.org/github.com/kolide/launcher/pkg/dataflatten) + +package profiles + +import ( + "bytes" + "context" + "os/exec" + "strings" + "time" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/dataflatten" + "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" + "github.com/kolide/osquery-go" + "github.com/kolide/osquery-go/plugin/table" + "github.com/pkg/errors" +) + +const profilesPath = "/usr/bin/profiles" + +const userAllowedCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" + +type Table struct { + client *osquery.ExtensionManagerClient + logger log.Logger + tableName string +} + +func TablePlugin(client *osquery.ExtensionManagerClient, logger log.Logger) *table.Plugin { + + columns := []table.ColumnDefinition{ + table.TextColumn("fullkey"), + table.TextColumn("parent"), + table.TextColumn("key"), + table.TextColumn("value"), + table.TextColumn("query"), + + // profiles options. See `man profiles`. These may not be needed, + // we use `show -all` as the default, and it probably covers + // everything. + table.TextColumn("user"), + table.TextColumn("command"), + table.TextColumn("type"), + } + + t := &Table{ + client: client, + logger: logger, + tableName: "kolide_profiles", + } + + return table.NewPlugin(t.tableName, columns, t.generate) +} + +func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { + var results []map[string]string + + for _, command := range tablehelpers.GetConstraints(queryContext, "command", tablehelpers.WithAllowedCharacters("abcdefghijklmnopqrstuvwxyz"), tablehelpers.WithDefaults("show")) { + for _, profileType := range tablehelpers.GetConstraints(queryContext, "type", tablehelpers.WithAllowedCharacters("abcdefghijklmnopqrstuvwxyz"), tablehelpers.WithDefaults("")) { + for _, user := range tablehelpers.GetConstraints(queryContext, "user", tablehelpers.WithAllowedCharacters(userAllowedCharacters), tablehelpers.WithDefaults("_all")) { + for _, dataQuery := range tablehelpers.GetConstraints(queryContext, "query", tablehelpers.WithDefaults("")) { + + profileArgs := []string{command, "-output", "stdout-xml"} + + if profileType != "" { + profileArgs = append(profileArgs, "-type", profileType) + } + + // setup the command line. This table overloads the `user` + // column so one can select either: + // * All profiles merged, using the special value `_all` (this is the default) + // * The device profiles, using the special value `_device` + // * a user specific one, using the username + switch { + case user == "" || user == "_all": + profileArgs = append(profileArgs, "-all") + case user == "_device": + break + case user != "": + profileArgs = append(profileArgs, "-user", user) + default: + return nil, errors.Errorf("Unknown user argument: %s", user) + } + + profilesOutput, err := t.execProfiles(ctx, profileArgs) + if err != nil { + level.Info(t.logger).Log("msg", "exec failed", "err", err) + continue + } + + flatData, err := t.flattenOutput(dataQuery, profilesOutput) + if err != nil { + level.Info(t.logger).Log("msg", "flatten failed", "err", err) + continue + } + + for _, row := range flatData { + p, k := row.ParentKey("/") + + res := map[string]string{ + "fullkey": row.StringPath("/"), + "parent": p, + "key": k, + "value": row.Value, + "query": dataQuery, + + "command": command, + "type": profileType, + "user": user, + } + results = append(results, res) + } + } + } + } + } + return results, nil +} + +func (t *Table) flattenOutput(dataQuery string, systemOutput []byte) ([]dataflatten.Row, error) { + flattenOpts := []dataflatten.FlattenOpts{} + + if dataQuery != "" { + flattenOpts = append(flattenOpts, dataflatten.WithQuery(strings.Split(dataQuery, "/"))) + } + + if t.logger != nil { + flattenOpts = append(flattenOpts, + dataflatten.WithLogger(level.NewFilter(t.logger, level.AllowInfo())), + ) + } + + return dataflatten.Plist(systemOutput, flattenOpts...) +} + +func (t *Table) execProfiles(ctx context.Context, args []string) ([]byte, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, profilesPath, args...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + level.Debug(t.logger).Log("msg", "calling profiles", "args", cmd.Args) + + if err := cmd.Run(); err != nil { + return nil, errors.Wrapf(err, "calling profiles. Got: %s", string(stderr.Bytes())) + } + + // Check for an error about root permissions + if bytes.Contains(stdout.Bytes(), []byte("requires root privileges")) { + return nil, errors.New("Requires root privileges") + } + + return stdout.Bytes(), nil +}