From e7f8c10a7b9d261448144652fa9ac92615e16f77 Mon Sep 17 00:00:00 2001 From: Inanna Malick Date: Tue, 30 Apr 2024 20:37:10 -0700 Subject: [PATCH 1/2] DLP-1800: add support for zero trust user risk scoring --- .changelog/1887.txt | 3 + zt_risk_behaviors.go | 126 ++++++++++++++++++++++++++++++ zt_risk_behaviors_test.go | 159 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 288 insertions(+) create mode 100644 .changelog/1887.txt create mode 100644 zt_risk_behaviors.go create mode 100644 zt_risk_behaviors_test.go diff --git a/.changelog/1887.txt b/.changelog/1887.txt new file mode 100644 index 0000000000..cb86d3beb6 --- /dev/null +++ b/.changelog/1887.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +dlp: add support for zt risk behavior configuration +``` diff --git a/zt_risk_behaviors.go b/zt_risk_behaviors.go new file mode 100644 index 0000000000..c8b74d1910 --- /dev/null +++ b/zt_risk_behaviors.go @@ -0,0 +1,126 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/goccy/go-json" +) + +// Behavior represents a single zt risk behavior config. +type Behavior struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + RiskLevel RiskLevel `json:"risk_level"` + Enabled bool `json:"enabled"` +} + +// Wrapper used to have full-fidelity repro of json structure. +type Behaviors struct { + Behaviors map[string]Behavior `json:"behaviors"` +} + +// BehaviorResponse represents the response from the zt risk scoring endpoint +// and contains risk behaviors for an account. +type BehaviorResponse struct { + Success bool `json:"success"` + Result Behaviors `json:"result"` + Errors []string `json:"errors"` + Messages []string `json:"messages"` +} + +// Behaviors returns all zero trust risk scoring behaviors for the provided account +// +// API reference: https://developers.cloudflare.com/api/operations/dlp-zt-risk-score-get-behaviors +func (api *API) Behaviors(ctx context.Context, accountID string) (Behaviors, error) { + uri := fmt.Sprintf("/accounts/%s/zt_risk_scoring/behaviors", accountID) + + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return Behaviors{}, err + } + + var r BehaviorResponse + err = json.Unmarshal(res, &r) + if err != nil { + return Behaviors{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return r.Result, nil +} + +// UpdateBehaviors returns all zero trust risk scoring behaviors for the provided account +// NOTE: description/name updates are no-ops, risk_level [low medium high] and enabled [true/false] results in modifications +// +// API reference: https://developers.cloudflare.com/api/operations/dlp-zt-risk-score-put-behaviors +func (api *API) UpdateBehaviors(ctx context.Context, accountID string, behaviors Behaviors) (Behaviors, error) { + uri := fmt.Sprintf("/accounts/%s/zt_risk_scoring/behaviors", accountID) + + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, behaviors) + if err != nil { + return Behaviors{}, err + } + + var r BehaviorResponse + err = json.Unmarshal(res, &r) + if err != nil { + return Behaviors{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return r.Result, nil +} + +type RiskLevel int + +const ( + _ RiskLevel = iota + Low + Medium + High +) + +func (p RiskLevel) MarshalJSON() ([]byte, error) { + return json.Marshal(p.String()) +} + +func (p RiskLevel) String() string { + return [...]string{"low", "medium", "high"}[p-1] +} + +func (p *RiskLevel) UnmarshalJSON(data []byte) error { + var ( + s string + err error + ) + err = json.Unmarshal(data, &s) + if err != nil { + return err + } + v, err := RiskLevelFromString(s) + if err != nil { + return err + } + *p = *v + return nil +} + +func RiskLevelFromString(s string) (*RiskLevel, error) { + s = strings.ToLower(s) + var v RiskLevel + switch s { + case "low": + v = Low + case "medium": + v = Medium + case "high": + v = High + default: + return nil, fmt.Errorf("unknown variant for risk level: %s", s) + } + return &v, nil +} + +func (p RiskLevel) IntoRef() *RiskLevel { + return &p +} diff --git a/zt_risk_behaviors_test.go b/zt_risk_behaviors_test.go new file mode 100644 index 0000000000..b4c5abec4f --- /dev/null +++ b/zt_risk_behaviors_test.go @@ -0,0 +1,159 @@ +package cloudflare + +import ( + "context" + "fmt" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + expectedBehaviors = Behaviors{ + Behaviors: map[string]Behavior{ + "high_dlp": { + Name: "High Number of DLP Policies Triggered", + Description: "User has triggered an active DLP profile in a Gateway policy fifteen times or more within one minute.", + RiskLevel: Low, + Enabled: true, + }, + "imp_travel": { + Name: "Impossible Travel", + Description: "A user had a successful Access application log in from two locations that they could not have traveled to in that period of time.", + RiskLevel: High, + Enabled: false, + }, + }, + } + + updateBehaviors = Behaviors{ + Behaviors: map[string]Behavior{ + "high_dlp": { + RiskLevel: Low, + Enabled: true, + }, + "imp_travel": { + RiskLevel: High, + Enabled: false, + }, + }, + } +) + +func TestBehaviors(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "result": { + "behaviors": { + "high_dlp": { + "name": "High Number of DLP Policies Triggered", + "description": "User has triggered an active DLP profile in a Gateway policy fifteen times or more within one minute.", + "risk_level": "low", + "enabled":true + }, + "imp_travel": { + "name": "Impossible Travel", + "description": "A user had a successful Access application log in from two locations that they could not have traveled to in that period of time.", + "risk_level": "high", + "enabled": false + } + } + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/zt_risk_scoring/behaviors", handler) + want := expectedBehaviors + + actual, err := client.Behaviors(context.Background(), "01a7362d577a6c3019a474fd6f485823") + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestUpdateBehaviors(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) + b, err := io.ReadAll(r.Body) + defer r.Body.Close() + + if assert.NoError(t, err) { + assert.JSONEq(t, `{ + "behaviors": { + "high_dlp": { + "risk_level": "low", + "enabled":true + }, + "imp_travel": { + "risk_level": "high", + "enabled": false + } + } + }`, string(b), "JSON payload not equal") + } + + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "result": { + "behaviors": { + "high_dlp": { + "name": "High Number of DLP Policies Triggered", + "description": "User has triggered an active DLP profile in a Gateway policy fifteen times or more within one minute.", + "risk_level": "low", + "enabled":true + }, + "imp_travel": { + "name": "Impossible Travel", + "description": "A user had a successful Access application log in from two locations that they could not have traveled to in that period of time.", + "risk_level": "high", + "enabled": false + } + } + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/zt_risk_scoring/behaviors", handler) + + want := expectedBehaviors + actual, err := client.UpdateBehaviors(context.Background(), "01a7362d577a6c3019a474fd6f485823", updateBehaviors) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestRiskLevelFromString(t *testing.T) { + got, _ := RiskLevelFromString("high") + want := High + + if *got != want { + t.Errorf("got %#v, wanted %#v", *got, want) + } +} + +func TestStringFromRiskLevel(t *testing.T) { + got := fmt.Sprint(High) + want := "high" + + if got != want { + t.Errorf("got %#v, wanted %#v", got, want) + } +} From 5cbbda6995d2bb241b2b37eb6e7693ab5f857532 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Fri, 3 May 2024 10:32:28 +1000 Subject: [PATCH 2/2] fix bool lint --- zt_risk_behaviors.go | 2 +- zt_risk_behaviors_test.go | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/zt_risk_behaviors.go b/zt_risk_behaviors.go index c8b74d1910..370e3c3ed6 100644 --- a/zt_risk_behaviors.go +++ b/zt_risk_behaviors.go @@ -14,7 +14,7 @@ type Behavior struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` RiskLevel RiskLevel `json:"risk_level"` - Enabled bool `json:"enabled"` + Enabled *bool `json:"enabled"` } // Wrapper used to have full-fidelity repro of json structure. diff --git a/zt_risk_behaviors_test.go b/zt_risk_behaviors_test.go index b4c5abec4f..2ed6cfa26f 100644 --- a/zt_risk_behaviors_test.go +++ b/zt_risk_behaviors_test.go @@ -17,13 +17,13 @@ var ( Name: "High Number of DLP Policies Triggered", Description: "User has triggered an active DLP profile in a Gateway policy fifteen times or more within one minute.", RiskLevel: Low, - Enabled: true, + Enabled: BoolPtr(true), }, "imp_travel": { Name: "Impossible Travel", Description: "A user had a successful Access application log in from two locations that they could not have traveled to in that period of time.", RiskLevel: High, - Enabled: false, + Enabled: BoolPtr(false), }, }, } @@ -32,11 +32,11 @@ var ( Behaviors: map[string]Behavior{ "high_dlp": { RiskLevel: Low, - Enabled: true, + Enabled: BoolPtr(true), }, "imp_travel": { RiskLevel: High, - Enabled: false, + Enabled: BoolPtr(false), }, }, } @@ -56,7 +56,7 @@ func TestBehaviors(t *testing.T) { "name": "High Number of DLP Policies Triggered", "description": "User has triggered an active DLP profile in a Gateway policy fifteen times or more within one minute.", "risk_level": "low", - "enabled":true + "enabled":true }, "imp_travel": { "name": "Impossible Travel", @@ -96,7 +96,7 @@ func TestUpdateBehaviors(t *testing.T) { "behaviors": { "high_dlp": { "risk_level": "low", - "enabled":true + "enabled":true }, "imp_travel": { "risk_level": "high", @@ -114,7 +114,7 @@ func TestUpdateBehaviors(t *testing.T) { "name": "High Number of DLP Policies Triggered", "description": "User has triggered an active DLP profile in a Gateway policy fifteen times or more within one minute.", "risk_level": "low", - "enabled":true + "enabled":true }, "imp_travel": { "name": "Impossible Travel",