Repository: cloudflare/cloudflare-go
Pull request #1887 (2024-05-01 04:02:21)

DLP-1800: add support for zero trust user risk scoring
Adds support for DLP zero trust risk behaviors (get/update)

## Description

Add support for https://developers.cloudflare.com/api/operations/dlp-zt-risk-score-get-behaviors and https://developers.cloudflare.com/api/operations/dlp-zt-risk-score-put-behaviors to cloudflare-go

## Has your change been tested?

Yes, via unit tests and via a simple main method using the newly-added methods to modify a cloudflare test account (main method not included in PR)

## Types of changes

What sort of change does your code introduce/modify?

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [x] My code follows the code style of this project.
- [x] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [x] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) 
      and relies on stable APIs.

[1]: https://help.github.com/articles/closing-issues-using-keywords/


github-actions[bot]: Oops! It looks like no changelog entry is attached to this PR. Please include a release note as described in https://github.com/cloudflare/cloudflare-go/blob/master/docs/changelog-process.md.

Example: 

~~~
```release-note:TYPE
Release note
```
~~~

If you do not require a release note to be included, please add the `workflow/skip-changelog-entry` label.

Patch:
diff --git a/.changelog/1887.txt b/.changelog/1887.txt
new file mode 100644
index 000000000..cb86d3beb
--- /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 000000000..370e3c3ed
--- /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
+}


Test patch:
diff --git a/zt_risk_behaviors_test.go b/zt_risk_behaviors_test.go
new file mode 100644
index 000000000..2ed6cfa26
--- /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:     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:     BoolPtr(false),
+			},
+		},
+	}
+
+	updateBehaviors = Behaviors{
+		Behaviors: map[string]Behavior{
+			"high_dlp": {
+				RiskLevel: Low,
+				Enabled:   BoolPtr(true),
+			},
+			"imp_travel": {
+				RiskLevel: High,
+				Enabled:   BoolPtr(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)
+	}
+}