diff --git a/goztl.go b/goztl.go index f8b8942..8414f00 100644 --- a/goztl.go +++ b/goztl.go @@ -15,7 +15,7 @@ import ( ) const ( - libraryVersion = "0.1.7" + libraryVersion = "0.1.8" userAgent = "goztl/" + libraryVersion mediaType = "application/json" ) @@ -40,6 +40,7 @@ type Client struct { // Santa SantaConfigurations SantaConfigurationsService SantaEnrollments SantaEnrollmentsService + SantaRules SantaRulesService // Zentral API token token string @@ -130,6 +131,7 @@ func NewClient(httpClient *http.Client, bu string, token string, opts ...ClientO // Santa c.SantaConfigurations = &SantaConfigurationsServiceOp{client: c} c.SantaEnrollments = &SantaEnrollmentsServiceOp{client: c} + c.SantaRules = &SantaRulesServiceOp{client: c} c.headers = make(map[string]string) diff --git a/santa_rules.go b/santa_rules.go new file mode 100644 index 0000000..735ea17 --- /dev/null +++ b/santa_rules.go @@ -0,0 +1,228 @@ +package goztl + +import ( + "context" + "fmt" + "net/http" +) + +const srBasePath = "santa/rules/" + +// SantaRulesService is an interface for interfacing with the Santa rules +// endpoints of the Zentral API +type SantaRulesService interface { + List(context.Context, *ListOptions) ([]SantaRule, *Response, error) + GetByID(context.Context, int) (*SantaRule, *Response, error) + GetByConfigurationID(context.Context, int) ([]SantaRule, *Response, error) + GetByTargetIdentifier(context.Context, string) ([]SantaRule, *Response, error) + GetByTargetType(context.Context, string) ([]SantaRule, *Response, error) + Create(context.Context, *SantaRuleRequest) (*SantaRule, *Response, error) + Update(context.Context, int, *SantaRuleRequest) (*SantaRule, *Response, error) + Delete(context.Context, int) (*Response, error) +} + +// SantaRulesServiceOp handles communication with the Santa enrollments related +// methods of the Zentral API. +type SantaRulesServiceOp struct { + client *Client +} + +var _ SantaRulesService = &SantaRulesServiceOp{} + +// SantaRule represents a Zentral SantaRule +type SantaRule struct { + ID int `json:"id"` + ConfigurationID int `json:"configuration"` + Policy int `json:"policy"` + TargetType string `json:"target_type"` + TargetIdentifier string `json:"target_identifier"` + Description string `json:"description"` + CustomMessage string `json:"custom_msg"` + RulesetID *int `json:"ruleset"` + PrimaryUsers []string `json:"primary_users"` + ExcludedPrimaryUsers []string `json:"excluded_primary_users"` + SerialNumbers []string `json:"serial_numbers"` + ExcludedSerialNumbers []string `json:"excluded_serial_numbers"` + TagIDs []int `json:"tags"` + ExcludedTagIDs []int `json:"excluded_tags"` + Version int `json:"version"` + Created Timestamp `json:"created_at"` + Updated Timestamp `json:"updated_at"` +} + +func (sr SantaRule) String() string { + return Stringify(sr) +} + +// SantaRuleRequest represents a request to create or update a Santa rule +type SantaRuleRequest struct { + ConfigurationID int `json:"configuration"` + Policy int `json:"policy"` + TargetType string `json:"target_type"` + TargetIdentifier string `json:"target_identifier"` + Description string `json:"description"` + CustomMessage string `json:"custom_msg"` + PrimaryUsers []string `json:"primary_users"` + ExcludedPrimaryUsers []string `json:"excluded_primary_users"` + SerialNumbers []string `json:"serial_numbers"` + ExcludedSerialNumbers []string `json:"excluded_serial_numbers"` + TagIDs []int `json:"tags"` + ExcludedTagIDs []int `json:"excluded_tags"` +} + +type listSROptions struct { + ConfigurationID int `url:"configuration_id,omitempty"` + TargetType string `url:"target_type,omitempty"` + TargetIdentifier string `url:"target_identifier,omitempty"` +} + +// List lists all the Santa rules. +func (s *SantaRulesServiceOp) List(ctx context.Context, opt *ListOptions) ([]SantaRule, *Response, error) { + return s.list(ctx, opt, nil) +} + +// GetByID retrieves a Santa rule by id. +func (s *SantaRulesServiceOp) GetByID(ctx context.Context, srID int) (*SantaRule, *Response, error) { + if srID < 1 { + return nil, nil, NewArgError("srID", "cannot be less than 1") + } + + path := fmt.Sprintf("%s%d/", srBasePath, srID) + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + sr := new(SantaRule) + + resp, err := s.client.Do(ctx, req, sr) + if err != nil { + return nil, resp, err + } + + return sr, resp, err +} + +// GetByConfigurationID retrieves the Santa rules for a given configuration. +func (s *SantaRulesServiceOp) GetByConfigurationID(ctx context.Context, configuration_id int) ([]SantaRule, *Response, error) { + if configuration_id < 1 { + return nil, nil, NewArgError("configuration_id", "cannot be negative") + } + + listSROpt := &listSROptions{ConfigurationID: configuration_id} + + return s.list(ctx, nil, listSROpt) +} + +// GetByTargetIdentifier retrieves the Santa rules for a given target identifier +func (s *SantaRulesServiceOp) GetByTargetIdentifier(ctx context.Context, target_identifier string) ([]SantaRule, *Response, error) { + if len(target_identifier) == 0 { + return nil, nil, NewArgError("target_identifier", "cannot be empty") + } + + listSROpt := &listSROptions{TargetIdentifier: target_identifier} + + return s.list(ctx, nil, listSROpt) +} + +// GetByTargetType retrieves the Santa rules for a given target type +func (s *SantaRulesServiceOp) GetByTargetType(ctx context.Context, target_type string) ([]SantaRule, *Response, error) { + if len(target_type) == 0 { + return nil, nil, NewArgError("target_type", "cannot be empty") + } + + listSROpt := &listSROptions{TargetType: target_type} + + return s.list(ctx, nil, listSROpt) +} + +// Create a new Santa rule +func (s *SantaRulesServiceOp) Create(ctx context.Context, createRequest *SantaRuleRequest) (*SantaRule, *Response, error) { + if createRequest == nil { + return nil, nil, NewArgError("createRequest", "cannot be nil") + } + + req, err := s.client.NewRequest(ctx, http.MethodPost, srBasePath, createRequest) + if err != nil { + return nil, nil, err + } + + sr := new(SantaRule) + resp, err := s.client.Do(ctx, req, sr) + if err != nil { + return nil, resp, err + } + + return sr, resp, err +} + +// Update a Santa rule +func (s *SantaRulesServiceOp) Update(ctx context.Context, srID int, updateRequest *SantaRuleRequest) (*SantaRule, *Response, error) { + if srID < 1 { + return nil, nil, NewArgError("srID", "cannot be less than 1") + } + + if updateRequest == nil { + return nil, nil, NewArgError("updateRequest", "cannot be nil") + } + + path := fmt.Sprintf("%s%d/", srBasePath, srID) + + req, err := s.client.NewRequest(ctx, http.MethodPut, path, updateRequest) + if err != nil { + return nil, nil, err + } + + sr := new(SantaRule) + resp, err := s.client.Do(ctx, req, sr) + if err != nil { + return nil, resp, err + } + + return sr, resp, err +} + +// Delete a Santa rule +func (s *SantaRulesServiceOp) Delete(ctx context.Context, srID int) (*Response, error) { + if srID < 1 { + return nil, NewArgError("srID", "cannot be less than 1") + } + + path := fmt.Sprintf("%s%d/", srBasePath, srID) + + 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 Santa enrollments +func (s *SantaRulesServiceOp) list(ctx context.Context, opt *ListOptions, srOpt *listSROptions) ([]SantaRule, *Response, error) { + path := srBasePath + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + path, err = addOptions(path, srOpt) + 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 srs []SantaRule + resp, err := s.client.Do(ctx, req, &srs) + if err != nil { + return nil, resp, err + } + + return srs, resp, err +} diff --git a/santa_rules_test.go b/santa_rules_test.go new file mode 100644 index 0000000..f739102 --- /dev/null +++ b/santa_rules_test.go @@ -0,0 +1,446 @@ +package goztl + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" +) + +var srListJSONResponse = ` +[ + { + "id": 1, + "configuration": 2, + "policy": 1, + "target_type": "BINARY", + "target_identifier": "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02", + "description": "description", + "custom_msg": "custom message", + "ruleset": null, + "primary_users": ["un", "deux"], + "excluded_primary_users": ["trois", "quatre"], + "serial_numbers": ["cinq", "six"], + "excluded_serial_numbers": ["sept", "huit"], + "tags": [9, 10], + "excluded_tags": [11, 12], + "version": 1, + "created_at": "2022-07-22T01:02:03.444444", + "updated_at": "2022-07-22T01:02:03.444444" + } +] +` + +var srGetJSONResponse = ` +{ + "id": 1, + "configuration": 2, + "policy": 1, + "target_type": "BINARY", + "target_identifier": "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02", + "description": "description", + "custom_msg": "custom message", + "ruleset": 1, + "primary_users": ["un", "deux"], + "excluded_primary_users": ["trois", "quatre"], + "serial_numbers": ["cinq", "six"], + "excluded_serial_numbers": ["sept", "huit"], + "tags": [9, 10], + "excluded_tags": [11, 12], + "version": 1, + "created_at": "2022-07-22T01:02:03.444444", + "updated_at": "2022-07-22T01:02:03.444444" +} +` + +var srCreateJSONResponse = ` +{ + "id": 1, + "configuration": 2, + "policy": 1, + "target_type": "BINARY", + "target_identifier": "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02", + "description": "description", + "custom_msg": "custom message", + "ruleset": null, + "primary_users": ["un", "deux"], + "excluded_primary_users": ["trois", "quatre"], + "serial_numbers": ["cinq", "six"], + "excluded_serial_numbers": ["sept", "huit"], + "tags": [9, 10], + "excluded_tags": [11, 12], + "version": 1, + "created_at": "2022-07-22T01:02:03.444444", + "updated_at": "2022-07-22T01:02:03.444444" +} +` + +var srUpdateJSONResponse = ` +{ + "id": 1, + "configuration": 2, + "policy": 1, + "target_type": "BINARY", + "target_identifier": "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02", + "description": "", + "custom_msg": "", + "ruleset": null, + "primary_users": [], + "excluded_primary_users": [], + "serial_numbers": [], + "excluded_serial_numbers": [], + "tags": [], + "excluded_tags": [], + "version": 2, + "created_at": "2022-07-22T01:02:03.444444", + "updated_at": "2022-07-22T01:02:03.444444" +} +` + +func TestSantaRulesService_List(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/santa/rules/", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", "application/json") + fmt.Fprint(w, srListJSONResponse) + }) + + ctx := context.Background() + got, _, err := client.SantaRules.List(ctx, nil) + if err != nil { + t.Errorf("SantaRules.List returned error: %v", err) + } + + want := []SantaRule{ + { + ID: 1, + ConfigurationID: 2, + Policy: 1, + TargetType: "BINARY", + TargetIdentifier: "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02", + Description: "description", + CustomMessage: "custom message", + RulesetID: nil, + PrimaryUsers: []string{"un", "deux"}, + ExcludedPrimaryUsers: []string{"trois", "quatre"}, + SerialNumbers: []string{"cinq", "six"}, + ExcludedSerialNumbers: []string{"sept", "huit"}, + TagIDs: []int{9, 10}, + ExcludedTagIDs: []int{11, 12}, + Version: 1, + Created: Timestamp{referenceTime}, + Updated: Timestamp{referenceTime}, + }, + } + if !cmp.Equal(got, want) { + t.Errorf("SantaRules.List returned %+v, want %+v", got, want) + } +} + +func TestSantaRulesService_GetByID(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/santa/rules/1/", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", "application/json") + fmt.Fprint(w, srGetJSONResponse) + }) + + ctx := context.Background() + got, _, err := client.SantaRules.GetByID(ctx, 1) + if err != nil { + t.Errorf("SantaRules.GetByID returned error: %v", err) + } + + want := &SantaRule{ + ID: 1, + ConfigurationID: 2, + Policy: 1, + TargetType: "BINARY", + TargetIdentifier: "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02", + Description: "description", + CustomMessage: "custom message", + RulesetID: Int(1), + PrimaryUsers: []string{"un", "deux"}, + ExcludedPrimaryUsers: []string{"trois", "quatre"}, + SerialNumbers: []string{"cinq", "six"}, + ExcludedSerialNumbers: []string{"sept", "huit"}, + TagIDs: []int{9, 10}, + ExcludedTagIDs: []int{11, 12}, + Version: 1, + Created: Timestamp{referenceTime}, + Updated: Timestamp{referenceTime}, + } + if !cmp.Equal(got, want) { + t.Errorf("SantaRules.GetByID returned %+v, want %+v", got, want) + } +} + +func TestSantaRulesService_GetByConfigurationID(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/santa/rules/", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", "application/json") + testQueryArg(t, r, "configuration_id", "2") + fmt.Fprint(w, srListJSONResponse) + }) + + ctx := context.Background() + got, _, err := client.SantaRules.GetByConfigurationID(ctx, 2) + if err != nil { + t.Errorf("SantaRules.GetByConfigurationID returned error: %v", err) + } + + want := []SantaRule{ + { + ID: 1, + ConfigurationID: 2, + Policy: 1, + TargetType: "BINARY", + TargetIdentifier: "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02", + Description: "description", + CustomMessage: "custom message", + RulesetID: nil, + PrimaryUsers: []string{"un", "deux"}, + ExcludedPrimaryUsers: []string{"trois", "quatre"}, + SerialNumbers: []string{"cinq", "six"}, + ExcludedSerialNumbers: []string{"sept", "huit"}, + TagIDs: []int{9, 10}, + ExcludedTagIDs: []int{11, 12}, + Version: 1, + Created: Timestamp{referenceTime}, + Updated: Timestamp{referenceTime}, + }, + } + if !cmp.Equal(got, want) { + t.Errorf("SantaRules.GetByConfigurationID returned %+v, want %+v", got, want) + } +} + +func TestSantaRulesService_GetByTargetIdentifier(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/santa/rules/", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", "application/json") + testQueryArg(t, r, "target_identifier", "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02") + fmt.Fprint(w, srListJSONResponse) + }) + + ctx := context.Background() + got, _, err := client.SantaRules.GetByTargetIdentifier(ctx, "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02") + if err != nil { + t.Errorf("SantaRules.GetByTargetIdentifier returned error: %v", err) + } + + want := []SantaRule{ + { + ID: 1, + ConfigurationID: 2, + Policy: 1, + TargetType: "BINARY", + TargetIdentifier: "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02", + Description: "description", + CustomMessage: "custom message", + RulesetID: nil, + PrimaryUsers: []string{"un", "deux"}, + ExcludedPrimaryUsers: []string{"trois", "quatre"}, + SerialNumbers: []string{"cinq", "six"}, + ExcludedSerialNumbers: []string{"sept", "huit"}, + TagIDs: []int{9, 10}, + ExcludedTagIDs: []int{11, 12}, + Version: 1, + Created: Timestamp{referenceTime}, + Updated: Timestamp{referenceTime}, + }, + } + if !cmp.Equal(got, want) { + t.Errorf("SantaRules.GetByTargetIdentifier returned %+v, want %+v", got, want) + } +} + +func TestSantaRulesService_GetByTargetType(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/santa/rules/", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", "application/json") + testQueryArg(t, r, "target_type", "BINARY") + fmt.Fprint(w, srListJSONResponse) + }) + + ctx := context.Background() + got, _, err := client.SantaRules.GetByTargetType(ctx, "BINARY") + if err != nil { + t.Errorf("SantaRules.GetByTargetType returned error: %v", err) + } + + want := []SantaRule{ + { + ID: 1, + ConfigurationID: 2, + Policy: 1, + TargetType: "BINARY", + TargetIdentifier: "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02", + Description: "description", + CustomMessage: "custom message", + RulesetID: nil, + PrimaryUsers: []string{"un", "deux"}, + ExcludedPrimaryUsers: []string{"trois", "quatre"}, + SerialNumbers: []string{"cinq", "six"}, + ExcludedSerialNumbers: []string{"sept", "huit"}, + TagIDs: []int{9, 10}, + ExcludedTagIDs: []int{11, 12}, + Version: 1, + Created: Timestamp{referenceTime}, + Updated: Timestamp{referenceTime}, + }, + } + if !cmp.Equal(got, want) { + t.Errorf("SantaRules.GetByTargetType returned %+v, want %+v", got, want) + } +} + +func TestSantaRulesService_Create(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + createRequest := &SantaRuleRequest{ + ConfigurationID: 2, + Policy: 1, + TargetType: "BINARY", + TargetIdentifier: "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02", + Description: "description", + CustomMessage: "custom message", + PrimaryUsers: []string{"un", "deux"}, + ExcludedPrimaryUsers: []string{"trois", "quatre"}, + SerialNumbers: []string{"cinq", "six"}, + ExcludedSerialNumbers: []string{"sept", "huit"}, + TagIDs: []int{9, 10}, + ExcludedTagIDs: []int{11, 12}, + } + + mux.HandleFunc("/santa/rules/", func(w http.ResponseWriter, r *http.Request) { + v := new(SantaRuleRequest) + 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, srCreateJSONResponse) + }) + + ctx := context.Background() + got, _, err := client.SantaRules.Create(ctx, createRequest) + if err != nil { + t.Errorf("SantaRules.Create returned error: %v", err) + } + + want := &SantaRule{ + ID: 1, + ConfigurationID: 2, + Policy: 1, + TargetType: "BINARY", + TargetIdentifier: "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02", + Description: "description", + CustomMessage: "custom message", + RulesetID: nil, + PrimaryUsers: []string{"un", "deux"}, + ExcludedPrimaryUsers: []string{"trois", "quatre"}, + SerialNumbers: []string{"cinq", "six"}, + ExcludedSerialNumbers: []string{"sept", "huit"}, + TagIDs: []int{9, 10}, + ExcludedTagIDs: []int{11, 12}, + Version: 1, + Created: Timestamp{referenceTime}, + Updated: Timestamp{referenceTime}, + } + if !cmp.Equal(got, want) { + t.Errorf("SantaRules.Create returned %+v, want %+v", got, want) + } +} + +func TestSantaRulesService_Update(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + updateRequest := &SantaRuleRequest{ + ConfigurationID: 2, + Policy: 1, + TargetType: "BINARY", + TargetIdentifier: "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02", + } + + mux.HandleFunc("/santa/rules/1/", func(w http.ResponseWriter, r *http.Request) { + v := new(SantaRuleRequest) + 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, srUpdateJSONResponse) + }) + + ctx := context.Background() + got, _, err := client.SantaRules.Update(ctx, 1, updateRequest) + if err != nil { + t.Errorf("SantaRules.Update returned error: %v", err) + } + + want := &SantaRule{ + ID: 1, + ConfigurationID: 2, + Policy: 1, + TargetType: "BINARY", + TargetIdentifier: "311fe3feed16b9cd8df0f8b1517be5cb86048707df4889ba8dc37d4d68866d02", + Description: "", + CustomMessage: "", + RulesetID: nil, + PrimaryUsers: []string{}, + ExcludedPrimaryUsers: []string{}, + SerialNumbers: []string{}, + ExcludedSerialNumbers: []string{}, + TagIDs: []int{}, + ExcludedTagIDs: []int{}, + Version: 2, + Created: Timestamp{referenceTime}, + Updated: Timestamp{referenceTime}, + } + if !cmp.Equal(got, want) { + t.Errorf("SantaRules.Update returned %+v, want %+v", got, want) + } +} + +func TestSantaRulesService_Delete(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/santa/rules/1/", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := context.Background() + _, err := client.SantaRules.Delete(ctx, 1) + if err != nil { + t.Errorf("SantaRules.Delete returned error: %v", err) + } +}