github.com/Uptycs/basequery-go@v0.8.0/plugin/table/table_test.go (about)

     1  package table
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"testing"
     8  
     9  	"github.com/Uptycs/basequery-go/gen/osquery"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  )
    13  
    14  func TestTablePlugin(t *testing.T) {
    15  	var StatusOK = osquery.ExtensionStatus{Code: 0, Message: "OK"}
    16  	var calledQueryCtx QueryContext
    17  	plugin := NewPlugin(
    18  		"mock",
    19  		[]ColumnDefinition{
    20  			TextColumn("text"),
    21  			IntegerColumn("integer"),
    22  			BigIntColumn("big_int"),
    23  			DoubleColumn("double"),
    24  		},
    25  		func(ctx context.Context, queryCtx QueryContext) ([]map[string]string, error) {
    26  			calledQueryCtx = queryCtx
    27  			return []map[string]string{
    28  				{
    29  					"text":    "hello world",
    30  					"integer": "123",
    31  					"big_int": "-1234567890",
    32  					"double":  "3.14159",
    33  				},
    34  			}, nil
    35  		})
    36  
    37  	// Basic methods
    38  	assert.Equal(t, "table", plugin.RegistryName())
    39  	assert.Equal(t, "mock", plugin.Name())
    40  	assert.Equal(t, StatusOK, plugin.Ping())
    41  	assert.Equal(t, osquery.ExtensionPluginResponse{
    42  		{"id": "column", "name": "text", "type": "TEXT", "op": "0"},
    43  		{"id": "column", "name": "integer", "type": "INTEGER", "op": "0"},
    44  		{"id": "column", "name": "big_int", "type": "BIGINT", "op": "0"},
    45  		{"id": "column", "name": "double", "type": "DOUBLE", "op": "0"},
    46  	}, plugin.Routes())
    47  
    48  	// Call explicit columns action
    49  	resp := plugin.Call(context.Background(), osquery.ExtensionPluginRequest{"action": "columns"})
    50  	assert.Equal(t, &StatusOK, resp.Status)
    51  	assert.Equal(t, osquery.ExtensionPluginResponse{
    52  		{"id": "column", "name": "text", "type": "TEXT", "op": "0"},
    53  		{"id": "column", "name": "integer", "type": "INTEGER", "op": "0"},
    54  		{"id": "column", "name": "big_int", "type": "BIGINT", "op": "0"},
    55  		{"id": "column", "name": "double", "type": "DOUBLE", "op": "0"},
    56  	}, resp.Response)
    57  
    58  	// Call with good action and context
    59  	resp = plugin.Call(context.Background(), osquery.ExtensionPluginRequest{"action": "generate", "context": "{}"})
    60  	assert.Equal(t, QueryContext{map[string]ConstraintList{}}, calledQueryCtx)
    61  	assert.Equal(t, &StatusOK, resp.Status)
    62  	assert.Equal(t, osquery.ExtensionPluginResponse{
    63  		{
    64  			"text":    "hello world",
    65  			"integer": "123",
    66  			"big_int": "-1234567890",
    67  			"double":  "3.14159",
    68  		},
    69  	}, resp.Response)
    70  }
    71  
    72  func TestTablePluginErrors(t *testing.T) {
    73  	var called bool
    74  	plugin := NewPlugin(
    75  		"mock",
    76  		[]ColumnDefinition{
    77  			TextColumn("text"),
    78  			IntegerColumn("integer"),
    79  			BigIntColumn("big_int"),
    80  			DoubleColumn("double"),
    81  		},
    82  		func(ctx context.Context, queryCtx QueryContext) ([]map[string]string, error) {
    83  			called = true
    84  			return nil, errors.New("foobar")
    85  		},
    86  	)
    87  
    88  	// Call with bad actions
    89  	assert.Equal(t, int32(1), plugin.Call(context.Background(), osquery.ExtensionPluginRequest{}).Status.Code)
    90  	assert.False(t, called)
    91  	assert.Equal(t, int32(1), plugin.Call(context.Background(), osquery.ExtensionPluginRequest{"action": "bad"}).Status.Code)
    92  	assert.False(t, called)
    93  
    94  	// Call with good action but generate fails
    95  	assert.Equal(t, int32(1), plugin.Call(context.Background(), osquery.ExtensionPluginRequest{"action": "generate", "context": "{[]}"}).Status.Code)
    96  	assert.False(t, called)
    97  
    98  	// Call with good action but generate fails
    99  	resp := plugin.Call(context.Background(), osquery.ExtensionPluginRequest{"action": "generate", "context": "{}"})
   100  	assert.True(t, called)
   101  	assert.Equal(t, int32(1), resp.Status.Code)
   102  	assert.Equal(t, "error generating table: foobar", resp.Status.Message)
   103  
   104  }
   105  
   106  func TestParseConstraintList(t *testing.T) {
   107  	var testCases = []struct {
   108  		json        string
   109  		constraints []Constraint
   110  		shouldErr   bool
   111  	}{
   112  		{
   113  			json:      "bad",
   114  			shouldErr: true,
   115  		},
   116  		{
   117  			json:      `{"foo": "bar"}`,
   118  			shouldErr: true,
   119  		},
   120  		{
   121  			json:        `""`,
   122  			constraints: []Constraint{},
   123  		},
   124  		{
   125  			json: `[{"op":"2","expr":"foo"}]`,
   126  			constraints: []Constraint{
   127  				{OperatorEquals, "foo"},
   128  			},
   129  		},
   130  		{
   131  			json: `[{"op":"4","expr":"3"},{"op":"16","expr":"4"}]`,
   132  			constraints: []Constraint{
   133  				{OperatorGreaterThan, "3"},
   134  				{OperatorLessThan, "4"},
   135  			},
   136  		},
   137  	}
   138  
   139  	for _, tt := range testCases {
   140  		t.Run("", func(t *testing.T) {
   141  			constraints, err := parseConstraintList(json.RawMessage(tt.json))
   142  			if tt.shouldErr {
   143  				assert.NotNil(t, err)
   144  			} else {
   145  				assert.Equal(t, tt.constraints, constraints)
   146  			}
   147  		})
   148  	}
   149  }
   150  
   151  func TestParseQueryContext(t *testing.T) {
   152  	var testCases = []struct {
   153  		json      string
   154  		context   QueryContext
   155  		shouldErr bool
   156  	}{
   157  		{
   158  			json:    "",
   159  			context: QueryContext{map[string]ConstraintList{}},
   160  		},
   161  		{
   162  			json: `
   163  {
   164    "constraints":[
   165      {
   166        "name":"big_int",
   167        "list":"",
   168        "affinity":"BIGINT"
   169      },
   170      {
   171        "name":"double",
   172        "list":"",
   173        "affinity":"DOUBLE"
   174      },
   175      {
   176        "name":"integer",
   177        "list":"",
   178        "affinity":"INTEGER"
   179      },
   180      {
   181        "name":"text",
   182        "list":[
   183          {
   184            "op":"2",
   185            "expr":"foo"
   186          }
   187        ],
   188        "affinity":"TEXT"
   189      }
   190    ]
   191  }`,
   192  			context: QueryContext{map[string]ConstraintList{
   193  				"big_int": {ColumnTypeBigInt, []Constraint{}},
   194  				"double":  {ColumnTypeDouble, []Constraint{}},
   195  				"integer": {ColumnTypeInteger, []Constraint{}},
   196  				"text":    {ColumnTypeText, []Constraint{{OperatorEquals, "foo"}}},
   197  			}},
   198  		},
   199  		{
   200  			json: `
   201  {
   202    "constraints":[
   203      {
   204        "name":"big_int",
   205        "list":"",
   206        "affinity":"BIGINT"
   207      },
   208      {
   209        "name":"double",
   210        "list":[
   211          {
   212            "op":"32",
   213            "expr":"3.1"
   214          }
   215        ],
   216        "affinity":"DOUBLE"
   217      },
   218      {
   219        "name":"integer",
   220        "list":"",
   221        "affinity":"INTEGER"
   222      },
   223      {
   224        "name":"text",
   225        "list":[
   226          {
   227            "op":"2",
   228            "expr":"foobar"
   229          }
   230        ],
   231        "affinity":"TEXT"
   232      }
   233    ]
   234  }
   235  `,
   236  			context: QueryContext{map[string]ConstraintList{
   237  				"big_int": {ColumnTypeBigInt, []Constraint{}},
   238  				"double":  {ColumnTypeDouble, []Constraint{{OperatorGreaterThanOrEquals, "3.1"}}},
   239  				"integer": {ColumnTypeInteger, []Constraint{}},
   240  				"text":    {ColumnTypeText, []Constraint{{OperatorEquals, "foobar"}}},
   241  			}},
   242  		},
   243  	}
   244  	for _, tt := range testCases {
   245  		t.Run("", func(t *testing.T) {
   246  			context, err := parseQueryContext(tt.json)
   247  			if tt.shouldErr {
   248  				assert.NotNil(t, err)
   249  			} else {
   250  				assert.Equal(t, &tt.context, context)
   251  			}
   252  		})
   253  	}
   254  }
   255  
   256  func TestParseVaryingQueryContexts(t *testing.T) {
   257  	var testCases = []struct {
   258  		json            string
   259  		expectedContext *QueryContext
   260  		shouldErr       bool
   261  	}{
   262  		{ // Stringy JSON from osquery version < 3
   263  			`{"constraints":[{"name":"domain","list":[{"op":"2","expr":"kolide.co"}],"affinity":"TEXT"},{"name":"email","list":"","affinity":"TEXT"}]}`,
   264  			&QueryContext{
   265  				Constraints: map[string]ConstraintList{
   266  					"domain": {Affinity: "TEXT", Constraints: []Constraint{{Operator: OperatorEquals, Expression: "kolide.co"}}},
   267  					"email":  {Affinity: "TEXT", Constraints: []Constraint{}},
   268  				},
   269  			},
   270  			false,
   271  		},
   272  		{ // Strongly typed JSON from osquery version > 3
   273  			`{"constraints":[{"name":"domain","list":[{"op":2,"expr":"kolide.co"}],"affinity":"TEXT"},{"name":"email","list":[],"affinity":"TEXT"}]}`,
   274  			&QueryContext{
   275  				Constraints: map[string]ConstraintList{
   276  					"domain": {Affinity: "TEXT", Constraints: []Constraint{{Operator: OperatorEquals, Expression: "kolide.co"}}},
   277  					"email":  {Affinity: "TEXT", Constraints: []Constraint{}},
   278  				},
   279  			},
   280  			false,
   281  		},
   282  		{ // Stringy
   283  			`{"constraints":[{"name":"path","list":[{"op":"65","expr":"%foobar"}],"affinity":"TEXT"},{"name":"query","list":[{"op":"2","expr":"kMDItemFSName = \"google*\""}],"affinity":"TEXT"}]}`,
   284  			&QueryContext{
   285  				Constraints: map[string]ConstraintList{
   286  					"path":  {Affinity: "TEXT", Constraints: []Constraint{{Operator: OperatorLike, Expression: "%foobar"}}},
   287  					"query": {Affinity: "TEXT", Constraints: []Constraint{{Operator: OperatorEquals, Expression: "kMDItemFSName = \"google*\""}}},
   288  				},
   289  			},
   290  			false,
   291  		},
   292  		{ // Strong
   293  			`{"constraints":[{"name":"path","list":[{"op":65,"expr":"%foobar"}],"affinity":"TEXT"},{"name":"query","list":[{"op":2,"expr":"kMDItemFSName = \"google*\""}],"affinity":"TEXT"}]}`,
   294  			&QueryContext{
   295  				Constraints: map[string]ConstraintList{
   296  					"path":  {Affinity: "TEXT", Constraints: []Constraint{{Operator: OperatorLike, Expression: "%foobar"}}},
   297  					"query": {Affinity: "TEXT", Constraints: []Constraint{{Operator: OperatorEquals, Expression: "kMDItemFSName = \"google*\""}}},
   298  				},
   299  			},
   300  			false,
   301  		},
   302  
   303  		// Error cases
   304  		{`{bad json}`, nil, true},
   305  		{`{"constraints":[{"name":"foo","list":["bar", "baz"],"affinity":"TEXT"}]`, nil, true},
   306  	}
   307  
   308  	for _, tt := range testCases {
   309  		t.Run("", func(t *testing.T) {
   310  			context, err := parseQueryContext(tt.json)
   311  			if tt.shouldErr {
   312  				require.Error(t, err)
   313  				return
   314  			}
   315  			require.NoError(t, err)
   316  
   317  			assert.Equal(t, tt.expectedContext, context)
   318  		})
   319  	}
   320  }