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  }