diff --git a/.changelog/3700.txt b/.changelog/3700.txt new file mode 100644 index 00000000000..4c92995b1b4 --- /dev/null +++ b/.changelog/3700.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +content_scanning: Add new support for CRUD operations +``` \ No newline at end of file diff --git a/content_scanning.go b/content_scanning.go new file mode 100644 index 00000000000..08d5139e4bf --- /dev/null +++ b/content_scanning.go @@ -0,0 +1,189 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + + "github.com/goccy/go-json" +) + +type ContentScanningStatusResult struct { + Value string `json:"value"` + Modified string `json:"modified"` +} + +type ContentScanningEnableParams struct{} + +type ContentScanningEnableResponse struct { + Response + Result *ContentScanningStatusResult `json:"result"` // nil +} + +type ContentScanningDisableParams struct{} + +type ContentScanningDisableResponse struct { + Response + Result *ContentScanningStatusResult `json:"result"` // nil +} + +type ContentScanningStatusParams struct{} + +type ContentScanningStatusResponse struct { + Response + Result *ContentScanningStatusResult `json:"result"` // object or nil +} + +type ContentScanningCustomExpression struct { + ID string `json:"id"` + Payload string `json:"payload"` +} + +type ContentScanningListCustomExpressionsParams struct{} + +type ContentScanningListCustomExpressionsResponse struct { + Response + Result []ContentScanningCustomExpression `json:"result"` +} + +type ContentScanningCustomPayload struct { + Payload string `json:"payload"` +} + +type ContentScanningAddCustomExpressionsParams struct { + Payloads []ContentScanningCustomPayload +} + +type ContentScanningAddCustomExpressionsResponse struct { + Response + Result []ContentScanningCustomExpression `json:"result"` +} + +type ContentScanningDeleteCustomExpressionsParams struct { + ID string +} + +type ContentScanningDeleteCustomExpressionsResponse struct { + Response + Result []ContentScanningCustomExpression `json:"result"` +} + +// ContentScanningEnable enables Content Scanning. +// +// API reference: https://developers.cloudflare.com/api/operations/waf-content-scanning-enable +func (api *API) ContentScanningEnable(ctx context.Context, rc *ResourceContainer, params ContentScanningEnableParams) (ContentScanningEnableResponse, error) { + if rc.Identifier == "" { + return ContentScanningEnableResponse{}, ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/content-upload-scan/enable", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, nil) + if err != nil { + return ContentScanningEnableResponse{}, err + } + result := ContentScanningEnableResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return ContentScanningEnableResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result, nil +} + +// ContentScanningDisable disables Content Scanning. +// +// API reference: https://developers.cloudflare.com/api/operations/waf-content-scanning-disable +func (api *API) ContentScanningDisable(ctx context.Context, rc *ResourceContainer, params ContentScanningDisableParams) (ContentScanningDisableResponse, error) { + if rc.Identifier == "" { + return ContentScanningDisableResponse{}, ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/content-upload-scan/disable", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, nil) + if err != nil { + return ContentScanningDisableResponse{}, err + } + result := ContentScanningDisableResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return ContentScanningDisableResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result, nil +} + +// ContentScanningStatus reads the status of Content Scanning. +// +// API reference: https://developers.cloudflare.com/api/operations/waf-content-scanning-get-status +func (api *API) ContentScanningStatus(ctx context.Context, rc *ResourceContainer, params ContentScanningStatusParams) (ContentScanningStatusResponse, error) { + if rc.Identifier == "" { + return ContentScanningStatusResponse{}, ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/content-upload-scan/settings", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return ContentScanningStatusResponse{}, err + } + result := ContentScanningStatusResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return ContentScanningStatusResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result, nil +} + +// ContentScanningListCustomExpressions retrieves the list of existing custom scan expressions for Content Scanning +// +// API reference: https://developers.cloudflare.com/api/operations/waf-content-scanning-list-custom-scan-expressions +func (api *API) ContentScanningListCustomExpressions(ctx context.Context, rc *ResourceContainer, params ContentScanningListCustomExpressionsParams) ([]ContentScanningCustomExpression, error) { + if rc.Identifier == "" { + return []ContentScanningCustomExpression{}, ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/content-upload-scan/payloads", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return []ContentScanningCustomExpression{}, err + } + result := ContentScanningListCustomExpressionsResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return []ContentScanningCustomExpression{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result.Result, nil +} + +// ContentScanningAddCustomExpressions add new custom scan expressions for Content Scanning +// +// API reference: https://developers.cloudflare.com/api/operations/waf-content-scanning-list-custom-scan-expressions +func (api *API) ContentScanningAddCustomExpressions(ctx context.Context, rc *ResourceContainer, params ContentScanningAddCustomExpressionsParams) ([]ContentScanningCustomExpression, error) { + if rc.Identifier == "" { + return []ContentScanningCustomExpression{}, ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/content-upload-scan/payloads", rc.Identifier) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params.Payloads) + if err != nil { + return []ContentScanningCustomExpression{}, err + } + result := ContentScanningAddCustomExpressionsResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return []ContentScanningCustomExpression{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result.Result, nil +} + +// ContentScanningDeleteCustomExpressions delete custom scan expressions for Content Scanning +// +// API reference: https://developers.cloudflare.com/api/operations/waf-content-scanning-delete-custom-scan-expressions +func (api *API) ContentScanningDeleteCustomExpression(ctx context.Context, rc *ResourceContainer, params ContentScanningDeleteCustomExpressionsParams) ([]ContentScanningCustomExpression, error) { + if rc.Identifier == "" { + return []ContentScanningCustomExpression{}, ErrMissingZoneID + } + + uri := fmt.Sprintf("/zones/%s/content-upload-scan/payloads/%s", rc.Identifier, params.ID) + res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return []ContentScanningCustomExpression{}, err + } + result := ContentScanningDeleteCustomExpressionsResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return []ContentScanningCustomExpression{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + return result.Result, nil +} diff --git a/content_scanning_test.go b/content_scanning_test.go new file mode 100644 index 00000000000..654719607e8 --- /dev/null +++ b/content_scanning_test.go @@ -0,0 +1,230 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestContentScanningEnable(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": null + }`) + } + mux.HandleFunc("/zones/"+testZoneID+"/content-upload-scan/enable", handler) + + var want ContentScanningEnableResponse + want.Success = true + want.Result = nil + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + + actual, err := client.ContentScanningEnable(context.Background(), ZoneIdentifier(testZoneID), ContentScanningEnableParams{}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestContentScanningDisable(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": null + }`) + } + mux.HandleFunc("/zones/"+testZoneID+"/content-upload-scan/disable", handler) + + var want ContentScanningDisableResponse + want.Success = true + want.Result = nil + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + + actual, err := client.ContentScanningDisable(context.Background(), ZoneIdentifier(testZoneID), ContentScanningDisableParams{}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestContentScanningStatus(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": { + "value": "disabled", + "modified": "2024-12-03T13:34:38.376312Z" + } + }`) + } + mux.HandleFunc("/zones/"+testZoneID+"/content-upload-scan/settings", handler) + + var want ContentScanningStatusResponse + want.Success = true + want.Result = &ContentScanningStatusResult{ + Value: "disabled", + Modified: "2024-12-03T13:34:38.376312Z", + } + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + + actual, err := client.ContentScanningStatus(context.Background(), ZoneIdentifier(testZoneID), ContentScanningStatusParams{}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestContentScanningListCustomExpressions(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": "a350a054caa840c9becd89c3b4f0195b", + "payload": "lookup_json_string(http.request.body.raw, \"file\")" + }, + { + "id": "4e3b841073914278b7413d9c1e6581e4", + "payload": "lookup_json_string(http.request.body.raw, \"document\")" + } + ] + }`) + } + mux.HandleFunc("/zones/"+testZoneID+"/content-upload-scan/payloads", handler) + + want := []ContentScanningCustomExpression{ + { + ID: "a350a054caa840c9becd89c3b4f0195b", + Payload: "lookup_json_string(http.request.body.raw, \"file\")", + }, + { + ID: "4e3b841073914278b7413d9c1e6581e4", + Payload: "lookup_json_string(http.request.body.raw, \"document\")", + }, + } + + actual, err := client.ContentScanningListCustomExpressions(context.Background(), ZoneIdentifier(testZoneID), ContentScanningListCustomExpressionsParams{}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestContentScanningAddCustomExpressions(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": "a350a054caa840c9becd89c3b4f0195b", + "payload": "lookup_json_string(http.request.body.raw, \"alreadyExisting\")" + }, + { + "id": "4e3b841073914278b7413d9c1e6581e4", + "payload": "lookup_json_string(http.request.body.raw, \"OneNew\")" + }, + { + "id": "8f7403465db64e47bbb34bd7191186c6", + "payload": "lookup_json_string(http.request.body.raw, \"AnotherNew\")" + } + ] + }`) + } + mux.HandleFunc("/zones/"+testZoneID+"/content-upload-scan/payloads", handler) + + want := []ContentScanningCustomExpression{ + { + ID: "a350a054caa840c9becd89c3b4f0195b", + Payload: "lookup_json_string(http.request.body.raw, \"alreadyExisting\")", + }, + { + ID: "4e3b841073914278b7413d9c1e6581e4", + Payload: "lookup_json_string(http.request.body.raw, \"OneNew\")", + }, + { + ID: "8f7403465db64e47bbb34bd7191186c6", + Payload: "lookup_json_string(http.request.body.raw, \"AnotherNew\")", + }, + } + params := ContentScanningAddCustomExpressionsParams{ + Payloads: []ContentScanningCustomPayload{ + { + Payload: "lookup_json_string(http.request.body.raw, \"OneNew\")", + }, + { + Payload: "lookup_json_string(http.request.body.raw, \"AnotherNew\")", + }, + }, + } + actual, err := client.ContentScanningAddCustomExpressions(context.Background(), ZoneIdentifier(testZoneID), params) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestContentScanningDeleteCustomExpression(t *testing.T) { + setup() + payloadId := "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": [ + { + "id": "a350a054caa840c9becd89c3b4f0195b", + "payload": "lookup_json_string(http.request.body.raw, \"file\")" + } + ] + }`) + } + mux.HandleFunc("/zones/"+testZoneID+"/content-upload-scan/payloads/"+payloadId, handler) + + want := []ContentScanningCustomExpression{ + { + ID: "a350a054caa840c9becd89c3b4f0195b", + Payload: "lookup_json_string(http.request.body.raw, \"file\")", + }, + } + + actual, err := client.ContentScanningDeleteCustomExpression(context.Background(), ZoneIdentifier(testZoneID), ContentScanningDeleteCustomExpressionsParams{ID: payloadId}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +}