Skip to content

Commit

Permalink
macOS system profiler table (#574)
Browse files Browse the repository at this point in the history
Create a `systemprofile` table using `dataflatten` to process the output from `system_profile`
  • Loading branch information
directionless authored Feb 19, 2020
1 parent d99e581 commit 7e21812
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 4 deletions.
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion pkg/dataflatten/flatten.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 2 additions & 0 deletions pkg/osquery/table/platform_tables_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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),
}
Expand Down
236 changes: 236 additions & 0 deletions pkg/osquery/tables/systemprofiler/systemprofiler.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 7e21812

Please sign in to comment.