Skip to content

Commit 1788685

Browse files
committed
Spec Generation WIP
This is a WIP to generate table spec files. It pulls out some stuff from `table.go` to `column.go` To generate specs, we need to expand the options bitmask into the set of booleans. Instead of doing that via the bitmask (as in osquery#77), this explores storing these as booleans and generating the bitmask as needed.
1 parent a74aa86 commit 1788685

File tree

5 files changed

+267
-53
lines changed

5 files changed

+267
-53
lines changed

plugin/table/column.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package table
2+
3+
// ColumnDefinition defines the relevant information for a column in a table
4+
// plugin. Both values are mandatory. Prefer using the *Column helpers to
5+
// create ColumnDefinition structs.
6+
type ColumnDefinition struct {
7+
Name string `json:"name,omitempty"`
8+
Type ColumnType `json:"type,omitempty"`
9+
Description string `json:"description,omitempty"`
10+
11+
// Options from https://github.com/osquery/osquery/blob/master/osquery/core/sql/column.h#L37
12+
Index bool `json:"index"`
13+
Required bool `json:"required"`
14+
Additional bool `json:"additional"`
15+
Optimized bool `json:"optimized"`
16+
Hidden bool `json:"hidden"`
17+
}
18+
19+
// ColumnType is a strongly typed representation of the data type string for a
20+
// column definition. The named constants should be used.
21+
type ColumnType string
22+
23+
// The following column types are defined in osquery tables.h.
24+
const (
25+
ColumnTypeUnknown ColumnType = "UNKNOWN"
26+
ColumnTypeText = "TEXT"
27+
ColumnTypeInteger = "INTEGER"
28+
ColumnTypeBigInt = "BIGINT"
29+
ColumnTypeUnsignedBigInt = "UNSIGNED BIGINT"
30+
ColumnTypeDouble = "DOUBLE"
31+
ColumnTypeBlob = "BLOB"
32+
)
33+
34+
type ColumnOpt func(*ColumnDefinition)
35+
36+
// TextColumn is a helper for defining columns containing strings.
37+
func TextColumn(name string, opts ...ColumnOpt) ColumnDefinition {
38+
return NewColumn(name, ColumnTypeText, opts...)
39+
}
40+
41+
// IntegerColumn is a helper for defining columns containing integers.
42+
func IntegerColumn(name string, opts ...ColumnOpt) ColumnDefinition {
43+
return NewColumn(name, ColumnTypeInteger, opts...)
44+
}
45+
46+
// BigIntColumn is a helper for defining columns containing big integers.
47+
func BigIntColumn(name string, opts ...ColumnOpt) ColumnDefinition {
48+
return NewColumn(name, ColumnTypeBigInt, opts...)
49+
}
50+
51+
// DoubleColumn is a helper for defining columns containing floating point
52+
// values.
53+
func DoubleColumn(name string, opts ...ColumnOpt) ColumnDefinition {
54+
return NewColumn(name, ColumnTypeDouble, opts...)
55+
}
56+
57+
// NewColumn returns a ColumnDefinition for the specified column.
58+
func NewColumn(name string, ctype ColumnType, opts ...ColumnOpt) ColumnDefinition {
59+
cd := ColumnDefinition{
60+
Name: name,
61+
Type: ctype,
62+
}
63+
64+
for _, opt := range opts {
65+
opt(&cd)
66+
}
67+
68+
return cd
69+
70+
}
71+
72+
// IndexColumn is a functional argument to declare this as an indexed
73+
// column. Depending on impmelentation, this can significantly change
74+
// performance. See osquery source code for more information.
75+
func IndexColumn() ColumnOpt {
76+
return func(cd *ColumnDefinition) {
77+
cd.Index = true
78+
}
79+
}
80+
81+
// RequiredColumn is a functional argument that sets this as a
82+
// required column. sqlite will not process queries, if a required
83+
// column is missing. See osquery source code for more information.
84+
func RequiredColumn() ColumnOpt {
85+
return func(cd *ColumnDefinition) {
86+
cd.Required = true
87+
}
88+
89+
}
90+
91+
// AdditionalColumn is a functional argument that sets this as an
92+
// additional column. See osquery source code for more information.
93+
func AdditionalColumn() ColumnOpt {
94+
return func(cd *ColumnDefinition) {
95+
cd.Additional = true
96+
}
97+
98+
}
99+
100+
// OptimizedColumn is a functional argument that sets this as an
101+
// optimized column. See osquery source code for more information.
102+
func OptimizedColumn() ColumnOpt {
103+
return func(cd *ColumnDefinition) {
104+
cd.Optimized = true
105+
}
106+
107+
}
108+
109+
// HiddenColumn is a functional argument that sets this as an
110+
// hidden column. This omits it from `select *` queries. See osquery source code for more information.
111+
func HiddenColumn() ColumnOpt {
112+
return func(cd *ColumnDefinition) {
113+
cd.Hidden = true
114+
}
115+
116+
}
117+
118+
// ColumnDescription sets the column description. This is not
119+
// currently part of the underlying osquery api, it is here for human
120+
// consumption. It may become part of osquery spec generation.
121+
func ColumnDescription(d string) ColumnOpt {
122+
return func(cd *ColumnDefinition) {
123+
cd.Description = d
124+
}
125+
}
126+
127+
// Options returns the bitmask representation of the boolean column
128+
// options. This uses the values as encoded in
129+
// https://github.com/osquery/osquery/blob/master/osquery/core/sql/column.h#L37
130+
func (c *ColumnDefinition) Options() uint8 {
131+
optionsBitmask := uint8(0)
132+
133+
optionValues := map[uint8]bool{
134+
1: c.Index,
135+
2: c.Required,
136+
4: c.Additional,
137+
8: c.Optimized,
138+
16: c.Hidden,
139+
}
140+
141+
for v, b := range optionValues {
142+
if b {
143+
optionsBitmask = optionsBitmask | v
144+
}
145+
}
146+
return optionsBitmask
147+
}

plugin/table/column_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package table
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestColumnDefinition_Options(t *testing.T) {
10+
t.Parallel()
11+
12+
var tests = []struct {
13+
in []ColumnOpt
14+
expected uint8
15+
}{
16+
{
17+
in: []ColumnOpt{},
18+
expected: 0,
19+
},
20+
{
21+
in: []ColumnOpt{IndexColumn(), HiddenColumn()},
22+
expected: 17,
23+
},
24+
}
25+
26+
for _, tt := range tests {
27+
cd := TextColumn("foo", tt.in...)
28+
require.Equal(t, tt.expected, cd.Options())
29+
}
30+
}

plugin/table/spec.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package table
2+
3+
import (
4+
"encoding/json"
5+
6+
"github.com/pkg/errors"
7+
)
8+
9+
type osqueryTableSpec struct {
10+
Cacheable bool `json:"cacheable"`
11+
Evented bool `json:"evented"`
12+
Name string `json:"name,omitempty"`
13+
Url string `json:"url,omitempty"`
14+
Platforms []string `json:"platforms,omitempty"`
15+
Columns []ColumnDefinition `json:"columns,omitempty"`
16+
}
17+
18+
func (t *Plugin) Spec() (string, error) {
19+
// FIXME: the columndefinition type is upcased, is that an issue?
20+
tableSpec := osqueryTableSpec{
21+
Name: t.name,
22+
Columns: t.columns,
23+
//Platforms: []string{"FIXME"},
24+
}
25+
specBytes, err := json.MarshalIndent(tableSpec, "", " ")
26+
if err != nil {
27+
return "", errors.Wrap(err, "marshalling")
28+
}
29+
return string(specBytes), nil
30+
}

plugin/table/spec_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package table
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestTable_Spec(t *testing.T) {
13+
t.Parallel()
14+
15+
var tests = []struct {
16+
name string
17+
columns []ColumnDefinition
18+
expected string
19+
}{
20+
{
21+
name: "simple",
22+
columns: []ColumnDefinition{TextColumn("simple_text")},
23+
expected: `
24+
{
25+
"name": "simple",
26+
"cacheable": false,
27+
"evented": false,
28+
"columns":[
29+
{ "name": "simple_text", "type": "TEXT", "index": false, "required": false, "additional": false, "optimized": false, "hidden": false }
30+
]
31+
}`,
32+
},
33+
}
34+
35+
mockGenerate := func(_ context.Context, _ QueryContext) ([]map[string]string, error) { return nil, nil }
36+
37+
for _, tt := range tests {
38+
testTable := NewPlugin(tt.name, tt.columns, mockGenerate)
39+
generatedSpec, err := testTable.Spec()
40+
require.NoError(t, err, "generating spec for %s", tt.name)
41+
helperJSONEqVal(t, tt.expected, generatedSpec, "spec for %s", tt.name)
42+
}
43+
}
44+
45+
func helperJSONEqVal(t *testing.T, expected string, actual string, msgAndArgs ...interface{}) {
46+
var expectedJSONAsInterface, actualJSONAsInterface interface{}
47+
48+
if err := json.Unmarshal([]byte(expected), &expectedJSONAsInterface); err != nil {
49+
require.Fail(t, fmt.Sprintf("Expected value ('%s') is not valid json.\nJSON parsing error: '%s'", expected, err.Error()), msgAndArgs...)
50+
return
51+
}
52+
53+
if err := json.Unmarshal([]byte(actual), &actualJSONAsInterface); err != nil {
54+
require.Fail(t, fmt.Sprintf("Input ('%s') needs to be valid json.\nJSON parsing error: '%s'", actual, err.Error()), msgAndArgs...)
55+
return
56+
}
57+
58+
require.EqualValues(t, expectedJSONAsInterface, actualJSONAsInterface, msgAndArgs...)
59+
return
60+
}

plugin/table/table.go

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -103,59 +103,6 @@ func (t *Plugin) Ping() osquery.ExtensionStatus {
103103

104104
func (t *Plugin) Shutdown() {}
105105

106-
// ColumnDefinition defines the relevant information for a column in a table
107-
// plugin. Both values are mandatory. Prefer using the *Column helpers to
108-
// create ColumnDefinition structs.
109-
type ColumnDefinition struct {
110-
Name string
111-
Type ColumnType
112-
}
113-
114-
// TextColumn is a helper for defining columns containing strings.
115-
func TextColumn(name string) ColumnDefinition {
116-
return ColumnDefinition{
117-
Name: name,
118-
Type: ColumnTypeText,
119-
}
120-
}
121-
122-
// IntegerColumn is a helper for defining columns containing integers.
123-
func IntegerColumn(name string) ColumnDefinition {
124-
return ColumnDefinition{
125-
Name: name,
126-
Type: ColumnTypeInteger,
127-
}
128-
}
129-
130-
// BigIntColumn is a helper for defining columns containing big integers.
131-
func BigIntColumn(name string) ColumnDefinition {
132-
return ColumnDefinition{
133-
Name: name,
134-
Type: ColumnTypeBigInt,
135-
}
136-
}
137-
138-
// DoubleColumn is a helper for defining columns containing floating point
139-
// values.
140-
func DoubleColumn(name string) ColumnDefinition {
141-
return ColumnDefinition{
142-
Name: name,
143-
Type: ColumnTypeDouble,
144-
}
145-
}
146-
147-
// ColumnType is a strongly typed representation of the data type string for a
148-
// column definition. The named constants should be used.
149-
type ColumnType string
150-
151-
// The following column types are defined in osquery tables.h.
152-
const (
153-
ColumnTypeText ColumnType = "TEXT"
154-
ColumnTypeInteger = "INTEGER"
155-
ColumnTypeBigInt = "BIGINT"
156-
ColumnTypeDouble = "DOUBLE"
157-
)
158-
159106
// QueryContext contains the constraints from the WHERE clause of the query,
160107
// that can optionally be used to optimize the table generation. Note that the
161108
// osquery SQLite engine will perform the filtering with these constraints, so

0 commit comments

Comments
 (0)