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() {}