Skip to content

Commit

Permalink
Add namespace support to kolide_wmi (#623)
Browse files Browse the repository at this point in the history
Adds support for specifying the connection parameters to the underlying wmi package, and plumbs namespace through to the table.

Additionally add `tablehelper` methods to improve code readability when parsing column constraints
  • Loading branch information
directionless authored Jul 6, 2020
1 parent 2e4c979 commit e1e2998
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 44 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ launcher: .pre-build

table.ext: .pre-build
go run cmd/make/make.go -targets=table-extension -linkstamp
table.ext-windows: .pre-build deps
go run cmd/make/make.go -targets=table-extension -linkstamp --os windows


osqueryi-tables: table.ext
osqueryd -S --allow-unsafe --verbose --extension ./build/darwin/tables.ext
Expand Down
30 changes: 30 additions & 0 deletions pkg/osquery/tables/tablehelpers/getconstraints.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package tablehelpers

import (
"github.com/kolide/osquery-go/plugin/table"
)

// 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 {
q, ok := queryContext.Constraints[columnName]
if !ok || len(q.Constraints) == 0 {
return defaults
}

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

for _, c := range q.Constraints {
constraintSet[c.Expression] = struct{}{}
}

constraints := make([]string, len(constraintSet))

i := 0
for key := range constraintSet {
constraints[i] = key
i++
}

return constraints
}
83 changes: 83 additions & 0 deletions pkg/osquery/tables/tablehelpers/getconstraints_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package tablehelpers

import (
"testing"

"github.com/stretchr/testify/require"
)

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

mockQC := MockQueryContext(map[string][]string{
"empty_array": []string{},
"blank": []string{""},
"single": []string{"a"},
"double": []string{"a", "b"},
"duplicates": []string{"a", "a", "b", "b"},
"duplicate_blanks": []string{"a", "a", "", ""},
})

var tests = []struct {
name string
expected []string
defaults []string
}{
{
name: "does_not_exist",
expected: []string(nil),
},
{
name: "does_not_exist_with_defaults",
expected: []string{"a", "b"},
defaults: []string{"a", "b"},
},
{
name: "empty_array",
expected: []string{"a", "b"},
defaults: []string{"a", "b"},
},
{
name: "empty_array",
expected: []string(nil),
},
{
name: "blank",
expected: []string{""},
},
{
name: "blank",
expected: []string{""},
defaults: []string{"a", "b"},
},
{
name: "single",
expected: []string{"a"},
},
{
name: "single",
expected: []string{"a"},
defaults: []string{"a", "b"},
},
{
name: "double",
expected: []string{"a", "b"},
},
{
name: "duplicates",
expected: []string{"a", "b"},
},
{
name: "duplicate_blanks",
expected: []string{"a", ""},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := GetConstraints(mockQC, tt.name, tt.defaults...)
require.Equal(t, tt.expected, actual)
})
}

}
6 changes: 4 additions & 2 deletions pkg/osquery/tables/wmitable/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import "strings"

const allowedCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"

func onlyAllowedCharacters(input string) bool {
func onlyAllowedCharacters(input string, extras ...string) bool {

allowed := strings.Join(append(extras, allowedCharacters), "")
for _, char := range input {
if !strings.ContainsRune(allowedCharacters, char) {
if !strings.ContainsRune(allowed, char) {
return false
}
}
Expand Down
86 changes: 48 additions & 38 deletions pkg/osquery/tables/wmitable/wmitable.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/kolide/launcher/pkg/dataflatten"
"github.com/kolide/launcher/pkg/osquery/tables/tablehelpers"
"github.com/kolide/launcher/pkg/wmi"

"github.com/go-kit/kit/log"
Expand All @@ -33,6 +34,7 @@ func TablePlugin(client *osquery.ExtensionManagerClient, logger log.Logger) *tab
table.TextColumn("query"),

// wmi columns
table.TextColumn("namespace"),
table.TextColumn("class"),
table.TextColumn("properties"),
}
Expand All @@ -49,65 +51,72 @@ func TablePlugin(client *osquery.ExtensionManagerClient, logger log.Logger) *tab
func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
var results []map[string]string

classQ, ok := queryContext.Constraints["class"]
if !ok || len(classQ.Constraints) == 0 {
classes := tablehelpers.GetConstraints(queryContext, "class")
for _, c := range classes {
if !onlyAllowedCharacters(c) {
return nil, errors.New("Disallowed character in class expression")
}
}
if len(classes) == 0 {
return nil, errors.New("The kolide_wmi table requires a wmi class")
}

propertiesQ, ok := queryContext.Constraints["properties"]
if !ok || len(propertiesQ.Constraints) == 0 {
properties := tablehelpers.GetConstraints(queryContext, "properties")
for _, p := range properties {
// Allow comma, since we're going to split on it later.
if !onlyAllowedCharacters(p, `,`) {
return nil, errors.New("Disallowed character in properties expression")
}
}
if len(properties) == 0 {
return nil, errors.New("The kolide_wmi table requires wmi properties")
}

for _, classConstraint := range classQ.Constraints {
if !onlyAllowedCharacters(classConstraint.Expression) {
level.Info(t.logger).Log("msg", "Disallowed character in class expression")
return nil, errors.New("Disallowed character in class expression")
// Get the list of namespaces to query. If not specified, that's
// okay too, default to ""
namespaces := tablehelpers.GetConstraints(queryContext, "namespace", "")
for _, ns := range namespaces {
if !onlyAllowedCharacters(ns, `\`) {
return nil, errors.New("Disallowed character in namespace expression")
}
}

for _, propertiesConstraint := range propertiesQ.Constraints {

if !onlyAllowedCharacters(strings.Replace(propertiesConstraint.Expression, ",", "", -1)) {
level.Info(t.logger).Log("msg", "Disallowed character in properties expression")
return nil, errors.New("Disallowed character in properties expression")
}
flattenQueries := tablehelpers.GetConstraints(queryContext, "query", "")

properties := strings.Split(propertiesConstraint.Expression, ",")
for _, class := range classes {
for _, rawProperties := range properties {
properties := strings.Split(rawProperties, ",")
if len(properties) == 0 {
continue
}
for _, ns := range namespaces {
// Set a timeout in case wmi hangs
ctx, cancel := context.WithTimeout(ctx, 120*time.Second)
defer cancel()

wmiResults, err := wmi.Query(ctx, class, properties, wmi.ConnectUseMaxWait(), wmi.ConnectNamespace(ns))
if err != nil {
level.Info(t.logger).Log(
"msg", "wmi query failure",
"err", err,
"class", class,
"properties", rawProperties,
"namespace", ns,
)
continue
}

// Set a timeout in case wmi hangs
ctx, cancel := context.WithTimeout(ctx, 120*time.Second)
defer cancel()

wmiResults, err := wmi.Query(ctx, classConstraint.Expression, properties)
if err != nil {
level.Info(t.logger).Log(
"msg", "wmi query failure",
"err", err,
"class", classConstraint.Expression,
"properties", propertiesConstraint.Expression,
)
continue
}

if q, ok := queryContext.Constraints["query"]; ok && len(q.Constraints) != 0 {
for _, constraint := range q.Constraints {
dataQuery := constraint.Expression
results = append(results, t.flattenRowsFromWmi(dataQuery, wmiResults, classConstraint.Expression, propertiesConstraint.Expression)...)
for _, dataQuery := range flattenQueries {
results = append(results, t.flattenRowsFromWmi(dataQuery, wmiResults, class, rawProperties, ns)...)
}
} else {
results = append(results, t.flattenRowsFromWmi("", wmiResults, classConstraint.Expression, propertiesConstraint.Expression)...)
}

}
}

return results, nil
}

func (t *Table) flattenRowsFromWmi(dataQuery string, wmiResults []map[string]interface{}, wmiClass, wmiProperties string) []map[string]string {
func (t *Table) flattenRowsFromWmi(dataQuery string, wmiResults []map[string]interface{}, wmiClass, wmiProperties, wmiNamespace string) []map[string]string {
flattenOpts := []dataflatten.FlattenOpts{}

if dataQuery != "" {
Expand Down Expand Up @@ -144,6 +153,7 @@ func (t *Table) flattenRowsFromWmi(dataQuery string, wmiResults []map[string]int
"query": dataQuery,
"class": wmiClass,
"properties": wmiProperties,
"namespace": wmiNamespace,
}
results = append(results, res)
}
Expand Down
24 changes: 24 additions & 0 deletions pkg/osquery/tables/wmitable/wmitable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ func TestQueries(t *testing.T) {
name string
class string
properties []string
namespace string
minRows int
noData bool
err bool
}{
{
Expand Down Expand Up @@ -61,13 +63,30 @@ func TestQueries(t *testing.T) {
properties: []string{"name,ver;sion"},
err: true,
},

{
name: "bad namespace",
class: "Win32_OperatingSystem",
properties: []string{"name,version"},
namespace: `root\unknown\namespace`,
noData: true,
},

{
name: "different namespace",
class: "MSKeyboard_PortInformation",
properties: []string{"ConnectorType,FunctionKeys,Indicators"},
namespace: `root\wmi`,
minRows: 3,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockQC := tablehelpers.MockQueryContext(map[string][]string{
"class": []string{tt.class},
"properties": tt.properties,
"namespace": []string{tt.namespace},
})

rows, err := wmiTable.generate(context.TODO(), mockQC)
Expand All @@ -79,6 +98,11 @@ func TestQueries(t *testing.T) {

require.NoError(t, err)

if tt.noData {
assert.Empty(t, rows, "Expected no results")
return
}

// It's hard to model what we expect to get
// from a random windows machine. So let's
// just check for non-empty data.
Expand Down
Loading

0 comments on commit e1e2998

Please sign in to comment.