Repository: cloudflare/cloudflare-go
Pull request #3700 (2024-12-04 14:42:43)

Add content scanning
# Description

Add new methods to interact with the [Content Scanning Cloudflare API](https://developers.cloudflare.com/api/operations/waf-content-scanning-disable)

## Types of changes

What sort of change does your code introduce/modify?

- [x] New feature (non-breaking change which adds functionality)

## Checklist:

- [x] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] 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.



Patch:
diff --git a/.changelog/3700.txt b/.changelog/3700.txt
new file mode 100644
index 000000000..4c92995b1
--- /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/CHANGELOG.md b/CHANGELOG.md
index 87187ed89..84a55a165 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,25 +1,4 @@
-## 0.112.0 (Unreleased)
-
-## 0.111.0 (December 4th, 2024)
-
-BREAKING CHANGES:
-
-* access: Remove the deprecated `SelfHostedDomains` field from Access applications. Use `Destinations` instead. ([#3667](https://github.com/cloudflare/cloudflare-go/issues/3667))
-
-ENHANCEMENTS:
-
-* Adds allow_email_alias connection rule boolean to access infra policy payload ([#3675](https://github.com/cloudflare/cloudflare-go/issues/3675))
-* access: Add `DomainType` field to Access applications. ([#3667](https://github.com/cloudflare/cloudflare-go/issues/3667))
-* leaked_credential_check: add new methods to interact with leaked credential check cloudfare API ([#3634](https://github.com/cloudflare/cloudflare-go/issues/3634))
-
-BUG FIXES:
-
-* authenticated_origin_pulls_per_hostname: fixes the struct to allow null (nil) values on the `Enabled` property ([#3680](https://github.com/cloudflare/cloudflare-go/issues/3680))
-* snippets_rules: fix upsert request type ([#3669](https://github.com/cloudflare/cloudflare-go/issues/3669))
-
-DEPENDENCIES:
-
-* deps: bumps github.com/stretchr/testify from 1.9.0 to 1.10.0 ([#3672](https://github.com/cloudflare/cloudflare-go/issues/3672))
+## 0.111.0 (Unreleased)
 
 ## 0.110.0 (November 20th, 2024)
 
diff --git a/content_scanning.go b/content_scanning.go
new file mode 100644
index 000000000..08d5139e4
--- /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
+}


Test patch:
diff --git a/content_scanning_test.go b/content_scanning_test.go
new file mode 100644
index 000000000..654719607
--- /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)
+	}
+}