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 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 diff --git a/leaked_credential_check.go b/leaked_credential_check.go new file mode 100644 index 00000000000..85ee35074ad --- /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 +} diff --git a/leaked_credential_check_test.go b/leaked_credential_check_test.go new file mode 100644 index 00000000000..c069bdba263 --- /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: BoolPtr(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: BoolPtr(false), + } + actual, err := client.LeakedCredentialCheckSetStatus(context.Background(), ZoneIdentifier(testZoneID), LeakCredentialCheckSetStatusParams{BoolPtr(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) + } +}