Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DLP-1800: add support for zero trust user risk scoring #1887

Merged
merged 2 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/1887.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
dlp: add support for zt risk behavior configuration
```
126 changes: 126 additions & 0 deletions zt_risk_behaviors.go
Original file line number Diff line number Diff line change
@@ -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
}
159 changes: 159 additions & 0 deletions zt_risk_behaviors_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading