From 7e21812f0ca5d0a0a6f333d5535b9ece3b1a5fd1 Mon Sep 17 00:00:00 2001 From: seph Date: Wed, 19 Feb 2020 17:17:16 -0500 Subject: [PATCH] macOS system profiler table (#574) Create a `systemprofile` table using `dataflatten` to process the output from `system_profile` --- Makefile | 6 +- pkg/dataflatten/flatten.go | 4 +- pkg/osquery/table/platform_tables_darwin.go | 2 + .../tables/systemprofiler/systemprofiler.go | 236 ++++++++++++++++++ 4 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 pkg/osquery/tables/systemprofiler/systemprofiler.go diff --git a/Makefile b/Makefile index 24c119258..8ed5de48c 100644 --- a/Makefile +++ b/Makefile @@ -40,11 +40,11 @@ table.ext: .pre-build go run cmd/make/make.go -targets=table-extension -linkstamp osqueryi-tables: table.ext - osqueryd -S --allow-unsafe --extension ./build/darwin/tables.ext + osqueryd -S --allow-unsafe --verbose --extension ./build/darwin/tables.ext sudo-osqueryi-tables: table.ext - sudo osqueryd -S --allow-unsafe --extension ./build/darwin/tables.ext + sudo osqueryd -S --allow-unsafe --verbose --extension ./build/darwin/tables.ext launchas-osqueryi-tables: table.ext - sudo launchctl asuser 0 osqueryd -S --allow-unsafe --extension ./build/darwin/tables.ext + sudo launchctl asuser 0 osqueryd -S --allow-unsafe --verbose --extension ./build/darwin/tables.ext extension: .pre-build diff --git a/pkg/dataflatten/flatten.go b/pkg/dataflatten/flatten.go index 8d8aeb433..6c5179eb7 100644 --- a/pkg/dataflatten/flatten.go +++ b/pkg/dataflatten/flatten.go @@ -363,12 +363,14 @@ func stringify(data interface{}) (string, error) { return strconv.FormatFloat(v, 'f', -1, 64), nil case int: return strconv.Itoa(v), nil + case int64: + return strconv.FormatInt(v, 10), nil case bool: return strconv.FormatBool(v), nil case time.Time: return strconv.FormatInt(v.Unix(), 10), nil default: - //spew.Dump(data) + // spew.Dump(data) return "", errors.Errorf("unknown type on %v", data) } } diff --git a/pkg/osquery/table/platform_tables_darwin.go b/pkg/osquery/table/platform_tables_darwin.go index fc7d6502e..474bbabe9 100644 --- a/pkg/osquery/table/platform_tables_darwin.go +++ b/pkg/osquery/table/platform_tables_darwin.go @@ -7,6 +7,7 @@ import ( "github.com/knightsc/system_policy/osquery/table/kextpolicy" "github.com/knightsc/system_policy/osquery/table/legacyexec" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" + "github.com/kolide/launcher/pkg/osquery/tables/systemprofiler" osquery "github.com/kolide/osquery-go" "github.com/kolide/osquery-go/plugin/table" _ "github.com/mattn/go-sqlite3" @@ -32,6 +33,7 @@ func platformTables(client *osquery.ExtensionManagerClient, logger log.Logger) [ kextpolicy.TablePlugin(), legacyexec.TablePlugin(), dataflattentable.TablePlugin(client, logger, dataflattentable.PlistType), + systemprofiler.TablePlugin(client, logger), munki.ManagedInstalls(client, logger), munki.MunkiReport(client, logger), } diff --git a/pkg/osquery/tables/systemprofiler/systemprofiler.go b/pkg/osquery/tables/systemprofiler/systemprofiler.go new file mode 100644 index 000000000..287ee9980 --- /dev/null +++ b/pkg/osquery/tables/systemprofiler/systemprofiler.go @@ -0,0 +1,236 @@ +//+build darwin + +// Package systemprofiler provides a suite table wrapper around +// `system_profiler` macOS command. It supports some basic arguments +// like `detaillevel` and requested data types. +// +// Note that some detail levels and data types will have performance +// impact if requested. +// +// As the returned data is a complex nested plist, this uses the +// dataflatten tooling. (See +// https://godoc.org/github.com/kolide/launcher/pkg/dataflatten) +// +// Everything, minimal details: +// +// osquery> select count(*) from kolide_system_profiler where datatype like "%" and detaillevel = "mini"; +// +----------+ +// | count(*) | +// +----------+ +// | 1270 | +// +----------+ +// +// Multiple data types (slightly redacted): +// +// osquery> select fullkey, key, value, datatype from kolide_system_profiler where datatype in ("SPCameraDataType", "SPiBridgeDataType"); +// +----------------------+--------------------+------------------------------------------+-------------------+ +// | fullkey | key | value | datatype | +// +----------------------+--------------------+------------------------------------------+-------------------+ +// | 0/spcamera_unique-id | spcamera_unique-id | 0x1111111111111111 | SPCameraDataType | +// | 0/_name | _name | FaceTime HD Camera | SPCameraDataType | +// | 0/spcamera_model-id | spcamera_model-id | UVC Camera VendorID_1452 ProductID_30000 | SPCameraDataType | +// | 0/_name | _name | Controller Information | SPiBridgeDataType | +// | 0/ibridge_build | ibridge_build | 14Y000 | SPiBridgeDataType | +// | 0/ibridge_model_name | ibridge_model_name | Apple T1 Security Chip | SPiBridgeDataType | +// +----------------------+--------------------+------------------------------------------+-------------------+ + +package systemprofiler + +import ( + "bytes" + "context" + "os/exec" + "strings" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/groob/plist" + "github.com/kolide/launcher/pkg/dataflatten" + "github.com/kolide/osquery-go" + "github.com/kolide/osquery-go/plugin/table" + "github.com/pkg/errors" +) + +const systemprofilerPath = "/usr/sbin/system_profiler" + +var knownDetailLevels = []string{ + "mini", // short report (contains no identifying or personal information) + "basic", // basic hardware and network information + "full", // all available information +} + +type Property struct { + Order string `plist:"_order"` + SuppressLocalization string `plist:"_suppressLocalization"` + DetailLevel string `plist:"_detailLevel"` +} + +type Result struct { + Items []interface{} `plist:"_items"` + DataType string `plist:"_dataType"` + SPCommandLine []string `plist:"_SPCommandLineArguments"` + ParentDataType string `plist:"_parentDataType"` + + // These would be nice to add, but they come back with inconsistent + // types, so doing a straight unmarshal is hard. + // DetailLevel int `plist:"_detailLevel"` + // Properties map[string]Property `plist:"_properties"` +} + +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("parentdatatype"), + + table.TextColumn("query"), + table.TextColumn("datatype"), + table.TextColumn("detaillevel"), + } + + t := &Table{ + client: client, + logger: level.NewFilter(logger, level.AllowInfo()), + tableName: "kolide_system_profiler", + } + + 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 + + requestedDatatypes := []string{} + + datatypeQ, ok := queryContext.Constraints["datatype"] + if !ok || len(datatypeQ.Constraints) == 0 { + return results, errors.Errorf("The %s table requires that you specify a constraint for datatype", t.tableName) + } + + for _, datatypeConstraint := range datatypeQ.Constraints { + dt := datatypeConstraint.Expression + + // If the constraint is the magic "%", it's eqivlent to an `all` style + if dt == "%" { + requestedDatatypes = []string{} + break + } + + requestedDatatypes = append(requestedDatatypes, dt) + } + + var detailLevel string + if q, ok := queryContext.Constraints["detaillevel"]; ok && len(q.Constraints) != 0 { + if len(q.Constraints) > 1 { + level.Info(t.logger).Log("msg", "WARNING: Only using the first detaillevel request") + } + + dl := q.Constraints[0].Expression + for _, known := range knownDetailLevels { + if known == dl { + detailLevel = dl + } + } + + } + + systemProfilerOutput, err := t.execSystemProfiler(ctx, detailLevel, requestedDatatypes) + if err != nil { + return results, errors.Wrap(err, "exec") + } + + if q, ok := queryContext.Constraints["query"]; ok && len(q.Constraints) != 0 { + for _, constraint := range q.Constraints { + dataQuery := constraint.Expression + results = append(results, t.getRowsFromOutput(dataQuery, detailLevel, systemProfilerOutput)...) + } + } else { + results = append(results, t.getRowsFromOutput("", detailLevel, systemProfilerOutput)...) + } + + return results, nil +} + +func (t *Table) getRowsFromOutput(dataQuery, detailLevel string, systemProfilerOutput []byte) []map[string]string { + var results []map[string]string + + flattenOpts := []dataflatten.FlattenOpts{} + + if dataQuery != "" { + flattenOpts = append(flattenOpts, dataflatten.WithQuery(strings.Split(dataQuery, "/"))) + } + + if t.logger != nil { + flattenOpts = append(flattenOpts, dataflatten.WithLogger(t.logger)) + } + + var systemProfilerResults []Result + if err := plist.Unmarshal(systemProfilerOutput, &systemProfilerResults); err != nil { + level.Info(t.logger).Log("msg", "error unmarshalling system_profile output", "err", err) + return nil + } + + for _, systemProfilerResult := range systemProfilerResults { + + dataType := systemProfilerResult.DataType + + data, err := dataflatten.Flatten(systemProfilerResult.Items, flattenOpts...) + + if err != nil { + level.Info(t.logger).Log("msg", "failure flattening system_profile output", "err", err) + return nil + } + + for _, row := range data { + p, k := row.ParentKey("/") + + res := map[string]string{ + "datatype": dataType, + "parentdatatype": systemProfilerResult.ParentDataType, + "fullkey": row.StringPath("/"), + "parent": p, + "key": k, + "value": row.Value, + "query": dataQuery, + "detaillevel": detailLevel, + } + results = append(results, res) + } + } + + return results +} + +func (t *Table) execSystemProfiler(ctx context.Context, detailLevel string, subcommands []string) ([]byte, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + + args := []string{"-xml"} + + if detailLevel != "" { + args = append(args, "-detailLevel", detailLevel) + } + + args = append(args, subcommands...) + + cmd := exec.CommandContext(ctx, systemprofilerPath, args...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + level.Debug(t.logger).Log("msg", "calling system_profiler", "args", cmd.Args) + + if err := cmd.Run(); err != nil { + return nil, errors.Wrapf(err, "calling system_profiler. Got: %s", string(stderr.Bytes())) + } + + return stdout.Bytes(), nil +}