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 }