mirror of
https://github.com/fleetdm/fleet
synced 2026-05-18 14:38:53 +00:00
285 lines
7.3 KiB
Go
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{})
|
|
}
|