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

     1  // Package distributed creates an osquery distributed query plugin.
     2  package distributed
     3  
     4  import (
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"reflect"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/Uptycs/basequery-go/gen/osquery"
    14  )
    15  
    16  // GetQueriesResult contains the information about which queries the
    17  // distributed system should run.
    18  type GetQueriesResult struct {
    19  	// Queries is a map from query name to query SQL
    20  	Queries map[string]string `json:"queries"`
    21  	// Discovery is used for "discovery" queries in the distributed
    22  	// system. When used, discovery queries should be specified with query
    23  	// name as the key and the discover query SQL as the value. If this is
    24  	// nonempty, only queries for which the associated discovery query
    25  	// returns results will be run in osquery.
    26  	Discovery map[string]string `json:"discovery,omitempty"`
    27  	// AccelerateSeconds can be specified to have "accelerated" checkins
    28  	// for a given number of seconds after this checkin. Currently this
    29  	// means that checkins will occur every 5 seconds.
    30  	AccelerateSeconds int `json:"accelerate,omitempty"`
    31  }
    32  
    33  // GetQueriesFunc returns the queries that should be executed.
    34  // The returned map should include the query name as the keys, and the query
    35  // text as values. Results will be returned corresponding to the provided name.
    36  // The context argument can optionally be used for cancellation in long-running
    37  // operations.
    38  type GetQueriesFunc func(ctx context.Context) (*GetQueriesResult, error)
    39  
    40  // Result contains the status and results for a distributed query.
    41  type Result struct {
    42  	// QueryName is the name that was originally provided for the query.
    43  	QueryName string `json:"query_name"`
    44  	// Status is an integer status code for the query execution (0 = OK)
    45  	Status int `json:"status"`
    46  	// Rows is the result rows of the query.
    47  	Rows []map[string]string `json:"rows"`
    48  }
    49  
    50  // WriteResultsFunc writes the results of the executed distributed queries. The
    51  // query results will be serialized JSON in the results map with the query name
    52  // as the key.
    53  type WriteResultsFunc func(ctx context.Context, results []Result) error
    54  
    55  // Plugin is an osquery configuration plugin. Plugin implements the OsqueryPlugin
    56  // interface.
    57  type Plugin struct {
    58  	name         string
    59  	getQueries   GetQueriesFunc
    60  	writeResults WriteResultsFunc
    61  }
    62  
    63  // NewPlugin takes the distributed query functions and returns a struct
    64  // implementing the OsqueryPlugin interface. Use this to wrap the appropriate
    65  // functions into an osquery plugin.
    66  func NewPlugin(name string, getQueries GetQueriesFunc, writeResults WriteResultsFunc) *Plugin {
    67  	return &Plugin{name: name, getQueries: getQueries, writeResults: writeResults}
    68  }
    69  
    70  // Name returns distributed plugin name.
    71  func (t *Plugin) Name() string {
    72  	return t.name
    73  }
    74  
    75  // Registry name for distributed plugins
    76  const distributedRegistryName = "distributed"
    77  
    78  // RegistryName returns the static string "distributed" for this plugin.
    79  func (t *Plugin) RegistryName() string {
    80  	return distributedRegistryName
    81  }
    82  
    83  // Routes returns empty plugin response for distributed plugin.
    84  func (t *Plugin) Routes() osquery.ExtensionPluginResponse {
    85  	return osquery.ExtensionPluginResponse{}
    86  }
    87  
    88  // Ping returns static OK response.
    89  func (t *Plugin) Ping() osquery.ExtensionStatus {
    90  	return osquery.ExtensionStatus{Code: 0, Message: "OK"}
    91  }
    92  
    93  // Key that the request method is stored under
    94  const requestActionKey = "action"
    95  
    96  // Action value used when queries are requested
    97  const getQueriesAction = "getQueries"
    98  
    99  // Action value used when results are written
   100  const writeResultsAction = "writeResults"
   101  
   102  // Key that results are stored under
   103  const requestResultKey = "results"
   104  
   105  // OsqueryInt handles unmarshaling integers in noncanonical osquery json.
   106  type OsqueryInt int
   107  
   108  // UnmarshalJSON marshals a json string that is convertable to an int, for
   109  // example "234" -> 234.
   110  func (oi *OsqueryInt) UnmarshalJSON(buff []byte) error {
   111  	s := string(buff)
   112  	if strings.Contains(s, `"`) {
   113  		unquoted, err := strconv.Unquote(s)
   114  		if err != nil {
   115  			return &json.UnmarshalTypeError{
   116  				Value:  string(buff),
   117  				Type:   reflect.TypeOf(oi),
   118  				Struct: "statuses",
   119  			}
   120  		}
   121  		s = unquoted
   122  	}
   123  
   124  	if len(s) == 0 {
   125  		*oi = OsqueryInt(0)
   126  		return nil
   127  	}
   128  
   129  	parsedInt, err := strconv.ParseInt(s, 10, 32)
   130  	if err != nil {
   131  		return &json.UnmarshalTypeError{
   132  			Value:  string(buff),
   133  			Type:   reflect.TypeOf(oi),
   134  			Struct: "statuses",
   135  		}
   136  	}
   137  
   138  	*oi = OsqueryInt(parsedInt)
   139  	return nil
   140  }
   141  
   142  // ResultsStruct is used for unmarshalling the results passed from osquery.
   143  type ResultsStruct struct {
   144  	Queries  map[string][]map[string]string `json:"queries"`
   145  	Statuses map[string]OsqueryInt          `json:"statuses"`
   146  }
   147  
   148  // UnmarshalJSON turns structurally inconsistent osquery json into a ResultsStruct.
   149  func (rs *ResultsStruct) UnmarshalJSON(buff []byte) error {
   150  	emptyRow := []map[string]string{}
   151  	rs.Queries = make(map[string][]map[string]string)
   152  	rs.Statuses = make(map[string]OsqueryInt)
   153  	// Queries can be []map[string]string OR an empty string
   154  	// so we need to deal with an interface to accomodate two types
   155  	intermediate := struct {
   156  		Queries  map[string]interface{} `json:"queries"`
   157  		Statuses map[string]OsqueryInt  `json:"statuses"`
   158  	}{}
   159  	if err := json.Unmarshal(buff, &intermediate); err != nil {
   160  		return err
   161  	}
   162  	for queryName, status := range intermediate.Statuses {
   163  		rs.Statuses[queryName] = status
   164  		// Sometimes we have a status but don't have a corresponding
   165  		// result.
   166  		queryResult, ok := intermediate.Queries[queryName]
   167  		if !ok {
   168  			rs.Queries[queryName] = emptyRow
   169  			continue
   170  		}
   171  		// Deal with structurally inconsistent results, sometimes a query
   172  		// without any results is just a name with an empty string.
   173  		switch val := queryResult.(type) {
   174  		case string:
   175  			rs.Queries[queryName] = emptyRow
   176  		case []interface{}:
   177  			results, err := convertRows(val)
   178  			if err != nil {
   179  				return err
   180  			}
   181  			rs.Queries[queryName] = results
   182  		default:
   183  			return fmt.Errorf("results for %q unknown type", queryName)
   184  		}
   185  	}
   186  	return nil
   187  }
   188  
   189  func (rs *ResultsStruct) toResults() ([]Result, error) {
   190  	var results []Result
   191  	for queryName, rows := range rs.Queries {
   192  		result := Result{
   193  			QueryName: queryName,
   194  			Rows:      rows,
   195  			Status:    int(rs.Statuses[queryName]),
   196  		}
   197  		results = append(results, result)
   198  	}
   199  	return results, nil
   200  }
   201  
   202  func convertRows(rows []interface{}) ([]map[string]string, error) {
   203  	var results []map[string]string
   204  	for _, intf := range rows {
   205  		row, ok := intf.(map[string]interface{})
   206  		if !ok {
   207  			return nil, errors.New("invalid row type for query")
   208  		}
   209  		result := make(map[string]string)
   210  		for col, val := range row {
   211  			sval, ok := val.(string)
   212  			if !ok {
   213  				return nil, fmt.Errorf("invalid type for col %q", col)
   214  			}
   215  			result[col] = sval
   216  		}
   217  		results = append(results, result)
   218  	}
   219  	return results, nil
   220  }
   221  
   222  // Call is the function invoked for distributed read and write requests. "request" should have "action" that is
   223  // "getQueriesAction" or "writeResultsAction". "getQueriesAction" should query the distributed endpoint and get
   224  // pending queries to run. "writeResultsAction" is used when there are distributed write response to be sent to target.
   225  func (t *Plugin) Call(ctx context.Context, request osquery.ExtensionPluginRequest) osquery.ExtensionResponse {
   226  	switch request[requestActionKey] {
   227  	case getQueriesAction:
   228  		queries, err := t.getQueries(ctx)
   229  		if err != nil {
   230  			return osquery.ExtensionResponse{
   231  				Status: &osquery.ExtensionStatus{
   232  					Code:    1,
   233  					Message: "error getting queries: " + err.Error(),
   234  				},
   235  			}
   236  		}
   237  
   238  		queryJSON, err := json.Marshal(queries)
   239  		if err != nil {
   240  			return osquery.ExtensionResponse{
   241  				Status: &osquery.ExtensionStatus{
   242  					Code:    1,
   243  					Message: "error marshalling queries: " + err.Error(),
   244  				},
   245  			}
   246  		}
   247  
   248  		return osquery.ExtensionResponse{
   249  			Status:   &osquery.ExtensionStatus{Code: 0, Message: "OK"},
   250  			Response: osquery.ExtensionPluginResponse{map[string]string{"results": string(queryJSON)}},
   251  		}
   252  
   253  	case writeResultsAction:
   254  		var rs ResultsStruct
   255  		if err := json.Unmarshal([]byte(request[requestResultKey]), &rs); err != nil {
   256  			return osquery.ExtensionResponse{
   257  				Status: &osquery.ExtensionStatus{
   258  					Code:    1,
   259  					Message: "error unmarshalling results: " + err.Error(),
   260  				},
   261  			}
   262  		}
   263  		results, err := rs.toResults()
   264  		if err != nil {
   265  			return osquery.ExtensionResponse{
   266  				Status: &osquery.ExtensionStatus{
   267  					Code:    1,
   268  					Message: "error writing results: " + err.Error(),
   269  				},
   270  			}
   271  		}
   272  		// invoke callback
   273  		err = t.writeResults(ctx, results)
   274  		if err != nil {
   275  			return osquery.ExtensionResponse{
   276  				Status: &osquery.ExtensionStatus{
   277  					Code:    1,
   278  					Message: "error writing results: " + err.Error(),
   279  				},
   280  			}
   281  		}
   282  
   283  		return osquery.ExtensionResponse{
   284  			Status:   &osquery.ExtensionStatus{Code: 0, Message: "OK"},
   285  			Response: osquery.ExtensionPluginResponse{},
   286  		}
   287  
   288  	default:
   289  		return osquery.ExtensionResponse{
   290  			Status: &osquery.ExtensionStatus{
   291  				Code:    1,
   292  				Message: "unknown action: " + request["action"],
   293  			},
   294  		}
   295  	}
   296  
   297  }
   298  
   299  // Shutdown is a no-op for distributed plugins.
   300  func (t *Plugin) Shutdown() {}