dnscontrol/pkg/cloudflare-go/waf.go
Tom Limoncelli 7fd6a74e0c
CLOUDFLAREAPI: CF_REDIRECT/CF_TEMP_REDIRECT should dtrt using Single Redirects (#3002)
Co-authored-by: Josh Zhang <jzhang1@stackoverflow.com>
2024-06-18 17:38:50 -04:00

353 lines
10 KiB
Go

package cloudflare
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"github.com/goccy/go-json"
)
// WAFPackage represents a WAF package configuration.
type WAFPackage struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
ZoneID string `json:"zone_id"`
DetectionMode string `json:"detection_mode"`
Sensitivity string `json:"sensitivity"`
ActionMode string `json:"action_mode"`
}
// WAFPackagesResponse represents the response from the WAF packages endpoint.
type WAFPackagesResponse struct {
Response
Result []WAFPackage `json:"result"`
ResultInfo ResultInfo `json:"result_info"`
}
// WAFPackageResponse represents the response from the WAF package endpoint.
type WAFPackageResponse struct {
Response
Result WAFPackage `json:"result"`
ResultInfo ResultInfo `json:"result_info"`
}
// WAFPackageOptions represents options to edit a WAF package.
type WAFPackageOptions struct {
Sensitivity string `json:"sensitivity,omitempty"`
ActionMode string `json:"action_mode,omitempty"`
}
// WAFGroup represents a WAF rule group.
type WAFGroup struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
RulesCount int `json:"rules_count"`
ModifiedRulesCount int `json:"modified_rules_count"`
PackageID string `json:"package_id"`
Mode string `json:"mode"`
AllowedModes []string `json:"allowed_modes"`
}
// WAFGroupsResponse represents the response from the WAF groups endpoint.
type WAFGroupsResponse struct {
Response
Result []WAFGroup `json:"result"`
ResultInfo ResultInfo `json:"result_info"`
}
// WAFGroupResponse represents the response from the WAF group endpoint.
type WAFGroupResponse struct {
Response
Result WAFGroup `json:"result"`
ResultInfo ResultInfo `json:"result_info"`
}
// WAFRule represents a WAF rule.
type WAFRule struct {
ID string `json:"id"`
Description string `json:"description"`
Priority string `json:"priority"`
PackageID string `json:"package_id"`
Group struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"group"`
Mode string `json:"mode"`
DefaultMode string `json:"default_mode"`
AllowedModes []string `json:"allowed_modes"`
}
// WAFRulesResponse represents the response from the WAF rules endpoint.
type WAFRulesResponse struct {
Response
Result []WAFRule `json:"result"`
ResultInfo ResultInfo `json:"result_info"`
}
// WAFRuleResponse represents the response from the WAF rule endpoint.
type WAFRuleResponse struct {
Response
Result WAFRule `json:"result"`
ResultInfo ResultInfo `json:"result_info"`
}
// WAFRuleOptions is a subset of WAFRule, for editable options.
type WAFRuleOptions struct {
Mode string `json:"mode"`
}
// ListWAFPackages returns a slice of the WAF packages for the given zone.
//
// API Reference: https://api.cloudflare.com/#waf-rule-packages-list-firewall-packages
func (api *API) ListWAFPackages(ctx context.Context, zoneID string) ([]WAFPackage, error) {
// Construct a query string
v := url.Values{}
// Request as many WAF packages as possible per page - API max is 100
v.Set("per_page", "100")
var packages []WAFPackage
var res []byte
var err error
page := 1
// Loop over makeRequest until what we've fetched all records
for {
v.Set("page", strconv.Itoa(page))
uri := fmt.Sprintf("/zones/%s/firewall/waf/packages?%s", zoneID, v.Encode())
res, err = api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return []WAFPackage{}, err
}
var p WAFPackagesResponse
err = json.Unmarshal(res, &p)
if err != nil {
return []WAFPackage{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
if !p.Success {
// TODO: Provide an actual error message instead of always returning nil
return []WAFPackage{}, err
}
packages = append(packages, p.Result...)
if p.ResultInfo.Page >= p.ResultInfo.TotalPages {
break
}
// Loop around and fetch the next page
page++
}
return packages, nil
}
// WAFPackage returns a WAF package for the given zone.
//
// API Reference: https://api.cloudflare.com/#waf-rule-packages-firewall-package-details
func (api *API) WAFPackage(ctx context.Context, zoneID, packageID string) (WAFPackage, error) {
uri := fmt.Sprintf("/zones/%s/firewall/waf/packages/%s", zoneID, packageID)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return WAFPackage{}, err
}
var r WAFPackageResponse
err = json.Unmarshal(res, &r)
if err != nil {
return WAFPackage{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// UpdateWAFPackage lets you update the a WAF Package.
//
// API Reference: https://api.cloudflare.com/#waf-rule-packages-edit-firewall-package
func (api *API) UpdateWAFPackage(ctx context.Context, zoneID, packageID string, opts WAFPackageOptions) (WAFPackage, error) {
uri := fmt.Sprintf("/zones/%s/firewall/waf/packages/%s", zoneID, packageID)
res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, opts)
if err != nil {
return WAFPackage{}, err
}
var r WAFPackageResponse
err = json.Unmarshal(res, &r)
if err != nil {
return WAFPackage{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// ListWAFGroups returns a slice of the WAF groups for the given WAF package.
//
// API Reference: https://api.cloudflare.com/#waf-rule-groups-list-rule-groups
func (api *API) ListWAFGroups(ctx context.Context, zoneID, packageID string) ([]WAFGroup, error) {
// Construct a query string
v := url.Values{}
// Request as many WAF groups as possible per page - API max is 100
v.Set("per_page", "100")
var groups []WAFGroup
var res []byte
var err error
page := 1
// Loop over makeRequest until what we've fetched all records
for {
v.Set("page", strconv.Itoa(page))
uri := fmt.Sprintf("/zones/%s/firewall/waf/packages/%s/groups?%s", zoneID, packageID, v.Encode())
res, err = api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return []WAFGroup{}, err
}
var r WAFGroupsResponse
err = json.Unmarshal(res, &r)
if err != nil {
return []WAFGroup{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
if !r.Success {
// TODO: Provide an actual error message instead of always returning nil
return []WAFGroup{}, err
}
groups = append(groups, r.Result...)
if r.ResultInfo.Page >= r.ResultInfo.TotalPages {
break
}
// Loop around and fetch the next page
page++
}
return groups, nil
}
// WAFGroup returns a WAF rule group from the given WAF package.
//
// API Reference: https://api.cloudflare.com/#waf-rule-groups-rule-group-details
func (api *API) WAFGroup(ctx context.Context, zoneID, packageID, groupID string) (WAFGroup, error) {
uri := fmt.Sprintf("/zones/%s/firewall/waf/packages/%s/groups/%s", zoneID, packageID, groupID)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return WAFGroup{}, err
}
var r WAFGroupResponse
err = json.Unmarshal(res, &r)
if err != nil {
return WAFGroup{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// UpdateWAFGroup lets you update the mode of a WAF Group.
//
// API Reference: https://api.cloudflare.com/#waf-rule-groups-edit-rule-group
func (api *API) UpdateWAFGroup(ctx context.Context, zoneID, packageID, groupID, mode string) (WAFGroup, error) {
opts := WAFRuleOptions{Mode: mode}
uri := fmt.Sprintf("/zones/%s/firewall/waf/packages/%s/groups/%s", zoneID, packageID, groupID)
res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, opts)
if err != nil {
return WAFGroup{}, err
}
var r WAFGroupResponse
err = json.Unmarshal(res, &r)
if err != nil {
return WAFGroup{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// ListWAFRules returns a slice of the WAF rules for the given WAF package.
//
// API Reference: https://api.cloudflare.com/#waf-rules-list-rules
func (api *API) ListWAFRules(ctx context.Context, zoneID, packageID string) ([]WAFRule, error) {
// Construct a query string
v := url.Values{}
// Request as many WAF rules as possible per page - API max is 100
v.Set("per_page", "100")
var rules []WAFRule
var res []byte
var err error
page := 1
// Loop over makeRequest until what we've fetched all records
for {
v.Set("page", strconv.Itoa(page))
uri := fmt.Sprintf("/zones/%s/firewall/waf/packages/%s/rules?%s", zoneID, packageID, v.Encode())
res, err = api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return []WAFRule{}, err
}
var r WAFRulesResponse
err = json.Unmarshal(res, &r)
if err != nil {
return []WAFRule{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
if !r.Success {
// TODO: Provide an actual error message instead of always returning nil
return []WAFRule{}, err
}
rules = append(rules, r.Result...)
if r.ResultInfo.Page >= r.ResultInfo.TotalPages {
break
}
// Loop around and fetch the next page
page++
}
return rules, nil
}
// WAFRule returns a WAF rule from the given WAF package.
//
// API Reference: https://api.cloudflare.com/#waf-rules-rule-details
func (api *API) WAFRule(ctx context.Context, zoneID, packageID, ruleID string) (WAFRule, error) {
uri := fmt.Sprintf("/zones/%s/firewall/waf/packages/%s/rules/%s", zoneID, packageID, ruleID)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return WAFRule{}, err
}
var r WAFRuleResponse
err = json.Unmarshal(res, &r)
if err != nil {
return WAFRule{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// UpdateWAFRule lets you update the mode of a WAF Rule.
//
// API Reference: https://api.cloudflare.com/#waf-rules-edit-rule
func (api *API) UpdateWAFRule(ctx context.Context, zoneID, packageID, ruleID, mode string) (WAFRule, error) {
opts := WAFRuleOptions{Mode: mode}
uri := fmt.Sprintf("/zones/%s/firewall/waf/packages/%s/rules/%s", zoneID, packageID, ruleID)
res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, opts)
if err != nil {
return WAFRule{}, err
}
var r WAFRuleResponse
err = json.Unmarshal(res, &r)
if err != nil {
return WAFRule{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}