Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spec Generation WIP #78

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions plugin/table/column.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package table

// ColumnDefinition defines the relevant information for a column in a table
// plugin. Both values are mandatory. Prefer using the *Column helpers to
// create ColumnDefinition structs.
type ColumnDefinition struct {
Name string `json:"name,omitempty"`
Type ColumnType `json:"type,omitempty"`
Description string `json:"description,omitempty"`

// Options from https://github.com/osquery/osquery/blob/master/osquery/core/sql/column.h#L37
Index bool `json:"index"`
Required bool `json:"required"`
Additional bool `json:"additional"`
Optimized bool `json:"optimized"`
Hidden bool `json:"hidden"`
}

// ColumnType is a strongly typed representation of the data type string for a
// column definition. The named constants should be used.
type ColumnType string

// The following column types are defined in osquery tables.h.
const (
ColumnTypeUnknown ColumnType = "UNKNOWN"
ColumnTypeText = "TEXT"
ColumnTypeInteger = "INTEGER"
ColumnTypeBigInt = "BIGINT"
ColumnTypeUnsignedBigInt = "UNSIGNED BIGINT"
ColumnTypeDouble = "DOUBLE"
ColumnTypeBlob = "BLOB"
)

type ColumnOpt func(*ColumnDefinition)

// TextColumn is a helper for defining columns containing strings.
func TextColumn(name string, opts ...ColumnOpt) ColumnDefinition {
return NewColumn(name, ColumnTypeText, opts...)
}

// IntegerColumn is a helper for defining columns containing integers.
func IntegerColumn(name string, opts ...ColumnOpt) ColumnDefinition {
return NewColumn(name, ColumnTypeInteger, opts...)
}

// BigIntColumn is a helper for defining columns containing big integers.
func BigIntColumn(name string, opts ...ColumnOpt) ColumnDefinition {
return NewColumn(name, ColumnTypeBigInt, opts...)
}

// DoubleColumn is a helper for defining columns containing floating point
// values.
func DoubleColumn(name string, opts ...ColumnOpt) ColumnDefinition {
return NewColumn(name, ColumnTypeDouble, opts...)
}

// NewColumn returns a ColumnDefinition for the specified column.
func NewColumn(name string, ctype ColumnType, opts ...ColumnOpt) ColumnDefinition {
cd := ColumnDefinition{
Name: name,
Type: ctype,
}

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

return cd

}

// IndexColumn is a functional argument to declare this as an indexed
// column. Depending on impmelentation, this can significantly change
// performance. See osquery source code for more information.
func IndexColumn() ColumnOpt {
return func(cd *ColumnDefinition) {
cd.Index = true
}
}

// RequiredColumn is a functional argument that sets this as a
// required column. sqlite will not process queries, if a required
// column is missing. See osquery source code for more information.
func RequiredColumn() ColumnOpt {
return func(cd *ColumnDefinition) {
cd.Required = true
}

}

// AdditionalColumn is a functional argument that sets this as an
// additional column. See osquery source code for more information.
func AdditionalColumn() ColumnOpt {
return func(cd *ColumnDefinition) {
cd.Additional = true
}

}

// OptimizedColumn is a functional argument that sets this as an
// optimized column. See osquery source code for more information.
func OptimizedColumn() ColumnOpt {
return func(cd *ColumnDefinition) {
cd.Optimized = true
}

}

// HiddenColumn is a functional argument that sets this as an
// hidden column. This omits it from `select *` queries. See osquery source code for more information.
func HiddenColumn() ColumnOpt {
return func(cd *ColumnDefinition) {
cd.Hidden = true
}

}

// ColumnDescription sets the column description. This is not
// currently part of the underlying osquery api, it is here for human
// consumption. It may become part of osquery spec generation.
func ColumnDescription(d string) ColumnOpt {
return func(cd *ColumnDefinition) {
cd.Description = d
}
}

// Options returns the bitmask representation of the boolean column
// options. This uses the values as encoded in
// https://github.com/osquery/osquery/blob/master/osquery/core/sql/column.h#L37
func (c *ColumnDefinition) Options() uint8 {
optionsBitmask := uint8(0)

optionValues := map[uint8]bool{
1: c.Index,
2: c.Required,
4: c.Additional,
8: c.Optimized,
16: c.Hidden,
}

for v, b := range optionValues {
if b {
optionsBitmask = optionsBitmask | v
}
}
return optionsBitmask
}
30 changes: 30 additions & 0 deletions plugin/table/column_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package table

import (
"testing"

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

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

var tests = []struct {
in []ColumnOpt
expected uint8
}{
{
in: []ColumnOpt{},
expected: 0,
},
{
in: []ColumnOpt{IndexColumn(), HiddenColumn()},
expected: 17,
},
}

for _, tt := range tests {
cd := TextColumn("foo", tt.in...)
require.Equal(t, tt.expected, cd.Options())
}
}
30 changes: 30 additions & 0 deletions plugin/table/spec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package table

import (
"encoding/json"

"github.com/pkg/errors"
)

type osqueryTableSpec struct {
Cacheable bool `json:"cacheable"`
Evented bool `json:"evented"`
Name string `json:"name,omitempty"`
Url string `json:"url,omitempty"`
Platforms []string `json:"platforms,omitempty"`
Columns []ColumnDefinition `json:"columns,omitempty"`
}

func (t *Plugin) Spec() (string, error) {
// FIXME: the columndefinition type is upcased, is that an issue?
tableSpec := osqueryTableSpec{
Name: t.name,
Columns: t.columns,
//Platforms: []string{"FIXME"},
}
specBytes, err := json.MarshalIndent(tableSpec, "", " ")
if err != nil {
return "", errors.Wrap(err, "marshalling")
}
return string(specBytes), nil
}
60 changes: 60 additions & 0 deletions plugin/table/spec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package table

import (
"context"
"encoding/json"
"fmt"
"testing"

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

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

var tests = []struct {
name string
columns []ColumnDefinition
expected string
}{
{
name: "simple",
columns: []ColumnDefinition{TextColumn("simple_text")},
expected: `
{
"name": "simple",
"cacheable": false,
"evented": false,
"columns":[
{ "name": "simple_text", "type": "TEXT", "index": false, "required": false, "additional": false, "optimized": false, "hidden": false }
]
}`,
},
}

mockGenerate := func(_ context.Context, _ QueryContext) ([]map[string]string, error) { return nil, nil }

for _, tt := range tests {
testTable := NewPlugin(tt.name, tt.columns, mockGenerate)
generatedSpec, err := testTable.Spec()
require.NoError(t, err, "generating spec for %s", tt.name)
helperJSONEqVal(t, tt.expected, generatedSpec, "spec for %s", tt.name)
}
}

func helperJSONEqVal(t *testing.T, expected string, actual string, msgAndArgs ...interface{}) {
var expectedJSONAsInterface, actualJSONAsInterface interface{}

if err := json.Unmarshal([]byte(expected), &expectedJSONAsInterface); err != nil {
require.Fail(t, fmt.Sprintf("Expected value ('%s') is not valid json.\nJSON parsing error: '%s'", expected, err.Error()), msgAndArgs...)
return
}

if err := json.Unmarshal([]byte(actual), &actualJSONAsInterface); err != nil {
require.Fail(t, fmt.Sprintf("Input ('%s') needs to be valid json.\nJSON parsing error: '%s'", actual, err.Error()), msgAndArgs...)
return
}

require.EqualValues(t, expectedJSONAsInterface, actualJSONAsInterface, msgAndArgs...)
return
}
55 changes: 1 addition & 54 deletions plugin/table/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (t *Plugin) Routes() osquery.ExtensionPluginResponse {
"id": "column",
"name": col.Name,
"type": string(col.Type),
"op": "0",
"op": strconv.FormatUint(uint64(col.Options()), 10),
})
}
return routes
Expand Down Expand Up @@ -103,59 +103,6 @@ func (t *Plugin) Ping() osquery.ExtensionStatus {

func (t *Plugin) Shutdown() {}

// ColumnDefinition defines the relevant information for a column in a table
// plugin. Both values are mandatory. Prefer using the *Column helpers to
// create ColumnDefinition structs.
type ColumnDefinition struct {
Name string
Type ColumnType
}

// TextColumn is a helper for defining columns containing strings.
func TextColumn(name string) ColumnDefinition {
return ColumnDefinition{
Name: name,
Type: ColumnTypeText,
}
}

// IntegerColumn is a helper for defining columns containing integers.
func IntegerColumn(name string) ColumnDefinition {
return ColumnDefinition{
Name: name,
Type: ColumnTypeInteger,
}
}

// BigIntColumn is a helper for defining columns containing big integers.
func BigIntColumn(name string) ColumnDefinition {
return ColumnDefinition{
Name: name,
Type: ColumnTypeBigInt,
}
}

// DoubleColumn is a helper for defining columns containing floating point
// values.
func DoubleColumn(name string) ColumnDefinition {
return ColumnDefinition{
Name: name,
Type: ColumnTypeDouble,
}
}

// ColumnType is a strongly typed representation of the data type string for a
// column definition. The named constants should be used.
type ColumnType string

// The following column types are defined in osquery tables.h.
const (
ColumnTypeText ColumnType = "TEXT"
ColumnTypeInteger = "INTEGER"
ColumnTypeBigInt = "BIGINT"
ColumnTypeDouble = "DOUBLE"
)

// QueryContext contains the constraints from the WHERE clause of the query,
// that can optionally be used to optimize the table generation. Note that the
// osquery SQLite engine will perform the filtering with these constraints, so
Expand Down