From 5bd6dae8328f9ff8e7bcd9921926e949207c4279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Falconnier?= Date: Mon, 27 Feb 2023 12:38:21 +0100 Subject: [PATCH] Add Osquery configurations --- goztl.go | 6 +- osquery_configurations.go | 198 ++++++++++++++++++++++ osquery_configurations_test.go | 293 +++++++++++++++++++++++++++++++++ 3 files changed, 496 insertions(+), 1 deletion(-) create mode 100644 osquery_configurations.go create mode 100644 osquery_configurations_test.go diff --git a/goztl.go b/goztl.go index 8414f00..e57422c 100644 --- a/goztl.go +++ b/goztl.go @@ -15,7 +15,7 @@ import ( ) const ( - libraryVersion = "0.1.8" + libraryVersion = "0.1.9" userAgent = "goztl/" + libraryVersion mediaType = "application/json" ) @@ -37,6 +37,8 @@ type Client struct { MetaBusinessUnits MetaBusinessUnitsService Tags TagsService Taxonomies TaxonomiesService + // Osquery + OsqueryConfigurations OsqueryConfigurationsService // Santa SantaConfigurations SantaConfigurationsService SantaEnrollments SantaEnrollmentsService @@ -128,6 +130,8 @@ func NewClient(httpClient *http.Client, bu string, token string, opts ...ClientO c.MetaBusinessUnits = &MetaBusinessUnitsServiceOp{client: c} c.Tags = &TagsServiceOp{client: c} c.Taxonomies = &TaxonomiesServiceOp{client: c} + // Osquery + c.OsqueryConfigurations = &OsqueryConfigurationsServiceOp{client: c} // Santa c.SantaConfigurations = &SantaConfigurationsServiceOp{client: c} c.SantaEnrollments = &SantaEnrollmentsServiceOp{client: c} diff --git a/osquery_configurations.go b/osquery_configurations.go new file mode 100644 index 0000000..426d309 --- /dev/null +++ b/osquery_configurations.go @@ -0,0 +1,198 @@ +package goztl + +import ( + "context" + "fmt" + "net/http" +) + +const ocBasePath = "osquery/configurations/" + +// OsqueryConfigurationsService is an interface for interfacing with the Osquery configurations +// endpoints of the Zentral API +type OsqueryConfigurationsService interface { + List(context.Context, *ListOptions) ([]OsqueryConfiguration, *Response, error) + GetByID(context.Context, int) (*OsqueryConfiguration, *Response, error) + GetByName(context.Context, string) (*OsqueryConfiguration, *Response, error) + Create(context.Context, *OsqueryConfigurationRequest) (*OsqueryConfiguration, *Response, error) + Update(context.Context, int, *OsqueryConfigurationRequest) (*OsqueryConfiguration, *Response, error) + Delete(context.Context, int) (*Response, error) +} + +// OsqueryConfigurationsServiceOp handles communication with the Osquery configurations related +// methods of the Zentral API. +type OsqueryConfigurationsServiceOp struct { + client *Client +} + +var _ OsqueryConfigurationsService = &OsqueryConfigurationsServiceOp{} + +// OsqueryConfiguration represents a Zentral Osquery configuration +type OsqueryConfiguration struct { + ID int `json:"id,omitempty"` + Name string `json:"name"` + Description string `json:"description"` + Inventory bool `json:"inventory"` + InventoryApps bool `json:"inventory_apps"` + InventoryEC2 bool `json:"inventory_ec2"` + InventoryInterval int `json:"inventory_interval"` + Options map[string]interface{} `json:"options"` + Created Timestamp `json:"created_at,omitempty"` + Updated Timestamp `json:"updated_at,omitempty"` +} + +func (oc OsqueryConfiguration) String() string { + return Stringify(oc) +} + +// OsqueryConfigurationRequest represents a request to create or update a Osquery configuration +type OsqueryConfigurationRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Inventory bool `json:"inventory"` + InventoryApps bool `json:"inventory_apps"` + InventoryEC2 bool `json:"inventory_ec2"` + InventoryInterval int `json:"inventory_interval"` + Options map[string]interface{} `json:"options"` +} + +type listOCOptions struct { + Name string `url:"name,omitempty"` +} + +// List lists all the Osquery configurations. +func (s *OsqueryConfigurationsServiceOp) List(ctx context.Context, opt *ListOptions) ([]OsqueryConfiguration, *Response, error) { + return s.list(ctx, opt, nil) +} + +// GetByID retrieves a Osquery configuration by id. +func (s *OsqueryConfigurationsServiceOp) GetByID(ctx context.Context, ocID int) (*OsqueryConfiguration, *Response, error) { + if ocID < 1 { + return nil, nil, NewArgError("ocID", "cannot be less than 1") + } + + path := fmt.Sprintf("%s%d/", ocBasePath, ocID) + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + oc := new(OsqueryConfiguration) + + resp, err := s.client.Do(ctx, req, oc) + if err != nil { + return nil, resp, err + } + + return oc, resp, err +} + +// GetByName retrieves a Osquery configuration by name. +func (s *OsqueryConfigurationsServiceOp) GetByName(ctx context.Context, name string) (*OsqueryConfiguration, *Response, error) { + if len(name) < 1 { + return nil, nil, NewArgError("name", "cannot be blank") + } + + listSCOpt := &listOCOptions{Name: name} + + ocs, resp, err := s.list(ctx, nil, listSCOpt) + if err != nil { + return nil, resp, err + } + if len(ocs) < 1 { + return nil, resp, nil + } + + return &ocs[0], resp, err +} + +// Create a new Osquery configuration. +func (s *OsqueryConfigurationsServiceOp) Create(ctx context.Context, createRequest *OsqueryConfigurationRequest) (*OsqueryConfiguration, *Response, error) { + if createRequest == nil { + return nil, nil, NewArgError("createRequest", "cannot be nil") + } + + req, err := s.client.NewRequest(ctx, http.MethodPost, ocBasePath, createRequest) + if err != nil { + return nil, nil, err + } + + oc := new(OsqueryConfiguration) + resp, err := s.client.Do(ctx, req, oc) + if err != nil { + return nil, resp, err + } + + return oc, resp, err +} + +// Update a Osquery configuration. +func (s *OsqueryConfigurationsServiceOp) Update(ctx context.Context, ocID int, updateRequest *OsqueryConfigurationRequest) (*OsqueryConfiguration, *Response, error) { + if ocID < 1 { + return nil, nil, NewArgError("ocID", "cannot be less than 1") + } + + if updateRequest == nil { + return nil, nil, NewArgError("updateRequest", "cannot be nil") + } + + path := fmt.Sprintf("%s%d/", ocBasePath, ocID) + + req, err := s.client.NewRequest(ctx, http.MethodPut, path, updateRequest) + if err != nil { + return nil, nil, err + } + + oc := new(OsqueryConfiguration) + resp, err := s.client.Do(ctx, req, oc) + if err != nil { + return nil, resp, err + } + + return oc, resp, err +} + +// Delete a Osquery configuration. +func (s *OsqueryConfigurationsServiceOp) Delete(ctx context.Context, ocID int) (*Response, error) { + if ocID < 1 { + return nil, NewArgError("ocID", "cannot be less than 1") + } + + path := fmt.Sprintf("%s%d/", ocBasePath, ocID) + + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + + return resp, err +} + +// Helper method for listing Osquery configurations +func (s *OsqueryConfigurationsServiceOp) list(ctx context.Context, opt *ListOptions, ocOpt *listOCOptions) ([]OsqueryConfiguration, *Response, error) { + path := ocBasePath + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + path, err = addOptions(path, ocOpt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + var ocs []OsqueryConfiguration + resp, err := s.client.Do(ctx, req, &ocs) + if err != nil { + return nil, resp, err + } + + return ocs, resp, err +} diff --git a/osquery_configurations_test.go b/osquery_configurations_test.go new file mode 100644 index 0000000..a382a15 --- /dev/null +++ b/osquery_configurations_test.go @@ -0,0 +1,293 @@ +package goztl + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" +) + +var ocListJSONResponse = ` +[ + { + "id": 4, + "name": "Default", + "description": "Description", + "inventory": true, + "inventory_apps": true, + "inventory_ec2": false, + "inventory_interval": 600, + "options": {"config_refresh": 120}, + "created_at": "2022-07-22T01:02:03.444444", + "updated_at": "2022-07-22T01:02:03.444444" + } +] +` + +var ocGetJSONResponse = ` +{ + "id": 4, + "name": "Default", + "description": "Description", + "inventory": true, + "inventory_apps": true, + "inventory_ec2": false, + "inventory_interval": 600, + "options": {"config_refresh": 120}, + "created_at": "2022-07-22T01:02:03.444444", + "updated_at": "2022-07-22T01:02:03.444444" +} +` + +var ocCreateJSONResponse = ` +{ + "id": 4, + "name": "Default", + "description": "Description", + "inventory": true, + "inventory_apps": true, + "inventory_ec2": false, + "inventory_interval": 600, + "options": {"config_refresh": 120}, + "created_at": "2022-07-22T01:02:03.444444", + "updated_at": "2022-07-22T01:02:03.444444" +} +` + +var ocUpdateJSONResponse = ` +{ + "id": 4, + "name": "Default", + "description": "Description", + "inventory": true, + "inventory_apps": true, + "inventory_ec2": false, + "inventory_interval": 600, + "options": {"config_refresh": 120}, + "created_at": "2022-07-22T01:02:03.444444", + "updated_at": "2022-07-22T01:02:03.444444" +} +` + +func TestOsqueryConfigurationsService_List(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/osquery/configurations/", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", "application/json") + fmt.Fprint(w, ocListJSONResponse) + }) + + ctx := context.Background() + got, _, err := client.OsqueryConfigurations.List(ctx, nil) + if err != nil { + t.Errorf("OsqueryConfigurations.List returned error: %v", err) + } + + want := []OsqueryConfiguration{ + { + ID: 4, + Name: "Default", + Description: "Description", + Inventory: true, + InventoryApps: true, + InventoryEC2: false, + InventoryInterval: 600, + Options: map[string]interface{}{"config_refresh": 120.0}, + Created: Timestamp{referenceTime}, + Updated: Timestamp{referenceTime}, + }, + } + if !cmp.Equal(got, want) { + t.Errorf("OsqueryConfigurations.List returned %+v, want %+v", got, want) + } +} + +func TestOsqueryConfigurationsService_GetByID(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/osquery/configurations/1/", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", "application/json") + fmt.Fprint(w, ocGetJSONResponse) + }) + + ctx := context.Background() + got, _, err := client.OsqueryConfigurations.GetByID(ctx, 1) + if err != nil { + t.Errorf("OsqueryConfigurations.GetByID returned error: %v", err) + } + + want := &OsqueryConfiguration{ + ID: 4, + Name: "Default", + Description: "Description", + Inventory: true, + InventoryApps: true, + InventoryEC2: false, + InventoryInterval: 600, + Options: map[string]interface{}{"config_refresh": 120.0}, + Created: Timestamp{referenceTime}, + Updated: Timestamp{referenceTime}, + } + if !cmp.Equal(got, want) { + t.Errorf("OsqueryConfigurations.GetByID returned %+v, want %+v", got, want) + } +} + +func TestOsqueryConfigurationsService_GetByName(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/osquery/configurations/", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", "application/json") + testQueryArg(t, r, "name", "Default") + fmt.Fprint(w, ocListJSONResponse) + }) + + ctx := context.Background() + got, _, err := client.OsqueryConfigurations.GetByName(ctx, "Default") + if err != nil { + t.Errorf("OsqueryConfigurations.GetByName returned error: %v", err) + } + + want := &OsqueryConfiguration{ + ID: 4, + Name: "Default", + Description: "Description", + Inventory: true, + InventoryApps: true, + InventoryEC2: false, + InventoryInterval: 600, + Options: map[string]interface{}{"config_refresh": 120.0}, + Created: Timestamp{referenceTime}, + Updated: Timestamp{referenceTime}, + } + if !cmp.Equal(got, want) { + t.Errorf("OsqueryConfigurations.GetByName returned %+v, want %+v", got, want) + } +} + +func TestOsqueryConfigurationsService_Create(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + createRequest := &OsqueryConfigurationRequest{ + Name: "Default", + Description: "Description", + Inventory: true, + InventoryApps: true, + InventoryEC2: false, + InventoryInterval: 600, + Options: map[string]interface{}{"config_refresh": 120.0}, + } + + mux.HandleFunc("/osquery/configurations/", func(w http.ResponseWriter, r *http.Request) { + v := new(OsqueryConfigurationRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatal(err) + } + testMethod(t, r, "POST") + testHeader(t, r, "Accept", "application/json") + testHeader(t, r, "Content-Type", "application/json") + assert.Equal(t, createRequest, v) + + fmt.Fprint(w, ocCreateJSONResponse) + }) + + ctx := context.Background() + got, _, err := client.OsqueryConfigurations.Create(ctx, createRequest) + if err != nil { + t.Errorf("OsqueryConfigurations.Create returned error: %v", err) + } + + want := &OsqueryConfiguration{ + ID: 4, + Name: "Default", + Description: "Description", + Inventory: true, + InventoryApps: true, + InventoryEC2: false, + InventoryInterval: 600, + Options: map[string]interface{}{"config_refresh": 120.0}, + Created: Timestamp{referenceTime}, + Updated: Timestamp{referenceTime}, + } + if !cmp.Equal(got, want) { + t.Errorf("OsqueryConfigurations.Create returned %+v, want %+v", got, want) + } +} + +func TestOsqueryConfigurationsService_Update(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + updateRequest := &OsqueryConfigurationRequest{ + Name: "Default", + Description: "Description", + Inventory: true, + InventoryApps: true, + InventoryEC2: false, + InventoryInterval: 600, + Options: map[string]interface{}{"config_refresh": 120.0}, + } + + mux.HandleFunc("/osquery/configurations/1/", func(w http.ResponseWriter, r *http.Request) { + v := new(OsqueryConfigurationRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatal(err) + } + testMethod(t, r, "PUT") + testHeader(t, r, "Accept", "application/json") + testHeader(t, r, "Content-Type", "application/json") + assert.Equal(t, updateRequest, v) + fmt.Fprint(w, ocUpdateJSONResponse) + }) + + ctx := context.Background() + got, _, err := client.OsqueryConfigurations.Update(ctx, 1, updateRequest) + if err != nil { + t.Errorf("OsqueryConfigurations.Update returned error: %v", err) + } + + want := &OsqueryConfiguration{ + ID: 4, + Name: "Default", + Description: "Description", + Inventory: true, + InventoryApps: true, + InventoryEC2: false, + InventoryInterval: 600, + Options: map[string]interface{}{"config_refresh": 120.0}, + Created: Timestamp{referenceTime}, + Updated: Timestamp{referenceTime}, + } + if !cmp.Equal(got, want) { + t.Errorf("OsqueryConfigurations.Update returned %+v, want %+v", got, want) + } +} + +func TestOsqueryConfigurationsService_Delete(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/osquery/configurations/1/", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := context.Background() + _, err := client.OsqueryConfigurations.Delete(ctx, 1) + if err != nil { + t.Errorf("OsqueryConfigurations.Delete returned error: %v", err) + } +}