From 2f59d9d1243a21937aba4293100964efc5ad3489 Mon Sep 17 00:00:00 2001 From: Michele Russo Date: Wed, 20 Nov 2024 17:31:10 +0100 Subject: [PATCH 1/6] Add new error constant --- errors.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/errors.go b/errors.go index 93faab0dbd6..c10f7eea317 100644 --- a/errors.go +++ b/errors.go @@ -32,6 +32,7 @@ const ( errInvalidResourceContainerAccess = "requested resource container (%q) is not supported for this endpoint" errRequiredAccountLevelResourceContainer = "this endpoint requires using an account level resource container and identifiers" errRequiredZoneLevelResourceContainer = "this endpoint requires using a zone level resource container and identifiers" + errMissingDetectionID = "required missing detection ID" ) var ( @@ -45,6 +46,7 @@ var ( ErrRequiredAccountLevelResourceContainer = errors.New(errRequiredAccountLevelResourceContainer) ErrRequiredZoneLevelResourceContainer = errors.New(errRequiredZoneLevelResourceContainer) + ErrMissingDetectionID = errors.New(errMissingDetectionID) ) type ErrorType string From 491f1e00e8d1f9606ab28328f7e1b0079c6ee98f Mon Sep 17 00:00:00 2001 From: Michele Russo Date: Wed, 20 Nov 2024 17:32:36 +0100 Subject: [PATCH 2/6] Add API wrapper for leak credential detection --- leaked_credential_check.go | 190 +++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 leaked_credential_check.go diff --git a/leaked_credential_check.go b/leaked_credential_check.go new file mode 100644 index 00000000000..22a355d3afe --- /dev/null +++ b/leaked_credential_check.go @@ -0,0 +1,190 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + + "github.com/goccy/go-json" +) + +type LeakedCredentialCheckGetStatusParams struct{} + +type LeakedCredentialCheckStatus struct { + Enabled bool `json:"enabled"` +} + +type LeakCredentialCheckStatusResponse struct { + Response + Result LeakedCredentialCheckStatus `json:"result"` +} + +type LeakCredentialCheckSetStatusParams struct { + Enabled bool `json:"enabled"` +} + +type LeakedCredentialCheckListDetectionsParams struct{} + +type LeakedCredentialCheckDetectionEntry struct { + ID string `json:"id"` + Username string `json:"username"` + Password string `json:"password"` +} + +type LeakedCredentialCheckListDetectionsResponse struct { + Response + Result []LeakedCredentialCheckDetectionEntry `json:"result"` +} + +type LeakedCredentialCheckCreateDetectionParams struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type LeakedCredentialCheckCreateDetectionResponse struct { + Response + Result LeakedCredentialCheckDetectionEntry `json:"result"` +} + +type LeakedCredentialCheckDeleteDetectionParams struct { + DetectionID string +} + +type LeakedCredentialCheckDeleteDetectionResponse struct { + Response + Result []struct{} `json:"result"` +} + +type LeakedCredentialCheckUpdateDetectionParams struct { + LeakedCredentialCheckDetectionEntry +} +type LeakedCredentialCheckUpdateDetectionResponse struct { + Response + Result LeakedCredentialCheckDetectionEntry +} + +// LeakCredentialCheckGetStatus returns whether Leaked credential check is enabled or not. It is false by default. +// +// API reference: https://developers.cloudflare.com/api/operations/waf-product-api-leaked-credentials-get-status +func (api *API) LeakedCredentialCheckGetStatus(ctx context.Context, rc *ResourceContainer, params LeakedCredentialCheckGetStatusParams) (LeakedCredentialCheckStatus, error) { + if rc.Identifier == "" { + return LeakedCredentialCheckStatus{}, ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/leaked-credential-checks", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return LeakedCredentialCheckStatus{}, err + } + result := LeakCredentialCheckStatusResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return LeakedCredentialCheckStatus{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result.Result, nil +} + +// LeakedCredentialCheckSetStatus enable or disable the Leak Credential Check. Returns the status. +// +// API reference: https://developers.cloudflare.com/api/operations/waf-product-api-leaked-credentials-set-status +func (api *API) LeakedCredentialCheckSetStatus(ctx context.Context, rc *ResourceContainer, params LeakCredentialCheckSetStatusParams) (LeakedCredentialCheckStatus, error) { + if rc.Identifier == "" { + return LeakedCredentialCheckStatus{}, ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/leaked-credential-checks", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) + if err != nil { + return LeakedCredentialCheckStatus{}, err + } + result := LeakCredentialCheckStatusResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return LeakedCredentialCheckStatus{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result.Result, nil +} + +// LeakedCredentialCheckListDetections lists user-defined detection patterns for Leaked Credential Checks. +// +// API reference: https://developers.cloudflare.com/api/operations/waf-product-api-leaked-credentials-list-detections +func (api *API) LeakedCredentialCheckListDetections(ctx context.Context, rc *ResourceContainer, params LeakedCredentialCheckListDetectionsParams) ([]LeakedCredentialCheckDetectionEntry, error) { + if rc.Identifier == "" { + return []LeakedCredentialCheckDetectionEntry{}, ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/leaked-credential-checks/detections", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, params) + if err != nil { + return []LeakedCredentialCheckDetectionEntry{}, err + } + result := LeakedCredentialCheckListDetectionsResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return []LeakedCredentialCheckDetectionEntry{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result.Result, nil +} + +// LeakedCredentialCheckCreateDetection creates user-defined detection pattern for Leaked Credential Checks +// +// API reference: https://developers.cloudflare.com/api/operations/waf-product-api-leaked-credentials-create-detection +func (api *API) LeakedCredentialCheckCreateDetection(ctx context.Context, rc *ResourceContainer, params LeakedCredentialCheckCreateDetectionParams) (LeakedCredentialCheckDetectionEntry, error) { + if rc.Identifier == "" { + return LeakedCredentialCheckDetectionEntry{}, ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/leaked-credential-checks/detections", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params) + if err != nil { + return LeakedCredentialCheckDetectionEntry{}, err + } + result := LeakedCredentialCheckCreateDetectionResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return LeakedCredentialCheckDetectionEntry{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result.Result, nil +} + +// LeakedCredentialCheckDeleteDetection removes user-defined detection pattern for Leaked Credential Checks +// +// API reference: https://developers.cloudflare.com/api/operations/waf-product-api-leaked-credentials-delete-detection +func (api *API) LeakedCredentialCheckDeleteDetection(ctx context.Context, rc *ResourceContainer, params LeakedCredentialCheckDeleteDetectionParams) (LeakedCredentialCheckDeleteDetectionResponse, error) { + if rc.Identifier == "" { + return LeakedCredentialCheckDeleteDetectionResponse{}, ErrMissingZoneID + } + if params.DetectionID == "" { + return LeakedCredentialCheckDeleteDetectionResponse{}, ErrMissingDetectionID + } + + uri := fmt.Sprintf("/zones/%s/leaked-credential-checks/detections/%s", rc.Identifier, params.DetectionID) + res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return LeakedCredentialCheckDeleteDetectionResponse{}, err + } + result := LeakedCredentialCheckDeleteDetectionResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return LeakedCredentialCheckDeleteDetectionResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result, nil +} + +// LeakedCredentialCheckUpdateDetection updates user-defined detection pattern for Leaked Credential Checks. Returns updated detection. +// +// API reference: https://developers.cloudflare.com/api/operations/waf-product-api-leaked-credentials-update-detection +func (api *API) LeakedCredentialCheckUpdateDetection(ctx context.Context, rc *ResourceContainer, params LeakedCredentialCheckUpdateDetectionParams) (LeakedCredentialCheckDetectionEntry, error) { + if rc.Identifier == "" { + return LeakedCredentialCheckDetectionEntry{}, ErrMissingZoneID + } + if params.ID == "" { + return LeakedCredentialCheckDetectionEntry{}, ErrMissingDetectionID + } + + uri := fmt.Sprintf("/zones/%s/leaked-credential-checks/detections/%s", rc.Identifier, params.ID) + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params) + if err != nil { + return LeakedCredentialCheckDetectionEntry{}, err + } + result := LeakedCredentialCheckUpdateDetectionResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return LeakedCredentialCheckDetectionEntry{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result.Result, nil +} From e396705b29d565096257efbc6a92b7bb9ceec69f Mon Sep 17 00:00:00 2001 From: Michele Russo Date: Wed, 20 Nov 2024 17:33:02 +0100 Subject: [PATCH 3/6] Add test for new API wrapper for leak credential check --- leaked_credential_check_test.go | 190 ++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 leaked_credential_check_test.go diff --git a/leaked_credential_check_test.go b/leaked_credential_check_test.go new file mode 100644 index 00000000000..31c4588c750 --- /dev/null +++ b/leaked_credential_check_test.go @@ -0,0 +1,190 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLeakedCredentialCheckGetStatus(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "enabled": true + } + }`) + } + mux.HandleFunc("/zones/"+testZoneID+"/leaked-credential-checks", handler) + + want := LeakedCredentialCheckStatus{ + Enabled: true, + } + actual, err := client.LeakedCredentialCheckGetStatus(context.Background(), ZoneIdentifier(testZoneID), LeakedCredentialCheckGetStatusParams{}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestLeakedCredentialCheckSetStatus(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "enabled": false + } + }`) + } + mux.HandleFunc("/zones/"+testZoneID+"/leaked-credential-checks", handler) + + want := LeakedCredentialCheckStatus{ + Enabled: false, + } + actual, err := client.LeakedCredentialCheckSetStatus(context.Background(), ZoneIdentifier(testZoneID), LeakCredentialCheckSetStatusParams{false}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestLeakedCredentialCheckListDetections(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "id": "18a14bafaa8eb1df04ce683ec18c765e", + "password": "lookup_json_string(http.request.body.raw, \"secret\")", + "username": "lookup_json_string(http.request.body.raw, \"user\")" + } + ] + }`) + } + mux.HandleFunc("/zones/"+testZoneID+"/leaked-credential-checks/detections", handler) + + want := []LeakedCredentialCheckDetectionEntry{ + { + ID: "18a14bafaa8eb1df04ce683ec18c765e", + Password: "lookup_json_string(http.request.body.raw, \"secret\")", + Username: "lookup_json_string(http.request.body.raw, \"user\")", + }, + } + actual, err := client.LeakedCredentialCheckListDetections(context.Background(), ZoneIdentifier(testZoneID), LeakedCredentialCheckListDetectionsParams{}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestLeakedCredentialCheckCreateDetection(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "18a14bafaa8eb1df04ce683ec18c765e", + "password": "lookup_json_string(http.request.body.raw, \"secret\")", + "username": "lookup_json_string(http.request.body.raw, \"user\")" + } + }`) + } + mux.HandleFunc("/zones/"+testZoneID+"/leaked-credential-checks/detections", handler) + + want := LeakedCredentialCheckDetectionEntry{ + ID: "18a14bafaa8eb1df04ce683ec18c765e", + Password: "lookup_json_string(http.request.body.raw, \"secret\")", + Username: "lookup_json_string(http.request.body.raw, \"user\")", + } + // POST data + detectionPattern := LeakedCredentialCheckCreateDetectionParams{ + Username: "lookup_json_string(http.request.body.raw, \"secret\")", + Password: "lookup_json_string(http.request.body.raw, \"user\")", + } + actual, err := client.LeakedCredentialCheckCreateDetection(context.Background(), ZoneIdentifier(testZoneID), detectionPattern) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestLeakedCredentialCheckDeleteDetection(t *testing.T) { + setup() + detectionId := "cafb3307c5cc4c029d6bbd557b9e223a" + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [] + }`) + } + mux.HandleFunc("/zones/"+testZoneID+"/leaked-credential-checks/detections/"+detectionId, handler) + + want := LeakedCredentialCheckDeleteDetectionResponse{} + want.Success = true + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + want.Result = []struct{}{} + + actual, err := client.LeakedCredentialCheckDeleteDetection(context.Background(), ZoneIdentifier(testZoneID), LeakedCredentialCheckDeleteDetectionParams{detectionId}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestLeakedCredentialCheckUpdateDetection(t *testing.T) { + setup() + detectionId := "18a14bafaa8eb1df04ce683ec18c765e" + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "%s", + "password": "lookup_json_string(http.request.body.raw, \"secret\")", + "username": "lookup_json_string(http.request.body.raw, \"user\")" + } + }`, detectionId) + } + mux.HandleFunc("/zones/"+testZoneID+"/leaked-credential-checks/detections/"+detectionId, handler) + + want := LeakedCredentialCheckDetectionEntry{ + ID: detectionId, + Password: "lookup_json_string(http.request.body.raw, \"secret\")", + Username: "lookup_json_string(http.request.body.raw, \"user\")", + } + actual, err := client.LeakedCredentialCheckUpdateDetection(context.Background(), ZoneIdentifier(testZoneID), LeakedCredentialCheckUpdateDetectionParams{want}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} From ea51d51bf86b1ce14dffef9c54594cc1be5b051e Mon Sep 17 00:00:00 2001 From: Michele Russo Date: Wed, 20 Nov 2024 18:06:21 +0100 Subject: [PATCH 4/6] Add changelog entry --- .changelog/3634.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/3634.txt diff --git a/.changelog/3634.txt b/.changelog/3634.txt new file mode 100644 index 00000000000..d7e488f87d4 --- /dev/null +++ b/.changelog/3634.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +leaked_credential_check: add new methods to interact with leaked credential check cloudfare API +``` \ No newline at end of file From d11c2e9f297437f6e07572c61c4cd192c68f1281 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Thu, 21 Nov 2024 07:54:12 +1100 Subject: [PATCH 5/6] swap booleans to pointers https://github.com/cloudflare/cloudflare-go/blob/master/docs/conventions.md#booleans --- leaked_credential_check.go | 4 ++-- leaked_credential_check_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/leaked_credential_check.go b/leaked_credential_check.go index 22a355d3afe..85ee35074ad 100644 --- a/leaked_credential_check.go +++ b/leaked_credential_check.go @@ -11,7 +11,7 @@ import ( type LeakedCredentialCheckGetStatusParams struct{} type LeakedCredentialCheckStatus struct { - Enabled bool `json:"enabled"` + Enabled *bool `json:"enabled"` } type LeakCredentialCheckStatusResponse struct { @@ -20,7 +20,7 @@ type LeakCredentialCheckStatusResponse struct { } type LeakCredentialCheckSetStatusParams struct { - Enabled bool `json:"enabled"` + Enabled *bool `json:"enabled"` } type LeakedCredentialCheckListDetectionsParams struct{} diff --git a/leaked_credential_check_test.go b/leaked_credential_check_test.go index 31c4588c750..84796704d55 100644 --- a/leaked_credential_check_test.go +++ b/leaked_credential_check_test.go @@ -27,7 +27,7 @@ func TestLeakedCredentialCheckGetStatus(t *testing.T) { mux.HandleFunc("/zones/"+testZoneID+"/leaked-credential-checks", handler) want := LeakedCredentialCheckStatus{ - Enabled: true, + Enabled: BoolPtr(true), } actual, err := client.LeakedCredentialCheckGetStatus(context.Background(), ZoneIdentifier(testZoneID), LeakedCredentialCheckGetStatusParams{}) if assert.NoError(t, err) { @@ -53,7 +53,7 @@ func TestLeakedCredentialCheckSetStatus(t *testing.T) { mux.HandleFunc("/zones/"+testZoneID+"/leaked-credential-checks", handler) want := LeakedCredentialCheckStatus{ - Enabled: false, + Enabled: BoolPtr(false), } actual, err := client.LeakedCredentialCheckSetStatus(context.Background(), ZoneIdentifier(testZoneID), LeakCredentialCheckSetStatusParams{false}) if assert.NoError(t, err) { From e7d32a70d5d23a4ac67169e21f774ce1373f1117 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Thu, 21 Nov 2024 08:55:27 +1100 Subject: [PATCH 6/6] swap `false` to boolptr helper --- leaked_credential_check_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leaked_credential_check_test.go b/leaked_credential_check_test.go index 84796704d55..c069bdba263 100644 --- a/leaked_credential_check_test.go +++ b/leaked_credential_check_test.go @@ -55,7 +55,7 @@ func TestLeakedCredentialCheckSetStatus(t *testing.T) { want := LeakedCredentialCheckStatus{ Enabled: BoolPtr(false), } - actual, err := client.LeakedCredentialCheckSetStatus(context.Background(), ZoneIdentifier(testZoneID), LeakCredentialCheckSetStatusParams{false}) + actual, err := client.LeakedCredentialCheckSetStatus(context.Background(), ZoneIdentifier(testZoneID), LeakCredentialCheckSetStatusParams{BoolPtr(false)}) if assert.NoError(t, err) { assert.Equal(t, want, actual) }