fleet/cmd/osquery-perf/agent.go
Tomas Touceda 8600d71d35
Add osquery perf (#2190)
* Add osquery perf

* Update dockerfile and gh action
2021-09-22 17:18:55 -03:00

285 lines
7.3 KiB
Go

package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"flag"
"fmt"
"log"
"math/rand"
"net/http"
"strings"
"text/template"
"time"
"github.com/google/uuid"
)
type Agent struct {
ServerAddress string
EnrollSecret string
NodeKey string
UUID string
Client http.Client
ConfigInterval time.Duration
QueryInterval time.Duration
Templates *template.Template
strings map[string]string
}
func NewAgent(serverAddress, enrollSecret string, templates *template.Template, configInterval, queryInterval time.Duration) *Agent {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
transport.DisableCompression = true
return &Agent{
ServerAddress: serverAddress,
EnrollSecret: enrollSecret,
Templates: templates,
ConfigInterval: configInterval,
QueryInterval: queryInterval,
UUID: uuid.New().String(),
Client: http.Client{Transport: transport},
strings: make(map[string]string),
}
}
type enrollResponse struct {
NodeKey string `json:"node_key"`
}
type distributedReadResponse struct {
Queries map[string]string `json:"queries"`
}
func (a *Agent) runLoop() {
a.Enroll()
a.Config()
resp, err := a.DistributedRead()
if err != nil {
log.Println(err)
} else {
if len(resp.Queries) > 0 {
a.DistributedWrite(resp.Queries)
}
}
configTicker := time.Tick(a.ConfigInterval)
liveQueryTicker := time.Tick(a.QueryInterval)
for {
select {
case <-configTicker:
a.Config()
case <-liveQueryTicker:
resp, err := a.DistributedRead()
if err != nil {
log.Println(err)
} else {
if len(resp.Queries) > 0 {
a.DistributedWrite(resp.Queries)
}
}
}
}
}
const stringVals = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_."
func (a *Agent) randomString(n int) string {
sb := strings.Builder{}
sb.Grow(n)
for i := 0; i < n; i++ {
sb.WriteByte(stringVals[rand.Int63()%int64(len(stringVals))])
}
return sb.String()
}
func (a *Agent) CachedString(key string) string {
if val, ok := a.strings[key]; ok {
return val
}
val := a.randomString(12)
a.strings[key] = val
return val
}
func (a *Agent) Enroll() {
var body bytes.Buffer
if err := a.Templates.ExecuteTemplate(&body, "enroll", a); err != nil {
log.Println("execute template:", err)
return
}
req, err := http.NewRequest("POST", a.ServerAddress+"/api/v1/osquery/enroll", &body)
if err != nil {
log.Println("create request:", err)
return
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("User-Agent", "osquery/4.6.0")
resp, err := a.Client.Do(req)
if err != nil {
log.Println("do request:", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Println("status:", resp.Status)
return
}
var parsedResp enrollResponse
if err := json.NewDecoder(resp.Body).Decode(&parsedResp); err != nil {
log.Println("json parse:", err)
return
}
a.NodeKey = parsedResp.NodeKey
}
func (a *Agent) Config() {
body := bytes.NewBufferString(`{"node_key": "` + a.NodeKey + `"}`)
req, err := http.NewRequest("POST", a.ServerAddress+"/api/v1/osquery/config", body)
if err != nil {
log.Println("create config request:", err)
return
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("User-Agent", "osquery/4.6.0")
resp, err := a.Client.Do(req)
if err != nil {
log.Println("do config request:", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Println("config status:", resp.Status)
return
}
// No need to read the config body
}
func (a *Agent) DistributedRead() (*distributedReadResponse, error) {
body := bytes.NewBufferString(`{"node_key": "` + a.NodeKey + `"}`)
req, err := http.NewRequest("POST", a.ServerAddress+"/api/v1/osquery/distributed/read", body)
if err != nil {
return nil, fmt.Errorf("create distributed read request: %s", err)
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("User-Agent", "osquery/4.6.0")
resp, err := a.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("do distributed read request: %s", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("distributed read status: %s", resp.Status)
}
var parsedResp distributedReadResponse
if err := json.NewDecoder(resp.Body).Decode(&parsedResp); err != nil {
return nil, fmt.Errorf("json parse distributed read response: %s", err)
}
return &parsedResp, nil
}
type distributedWriteRequest struct {
Queries map[string]json.RawMessage `json:"queries"`
Statuses map[string]string `json:"statuses"`
NodeKey string `json:"node_key"`
}
var defaultQueryResult = json.RawMessage(`[{"foo": "bar"}]`)
const statusSuccess = "0"
func (a *Agent) DistributedWrite(queries map[string]string) {
var body bytes.Buffer
if _, ok := queries["fleet_detail_query_network_interface"]; ok {
// Respond to label/detail queries
a.Templates.ExecuteTemplate(&body, "distributed_write", a)
} else {
// Return a generic response for any other queries
req := distributedWriteRequest{
Queries: make(map[string]json.RawMessage),
Statuses: make(map[string]string),
NodeKey: a.NodeKey,
}
for name := range queries {
req.Queries[name] = defaultQueryResult
req.Statuses[name] = statusSuccess
}
json.NewEncoder(&body).Encode(req)
}
req, err := http.NewRequest("POST", a.ServerAddress+"/api/v1/osquery/distributed/write", &body)
if err != nil {
log.Println("create distributed write request:", err)
return
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("User-Agent", "osquery/4.6.0")
resp, err := a.Client.Do(req)
if err != nil {
log.Println("do distributed write request:", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Println("distributed write status:", resp.Status)
return
}
// No need to read the distributed write body
}
func main() {
serverURL := flag.String("server_url", "https://localhost:8080", "URL (with protocol and port of osquery server)")
enrollSecret := flag.String("enroll_secret", "", "Enroll secret to authenticate enrollment")
hostCount := flag.Int("host_count", 10, "Number of hosts to start (default 10)")
randSeed := flag.Int64("seed", time.Now().UnixNano(), "Seed for random generator (default current time)")
startPeriod := flag.Duration("start_period", 10*time.Second, "Duration to spread start of hosts over")
configInterval := flag.Duration("config_interval", 1*time.Minute, "Interval for config requests")
queryInterval := flag.Duration("query_interval", 10*time.Second, "Interval for live query requests")
flag.Parse()
rand.Seed(*randSeed)
tmpl, err := template.ParseGlob("*.tmpl")
if err != nil {
log.Fatal("parse templates: ", err)
}
// Spread starts over the interval to prevent thunering herd
sleepTime := *startPeriod / time.Duration(*hostCount)
var agents []*Agent
for i := 0; i < *hostCount; i++ {
a := NewAgent(*serverURL, *enrollSecret, tmpl, *configInterval, *queryInterval)
agents = append(agents, a)
go a.runLoop()
time.Sleep(sleepTime)
}
fmt.Println("Agents running. Kill with C-c.")
<-make(chan struct{})
}