github.com/Uptycs/basequery-go@v0.8.0/plugin/table/table.go (about) 1 // Package table creates an osquery table plugin. 2 package table 3 4 import ( 5 "context" 6 "encoding/json" 7 "strconv" 8 9 "github.com/Uptycs/basequery-go/gen/osquery" 10 "github.com/pkg/errors" 11 ) 12 13 // GenerateFunc returns the rows generated by the table. The ctx argument 14 // should be checked for cancellation if the generation performs a 15 // substantial amount of work. The queryContext argument provides the 16 // deserialized JSON query context from osquery. 17 type GenerateFunc func(ctx context.Context, queryContext QueryContext) ([]map[string]string, error) 18 19 // InsertFunc is optional implementation that can be used to implement insert SQL semantics 20 type InsertFunc func(ctx context.Context, autoRowId bool, row []interface{}) ([]map[string]string, error) 21 22 // UpdateFunc is optional implementation that can be used to implement update SQL semantics 23 type UpdateFunc func(ctx context.Context, rowID int64, row []interface{}) error 24 25 // DeleteFunc is optional implementation that can be used to implement delete SQL semantics 26 type DeleteFunc func(ctx context.Context, rowID int64) error 27 28 // Plugin structure holds the plugin details. 29 type Plugin struct { 30 name string 31 columns []ColumnDefinition 32 generate GenerateFunc 33 insert InsertFunc 34 update UpdateFunc 35 delete DeleteFunc 36 } 37 38 // NewPlugin is helper method to create plugin structure. 39 func NewPlugin(name string, columns []ColumnDefinition, gen GenerateFunc) *Plugin { 40 return &Plugin{ 41 name: name, 42 columns: columns, 43 generate: gen, 44 } 45 } 46 47 // NewMutablePlugin is helper method to create mutable plugin structure. 48 func NewMutablePlugin(name string, columns []ColumnDefinition, gen GenerateFunc, ins InsertFunc, upd UpdateFunc, del DeleteFunc) *Plugin { 49 return &Plugin{ 50 name: name, 51 columns: columns, 52 generate: gen, 53 insert: ins, 54 update: upd, 55 delete: del, 56 } 57 } 58 59 func createError(prefix string, err error) osquery.ExtensionResponse { 60 msg := prefix 61 if err != nil { 62 msg += err.Error() 63 } 64 return osquery.ExtensionResponse{ 65 Status: &osquery.ExtensionStatus{ 66 Code: 1, 67 Message: msg, 68 }, 69 } 70 } 71 72 // Name returns the plugin name. 73 func (t *Plugin) Name() string { 74 return t.name 75 } 76 77 // RegistryName returns the plugin type, which is always "table" for table plugin. 78 func (t *Plugin) RegistryName() string { 79 return "table" 80 } 81 82 // Routes returns the table columns definitions. 83 func (t *Plugin) Routes() osquery.ExtensionPluginResponse { 84 routes := []map[string]string{} 85 for _, col := range t.columns { 86 routes = append(routes, map[string]string{ 87 "id": "column", 88 "name": col.Name, 89 "type": string(col.Type), 90 "op": strconv.Itoa(int(col.Op)), 91 }) 92 } 93 return routes 94 } 95 96 // Call is invoked to generate the table contents or to get the column details. 97 func (t *Plugin) Call(ctx context.Context, request osquery.ExtensionPluginRequest) osquery.ExtensionResponse { 98 ok := osquery.ExtensionStatus{Code: 0, Message: "OK"} 99 switch request["action"] { 100 case "generate": 101 queryContext, err := parseQueryContext(request["context"]) 102 if err != nil { 103 return createError("error parsing context JSON: ", err) 104 } 105 106 rows, err := t.generate(ctx, *queryContext) 107 if err != nil { 108 return createError("error generating table: ", err) 109 } 110 111 return osquery.ExtensionResponse{Status: &ok, Response: rows} 112 113 case "insert": 114 if t.insert == nil { 115 return createError("'insert' not implemented by table: "+t.name, nil) 116 } 117 118 row, err := parseRow(request["json_value_array"]) 119 if err != nil { 120 return createError("invalid data to insert: ", err) 121 } 122 123 autoRowID, err := strconv.ParseBool(request["auto_rowid"]) 124 if err != nil { 125 return createError("invalid value for auto_rowid: ", err) 126 } 127 128 rows, err := t.insert(ctx, autoRowID, row) 129 if err != nil { 130 return createError("error inserting into table: ", err) 131 } 132 133 return osquery.ExtensionResponse{Status: &ok, Response: rows} 134 135 case "update": 136 if t.update == nil { 137 return createError("'update' not implemented by table: "+t.name, nil) 138 } 139 140 rowID, err := strconv.ParseInt(request["id"], 10, 64) 141 if err != nil { 142 return createError("invalid row id to update: ", err) 143 } 144 145 row, err := parseRow(request["json_value_array"]) 146 if err != nil { 147 return createError("invalid data to update: ", err) 148 } 149 150 err = t.update(ctx, rowID, row) 151 if err != nil { 152 return createError("error updating table: ", err) 153 } 154 155 return osquery.ExtensionResponse{Status: &ok, Response: []map[string]string{{"status": "success"}}} 156 157 case "delete": 158 if t.delete == nil { 159 return createError("'delete' not implemented by table: "+t.name, nil) 160 } 161 162 rowID, err := strconv.ParseInt(request["id"], 10, 64) 163 if err != nil { 164 return createError("invalid row id to delete: ", err) 165 } 166 167 err = t.delete(ctx, rowID) 168 if err != nil { 169 return createError("error deleting from table: ", err) 170 } 171 172 return osquery.ExtensionResponse{Status: &ok, Response: []map[string]string{{"status": "success"}}} 173 174 case "columns": 175 return osquery.ExtensionResponse{Status: &ok, Response: t.Routes()} 176 177 default: 178 return createError("unknown action: "+request["action"], nil) 179 } 180 181 } 182 183 // Ping returns static OK response. 184 func (t *Plugin) Ping() osquery.ExtensionStatus { 185 return osquery.ExtensionStatus{Code: 0, Message: "OK"} 186 } 187 188 // Shutdown is a no-op for table plugins. 189 func (t *Plugin) Shutdown() {} 190 191 // ColumnDefinition defines the relevant information for a column in a table 192 // plugin. Both values are mandatory. Prefer using the *Column helpers to 193 // create ColumnDefinition structs. 194 type ColumnDefinition struct { 195 Name string 196 Type ColumnType 197 Op ColumnOptions 198 } 199 200 // TextColumn is a helper for defining columns containing strings. 201 func TextColumn(name string, options ...ColumnOptions) ColumnDefinition { 202 return ColumnDefinition{ 203 Name: name, 204 Type: ColumnTypeText, 205 Op: getColumnOption(options...), 206 } 207 } 208 209 // IntegerColumn is a helper for defining columns containing integers. 210 func IntegerColumn(name string, options ...ColumnOptions) ColumnDefinition { 211 return ColumnDefinition{ 212 Name: name, 213 Type: ColumnTypeInteger, 214 Op: getColumnOption(options...), 215 } 216 } 217 218 // BigIntColumn is a helper for defining columns containing big integers. 219 func BigIntColumn(name string, options ...ColumnOptions) ColumnDefinition { 220 return ColumnDefinition{ 221 Name: name, 222 Type: ColumnTypeBigInt, 223 Op: getColumnOption(options...), 224 } 225 } 226 227 // DoubleColumn is a helper for defining columns containing floating point 228 // values. 229 func DoubleColumn(name string, options ...ColumnOptions) ColumnDefinition { 230 return ColumnDefinition{ 231 Name: name, 232 Type: ColumnTypeDouble, 233 Op: getColumnOption(options...), 234 } 235 } 236 237 func getColumnOption(options ...ColumnOptions) ColumnOptions { 238 op := DEFAULT 239 if len(options) > 0 { 240 for _, option := range options { 241 op |= option 242 } 243 } 244 return op 245 } 246 247 // ColumnType is a strongly typed representation of the data type string for a 248 // column definition. The named constants should be used. 249 type ColumnType string 250 251 // The following column types are defined in osquery tables.h. 252 const ( 253 ColumnTypeText ColumnType = "TEXT" 254 ColumnTypeInteger = "INTEGER" 255 ColumnTypeBigInt = "BIGINT" 256 ColumnTypeDouble = "DOUBLE" 257 ) 258 259 // ColumnOptions for marking columns 260 type ColumnOptions int 261 262 const ( 263 // DEFAULT means no special column options. 264 DEFAULT ColumnOptions = iota // 0 265 266 // INDEX treats this column as a primary key. 267 INDEX = iota // 1 268 269 // REQUIRED column MUST be included in the query predicate. 270 REQUIRED = iota // 2 271 272 // ADDITIONAL column is used to generate additional information. 273 ADDITIONAL = iota + 1 // 3 + 1 274 275 // OPTIMIZED column can be used to optimize the query. 276 OPTIMIZED = iota + 4 // 4 + 4 277 278 // HIDDEN column should be hidden from '*'' selects. 279 HIDDEN = iota + 11 // 5 + 11 280 ) 281 282 // QueryContext contains the constraints from the WHERE clause of the query, 283 // that can optionally be used to optimize the table generation. Note that the 284 // osquery SQLite engine will perform the filtering with these constraints, so 285 // it is not mandatory that they be used in table generation. 286 type QueryContext struct { 287 // Constraints is a map from column name to the details of the 288 // constraints on that column. 289 Constraints map[string]ConstraintList 290 } 291 292 // ConstraintList contains the details of the constraints for the given column. 293 type ConstraintList struct { 294 Affinity ColumnType 295 Constraints []Constraint 296 } 297 298 // Constraint contains both an operator and an expression that are applied as 299 // constraints in the query. 300 type Constraint struct { 301 Operator Operator 302 Expression string 303 } 304 305 // Operator is an enum of the osquery operators. 306 type Operator int 307 308 // The following operators are dfined in osquery tables.h. 309 const ( 310 OperatorEquals Operator = 2 311 OperatorGreaterThan = 4 312 OperatorLessThanOrEquals = 8 313 OperatorLessThan = 16 314 OperatorGreaterThanOrEquals = 32 315 OperatorMatch = 64 316 OperatorLike = 65 317 OperatorGlob = 66 318 OperatorRegexp = 67 319 OperatorUnique = 1 320 ) 321 322 // The following types and functions exist for parsing of the queryContext 323 // JSON and are not made public. 324 type queryContextJSON struct { 325 Constraints []constraintListJSON `json:"constraints"` 326 } 327 328 type constraintListJSON struct { 329 Name string `json:"name"` 330 Affinity string `json:"affinity"` 331 List json.RawMessage `json:"list"` 332 } 333 334 func parseQueryContext(ctxJSON string) (*QueryContext, error) { 335 ctx := QueryContext{map[string]ConstraintList{}} 336 if ctxJSON == "" { 337 return &ctx, nil 338 } 339 340 var parsed queryContextJSON 341 err := json.Unmarshal([]byte(ctxJSON), &parsed) 342 if err != nil { 343 return nil, errors.Wrap(err, "unmarshaling context JSON") 344 } 345 346 for _, cList := range parsed.Constraints { 347 constraints, err := parseConstraintList(cList.List) 348 if err != nil { 349 return nil, err 350 } 351 352 ctx.Constraints[cList.Name] = ConstraintList{ 353 Affinity: ColumnType(cList.Affinity), 354 Constraints: constraints, 355 } 356 } 357 358 return &ctx, nil 359 } 360 361 func parseRow(row string) ([]interface{}, error) { 362 if row == "" { 363 return nil, errors.Errorf("invalid data to insert") 364 } 365 366 var parsed []interface{} 367 err := json.Unmarshal([]byte(row), &parsed) 368 if err != nil { 369 return nil, errors.Wrap(err, "unmarshaling JSON") 370 } 371 372 return parsed, nil 373 } 374 375 func parseConstraintList(constraints json.RawMessage) ([]Constraint, error) { 376 var str string 377 err := json.Unmarshal(constraints, &str) 378 if err == nil { 379 // string indicates empty list 380 return []Constraint{}, nil 381 } 382 383 var cList []map[string]interface{} 384 err = json.Unmarshal(constraints, &cList) 385 if err != nil { 386 // cannot do anything with other types 387 return nil, errors.Errorf("unexpected context list: %s", string(constraints)) 388 } 389 390 cl := []Constraint{} 391 for _, c := range cList { 392 var op Operator 393 switch opVal := c["op"].(type) { 394 case string: // osquery < 3.0 with stringy types 395 opInt, err := strconv.Atoi(opVal) 396 if err != nil { 397 return nil, errors.Errorf("parsing operator int: %s", c["op"]) 398 } 399 op = Operator(opInt) 400 case float64: // osquery > 3.0 with strong types 401 op = Operator(opVal) 402 default: 403 return nil, errors.Errorf("cannot parse type %T", opVal) 404 } 405 406 expr, ok := c["expr"].(string) 407 if !ok { 408 return nil, errors.Errorf("expr should be string: %s", c["expr"]) 409 } 410 411 cl = append(cl, Constraint{ 412 Operator: op, 413 Expression: expr, 414 }) 415 } 416 return cl, nil 417 }