Skip to content

Commit

Permalink
Add macOS ioreg table (#627)
Browse files Browse the repository at this point in the history
Add a `kolide_ioreg` table as a wrapper around exec calls to `ioreg`. Uses dataflatten.

Update `tablehelpers.GetConstraints` to make it a bit easier to handle these cases.
  • Loading branch information
directionless committed Jul 21, 2020
1 parent d4a5205 commit 620f42b
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 84 deletions.
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 @@ -8,6 +8,7 @@ import (
"github.com/knightsc/system_policy/osquery/table/legacyexec"
"github.com/kolide/launcher/pkg/osquery/tables/dataflattentable"
"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/screenlock"
"github.com/kolide/launcher/pkg/osquery/tables/systemprofiler"
Expand All @@ -34,6 +35,7 @@ func platformTables(client *osquery.ExtensionManagerClient, logger log.Logger, c
TouchIDUserConfig(client, logger),
TouchIDSystemConfig(client, logger),
UserAvatar(logger),
ioreg.TablePlugin(client, logger),
kextpolicy.TablePlugin(),
legacyexec.TablePlugin(),
dataflattentable.TablePlugin(client, logger, dataflattentable.PlistType),
Expand Down
192 changes: 192 additions & 0 deletions pkg/osquery/tables/ioreg/ioreg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
//+build darwin

// Package ioreg provides a tablle wrapper around the `ioreg` macOS
// command.
//
// 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 ioreg

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 ioregPath = "/usr/sbin/ioreg"

const allowedCharacters = "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"),

// ioreg input options. These match the ioreg
// command line. See the ioreg man page.
table.TextColumn("c"),
table.IntegerColumn("d"),
table.TextColumn("k"),
table.TextColumn("n"),
table.TextColumn("p"),
table.IntegerColumn("r"), // boolean
}

t := &Table{
client: client,
logger: logger,
tableName: "kolide_ioreg",
}

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

gcOpts := []tablehelpers.GetConstraintOpts{
tablehelpers.WithDefaults(""),
tablehelpers.WithAllowedCharacters(allowedCharacters),
tablehelpers.WithLogger(t.logger),
}

for _, ioC := range tablehelpers.GetConstraints(queryContext, "c", gcOpts...) {
ioregArgs := []string{}

if ioC != "" {
ioregArgs = append(ioregArgs, "-c", ioC)
}

for _, ioD := range tablehelpers.GetConstraints(queryContext, "d", gcOpts...) {
if ioD != "" {
ioregArgs = append(ioregArgs, "-d", ioD)
}

for _, ioK := range tablehelpers.GetConstraints(queryContext, "k", gcOpts...) {
if ioK != "" {
ioregArgs = append(ioregArgs, "-k", ioK)
}
for _, ioN := range tablehelpers.GetConstraints(queryContext, "n", gcOpts...) {
if ioN != "" {
ioregArgs = append(ioregArgs, "-n", ioN)
}

for _, ioP := range tablehelpers.GetConstraints(queryContext, "p", gcOpts...) {
if ioP != "" {
ioregArgs = append(ioregArgs, "-p", ioP)
}

for _, ioR := range tablehelpers.GetConstraints(queryContext, "r", gcOpts...) {
switch ioR {
case "", "0":
// do nothing
case "1":
ioregArgs = append(ioregArgs, "-r")
default:
level.Info(t.logger).Log("msg", "r should be blank, 0, or 1")
continue
}

for _, dataQuery := range tablehelpers.GetConstraints(queryContext, "query", tablehelpers.WithDefaults("")) {
// Finally, an inner loop

ioregOutput, err := t.execIoreg(ctx, ioregArgs)
if err != nil {
level.Info(t.logger).Log("msg", "ioreg failed", "err", err)
continue
}

flatData, err := t.flattenOutput(dataQuery, ioregOutput)
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,
"c": ioC,
"d": ioD,
"k": ioK,
"n": ioN,
"p": ioP,
"r": ioR,
}
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) execIoreg(ctx context.Context, args []string) ([]byte, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer

args = append(args, "-a")

ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

cmd := exec.CommandContext(ctx, ioregPath, args...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr

level.Debug(t.logger).Log("msg", "calling ioreg", "args", cmd.Args)

if err := cmd.Run(); err != nil {
return nil, errors.Wrapf(err, "calling ioreg. Got: %s", string(stderr.Bytes()))
}

return stdout.Bytes(), nil
}
70 changes: 68 additions & 2 deletions pkg/osquery/tables/tablehelpers/getconstraints.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,73 @@
package tablehelpers

import (
"strings"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/kolide/osquery-go/plugin/table"
)

type constraintOptions struct {
defaults []string
allowedCharacters string
logger log.Logger
}

type GetConstraintOpts func(*constraintOptions)

// WithLogger sets the logger to use
func WithLogger(logger log.Logger) GetConstraintOpts {
return func(co *constraintOptions) {
co.logger = logger
}
}

// WithDefaults sets the defaults to use if no constraints were
// specified. Note that this does not apply if there were constraints,
// which were invalidated.
func WithDefaults(defaults ...string) GetConstraintOpts {
return func(co *constraintOptions) {
co.defaults = append(co.defaults, defaults...)
}
}

func WithAllowedCharacters(allowed string) GetConstraintOpts {
return func(co *constraintOptions) {
co.allowedCharacters = allowed
}
}

// GetConstraints returns a []string of the constraint expressions on
// a column. It's meant for the common, simple, usecase of iterating over them.
func GetConstraints(queryContext table.QueryContext, columnName string, defaults ...string) []string {
func GetConstraints(queryContext table.QueryContext, columnName string, opts ...GetConstraintOpts) []string {

co := &constraintOptions{
logger: log.NewNopLogger(),
}

for _, opt := range opts {
opt(co)
}

q, ok := queryContext.Constraints[columnName]
if !ok || len(q.Constraints) == 0 {
return defaults
return co.defaults
}

constraintSet := make(map[string]struct{})

for _, c := range q.Constraints {
if !co.OnlyAllowedCharacters(c.Expression) {
level.Info(co.logger).Log(
"msg", "Disallowed character in expression",
"column", columnName,
"expression", c.Expression,
)
continue
}

// empty struct is less ram than bool would be
constraintSet[c.Expression] = struct{}{}
}

Expand All @@ -28,3 +81,16 @@ func GetConstraints(queryContext table.QueryContext, columnName string, defaults

return constraints
}

func (co *constraintOptions) OnlyAllowedCharacters(input string) bool {
if co.allowedCharacters == "" {
return true
}

for _, char := range input {
if !strings.ContainsRune(co.allowedCharacters, char) {
return false
}
}
return true
}

0 comments on commit 620f42b

Please sign in to comment.