Repository: cloudflare/cloudflare-go (go)

Issue:
Add support for intelligence
### Current cloudflare-go version

v0.43.0

### Description

Add support for intelligence endpoint

https://api.cloudflare.com/#domain-intelligence-get-domain-details

https://api.cloudflare.com/#ip-intelligence-properties

https://api.cloudflare.com/#asn-intelligence-properties 

### Use cases

Be able to query intelligence cloudflare-go

### Potential cloudflare-go usage

```go
result, err := cloudflare.IntelligenceIP(...)
```


### References

Happy to work on this. Wanted to make sure that the beta status was okay with this project.

Pull request:
Intelligence API
Adds support for the intelligence API

## Description

Resolves #965 

## Has your change been tested?

Unit tests have been added for all functions.

## Types of changes

What sort of change does your code introduce/modify?

- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [X] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [X] 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 (api.cloudflare.com or developers.cloudflare.com) and stable APIs.



Patch:
diff --git a/intelligence_asn.go b/intelligence_asn.go
new file mode 100644
index 000000000..014d2a54d
--- /dev/null
+++ b/intelligence_asn.go
@@ -0,0 +1,100 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"github.com/pkg/errors"
+	"net/http"
+)
+
+// ErrMissingASN is for when ASN is required but not set.
+var ErrMissingASN = errors.New("required asn missing")
+
+// ASNInfo represents ASN information.
+type ASNInfo struct {
+	ASN         int      `json:"asn"`
+	Description string   `json:"description"`
+	Country     string   `json:"country"`
+	Type        string   `json:"type"`
+	DomainCount int      `json:"domain_count"`
+	TopDomains  []string `json:"top_domains"`
+}
+
+// IntelligenceASNOverviewParameters represents parameters for an ASN request.
+type IntelligenceASNOverviewParameters struct {
+	AccountID string
+	ASN       int
+}
+
+// IntelligenceASNResponse represents an API response for ASN info.
+type IntelligenceASNResponse struct {
+	Response
+	Result []ASNInfo `json:"result,omitempty"`
+}
+
+// IntelligenceASNSubnetsParameters represents parameters for an ASN subnet request.
+type IntelligenceASNSubnetsParameters struct {
+	AccountID string
+	ASN       int
+}
+
+// IntelligenceASNSubnetResponse represents an ASN subnet API response.
+type IntelligenceASNSubnetResponse struct {
+	ASN          int      `json:"asn,omitempty"`
+	IPCountTotal int      `json:"ip_count_total,omitempty"`
+	Subnets      []string `json:"subnets,omitempty"`
+	Count        int      `json:"count,omitempty"`
+	Page         int      `json:"page,omitempty"`
+	PerPage      int      `json:"per_page,omitempty"`
+}
+
+// IntelligenceASNOverview get overview for an ASN number
+//
+// API Reference: https://api.cloudflare.com/#asn-intelligence-get-asn-overview
+func (api *API) IntelligenceASNOverview(ctx context.Context, params IntelligenceASNOverviewParameters) ([]ASNInfo, error) {
+	if params.AccountID == "" {
+		return []ASNInfo{}, ErrMissingAccountID
+	}
+
+	if params.ASN == 0 {
+		return []ASNInfo{}, ErrMissingASN
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/intel/asn/%d", params.AccountID, params.ASN)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return []ASNInfo{}, err
+	}
+
+	var asnInfoResponse IntelligenceASNResponse
+	if err := json.Unmarshal(res, &asnInfoResponse); err != nil {
+		return []ASNInfo{}, err
+	}
+	return asnInfoResponse.Result, nil
+}
+
+// IntelligenceASNSubnets gets all subnets of an ASN
+//
+// API Reference: https://api.cloudflare.com/#asn-intelligence-get-asn-subnets
+func (api *API) IntelligenceASNSubnets(ctx context.Context, params IntelligenceASNSubnetsParameters) (IntelligenceASNSubnetResponse, error) {
+	if params.AccountID == "" {
+		return IntelligenceASNSubnetResponse{}, ErrMissingAccountID
+	}
+
+	if params.ASN == 0 {
+		return IntelligenceASNSubnetResponse{}, ErrMissingASN
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/intel/asn/%d/subnets", params.AccountID, params.ASN)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return IntelligenceASNSubnetResponse{}, err
+	}
+
+	var intelligenceASNSubnetResponse IntelligenceASNSubnetResponse
+	if err := json.Unmarshal(res, &intelligenceASNSubnetResponse); err != nil {
+		return IntelligenceASNSubnetResponse{}, err
+	}
+	return intelligenceASNSubnetResponse, nil
+}
diff --git a/intelligence_domain.go b/intelligence_domain.go
new file mode 100644
index 000000000..fabdaa597
--- /dev/null
+++ b/intelligence_domain.go
@@ -0,0 +1,176 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"github.com/pkg/errors"
+	"net/http"
+)
+
+// ErrMissingDomain is for when domain is needed but not given.
+var ErrMissingDomain = errors.New("required domain missing")
+
+// DomainDetails represents details for a domain.
+type DomainDetails struct {
+	Domain                string                `json:"domain"`
+	ResolvesToRefs        []ResolvesToRefs      `json:"resolves_to_refs"`
+	PopularityRank        int                   `json:"popularity_rank"`
+	Application           Application           `json:"application"`
+	RiskTypes             []interface{}         `json:"risk_types"`
+	ContentCategories     []ContentCategories   `json:"content_categories"`
+	AdditionalInformation AdditionalInformation `json:"additional_information"`
+}
+
+// ResolvesToRefs what a domain resolves to.
+type ResolvesToRefs struct {
+	ID    string `json:"id"`
+	Value string `json:"value"`
+}
+
+type Application struct {
+	ID   int    `json:"id"`
+	Name string `json:"name"`
+}
+
+// ContentCategories represents the categories for a domain.
+type ContentCategories struct {
+	ID              int    `json:"id"`
+	SuperCategoryID int    `json:"super_category_id"`
+	Name            string `json:"name"`
+}
+
+// AdditionalInformation represents any additional information for a domain.
+type AdditionalInformation struct {
+	SuspectedMalwareFamily string `json:"suspected_malware_family"`
+}
+
+// DomainHistory represents the history for a domain.
+type DomainHistory struct {
+	Domain          string            `json:"domain"`
+	Categorizations []Categorizations `json:"categorizations"`
+}
+
+// Categories represents categories for a domain.
+type Categories struct {
+	ID   int    `json:"id"`
+	Name string `json:"name"`
+}
+
+// Categorizations represents the categories and when those categories were set.
+type Categorizations struct {
+	Categories []Categories `json:"categories"`
+	Start      string       `json:"start"`
+	End        string       `json:"end"`
+}
+
+// GetDomainDetailsParameters represent the parameters for a domain details request.
+type GetDomainDetailsParameters struct {
+	AccountID string `url:"-"`
+	Domain    string `url:"domain,omitempty"`
+}
+
+// DomainDetailsResponse represents an API response for domain details.
+type DomainDetailsResponse struct {
+	Response
+	Result DomainDetails `json:"result,omitempty"`
+}
+
+// GetBulkDomainDetailsParameters represents the parameters for bulk domain details request.
+type GetBulkDomainDetailsParameters struct {
+	AccountID string   `url:"-"`
+	Domains   []string `url:"domain"`
+}
+
+// GetBulkDomainDetailsResponse represents an API response for bulk domain details.
+type GetBulkDomainDetailsResponse struct {
+	Response
+	Result []DomainDetails `json:"result,omitempty"`
+}
+
+// GetDomainHistoryParameters represents the parameters for domain history request.
+type GetDomainHistoryParameters struct {
+	AccountID string `url:"-"`
+	Domain    string `url:"domain,omitempty"`
+}
+
+// GetDomainHistoryResponse represents an API response for domain history.
+type GetDomainHistoryResponse struct {
+	Response
+	Result []DomainHistory `json:"result,omitempty"`
+}
+
+// IntelligenceDomainDetails gets domain information.
+//
+// API Reference: https://api.cloudflare.com/#domain-intelligence-get-domain-details
+func (api *API) IntelligenceDomainDetails(ctx context.Context, params GetDomainDetailsParameters) (DomainDetails, error) {
+	if params.AccountID == "" {
+		return DomainDetails{}, ErrMissingAccountID
+	}
+
+	if params.Domain == "" {
+		return DomainDetails{}, ErrMissingDomain
+	}
+
+	uri := buildURI(fmt.Sprintf("/accounts/%s/intel/domain", params.AccountID), params)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return DomainDetails{}, err
+	}
+
+	var domainDetails DomainDetailsResponse
+	if err := json.Unmarshal(res, &domainDetails); err != nil {
+		return DomainDetails{}, err
+	}
+	return domainDetails.Result, nil
+}
+
+// IntelligenceBulkDomainDetails gets domain information for a list of domains.
+//
+// API Reference: https://api.cloudflare.com/#domain-intelligence-get-multiple-domain-details
+func (api *API) IntelligenceBulkDomainDetails(ctx context.Context, params GetBulkDomainDetailsParameters) ([]DomainDetails, error) {
+	if params.AccountID == "" {
+		return []DomainDetails{}, ErrMissingAccountID
+	}
+
+	if len(params.Domains) == 0 {
+		return []DomainDetails{}, ErrMissingDomain
+	}
+
+	uri := buildURI(fmt.Sprintf("/accounts/%s/intel/domain/bulk", params.AccountID), params)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return []DomainDetails{}, err
+	}
+
+	var domainDetails GetBulkDomainDetailsResponse
+	if err := json.Unmarshal(res, &domainDetails); err != nil {
+		return []DomainDetails{}, err
+	}
+	return domainDetails.Result, nil
+}
+
+// IntelligenceDomainHistory get domain history for given domain
+//
+// API Reference: https://api.cloudflare.com/#domain-history-get-domain-history
+func (api *API) IntelligenceDomainHistory(ctx context.Context, params GetDomainHistoryParameters) ([]DomainHistory, error) {
+	if params.AccountID == "" {
+		return []DomainHistory{}, ErrMissingAccountID
+	}
+
+	if params.Domain == "" {
+		return []DomainHistory{}, ErrMissingDomain
+	}
+
+	uri := buildURI(fmt.Sprintf("/accounts/%s/intel/domain-history", params.AccountID), params)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return []DomainHistory{}, err
+	}
+
+	var domainDetails GetDomainHistoryResponse
+	if err := json.Unmarshal(res, &domainDetails); err != nil {
+		return []DomainHistory{}, err
+	}
+	return domainDetails.Result, nil
+}
diff --git a/intelligence_ip.go b/intelligence_ip.go
new file mode 100644
index 000000000..1a8948b7a
--- /dev/null
+++ b/intelligence_ip.go
@@ -0,0 +1,155 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+// IPIntelligence represents IP intelligence information.
+type IPIntelligence struct {
+	IP           string       `json:"ip"`
+	BelongsToRef BelongsToRef `json:"belongs_to_ref"`
+	RiskTypes    []RiskTypes  `json:"risk_types"`
+}
+
+// BelongsToRef represents information about who owns an IP address.
+type BelongsToRef struct {
+	ID          string `json:"id"`
+	Value       int    `json:"value"`
+	Type        string `json:"type"`
+	Country     string `json:"country"`
+	Description string `json:"description"`
+}
+
+// RiskTypes represent risk types for an IP.
+type RiskTypes struct {
+	ID              int    `json:"id"`
+	SuperCategoryID int    `json:"super_category_id"`
+	Name            string `json:"name"`
+}
+
+// IPPassiveDNS represent DNS response.
+type IPPassiveDNS struct {
+	ReverseRecords []ReverseRecords `json:"reverse_records,omitempty"`
+	Count          int              `json:"count,omitempty"`
+	Page           int              `json:"page,omitempty"`
+	PerPage        int              `json:"per_page,omitempty"`
+}
+
+// ReverseRecords represent records for passive DNS.
+type ReverseRecords struct {
+	FirstSeen string `json:"first_seen,omitempty"`
+	LastSeen  string `json:"last_seen,omitempty"`
+	Hostname  string `json:"hostname,omitempty"`
+}
+
+// IPIntelligenceParameters represents parameters for an IP Intelligence request.
+type IPIntelligenceParameters struct {
+	AccountID string `url:"-"`
+	IPv4      string `url:"ipv4,omitempty"`
+	IPv6      string `url:"ipv6,omitempty"`
+}
+
+// IPIntelligenceResponse represents an IP Intelligence API response.
+type IPIntelligenceResponse struct {
+	Response
+	Result []IPIntelligence `json:"result,omitempty"`
+}
+
+// IPIntelligenceListParameters represents the parameters for an IP list request.
+type IPIntelligenceListParameters struct {
+	AccountID string
+}
+
+// IPIntelligenceItem represents an item in an IP list.
+type IPIntelligenceItem struct {
+	ID   int    `json:"id,omitempty"`
+	Name string `json:"name,omitempty"`
+}
+
+// IPIntelligenceListResponse represents the response for an IP list API response.
+type IPIntelligenceListResponse struct {
+	Response
+	Result []IPIntelligenceItem `json:"result,omitempty"`
+}
+
+// IPIntelligencePassiveDNSParameters represents the parameters for a passive DNS request.
+type IPIntelligencePassiveDNSParameters struct {
+	AccountID string `url:"-"`
+	IPv4      string `url:"ipv4,omitempty"`
+	Start     string `url:"start,omitempty"`
+	End       string `url:"end,omitempty"`
+	Page      int    `url:"page,omitempty"`
+	PerPage   int    `url:"per_page,omitempty"`
+}
+
+// IPIntelligencePassiveDNSResponse represents a passive API response.
+type IPIntelligencePassiveDNSResponse struct {
+	Response
+	Result IPPassiveDNS `json:"result,omitempty"`
+}
+
+// IntelligenceGetIPOverview gets information about ipv4 or ipv6 address.
+//
+// API Reference: https://api.cloudflare.com/#ip-intelligence-get-ip-overview
+func (api *API) IntelligenceGetIPOverview(ctx context.Context, params IPIntelligenceParameters) ([]IPIntelligence, error) {
+	if params.AccountID == "" {
+		return []IPIntelligence{}, ErrMissingAccountID
+	}
+
+	uri := buildURI(fmt.Sprintf("/accounts/%s/intel/ip", params.AccountID), params)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return []IPIntelligence{}, err
+	}
+
+	var ipDetails IPIntelligenceResponse
+	if err := json.Unmarshal(res, &ipDetails); err != nil {
+		return []IPIntelligence{}, err
+	}
+	return ipDetails.Result, nil
+}
+
+// IntelligenceGetIPList gets intelligence ip-lists.
+//
+// API Reference: https://api.cloudflare.com/#ip-list-get-ip-lists
+func (api *API) IntelligenceGetIPList(ctx context.Context, params IPIntelligenceListParameters) ([]IPIntelligenceItem, error) {
+	if params.AccountID == "" {
+		return []IPIntelligenceItem{}, ErrMissingAccountID
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/intel/ip-list", params.AccountID)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return []IPIntelligenceItem{}, err
+	}
+
+	var ipListItem IPIntelligenceListResponse
+	if err := json.Unmarshal(res, &ipListItem); err != nil {
+		return []IPIntelligenceItem{}, err
+	}
+	return ipListItem.Result, nil
+}
+
+// IntelligencePassiveDNS gets a history of DNS for an ip.
+//
+// API Reference: https://api.cloudflare.com/#passive-dns-by-ip-get-passive-dns-by-ip
+func (api *API) IntelligencePassiveDNS(ctx context.Context, params IPIntelligencePassiveDNSParameters) (IPPassiveDNS, error) {
+	if params.AccountID == "" {
+		return IPPassiveDNS{}, ErrMissingAccountID
+	}
+
+	uri := buildURI(fmt.Sprintf("/accounts/%s/intel/dns", params.AccountID), params)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return IPPassiveDNS{}, err
+	}
+
+	var passiveDNS IPIntelligencePassiveDNSResponse
+	if err := json.Unmarshal(res, &passiveDNS); err != nil {
+		return IPPassiveDNS{}, err
+	}
+	return passiveDNS.Result, nil
+}
diff --git a/intelligence_phishing.go b/intelligence_phishing.go
new file mode 100644
index 000000000..2d649ffc5
--- /dev/null
+++ b/intelligence_phishing.go
@@ -0,0 +1,51 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+// PhishingScan represent information about a phishing scan.
+type PhishingScan struct {
+	URL        string  `json:"url"`
+	Phishing   bool    `json:"phishing"`
+	Verified   bool    `json:"verified"`
+	Score      float64 `json:"score"`
+	Classifier string  `json:"classifier"`
+}
+
+// PhishingScanParameters represent parameters for a phishing scan request.
+type PhishingScanParameters struct {
+	AccountID string `url:"-"`
+	URL       string `url:"url,omitempty"`
+	Skip      bool   `url:"skip,omitempty"`
+}
+
+// PhishingScanResponse represent an API response for a phishing scan.
+type PhishingScanResponse struct {
+	Response
+	Result PhishingScan `json:"result,omitempty"`
+}
+
+// IntelligencePhishingScan scans a URL for suspected phishing
+//
+// API Reference: https://api.cloudflare.com/#phishing-url-scanner-scan-suspicious-url
+func (api *API) IntelligencePhishingScan(ctx context.Context, params PhishingScanParameters) (PhishingScan, error) {
+	if params.AccountID == "" {
+		return PhishingScan{}, ErrMissingAccountID
+	}
+
+	uri := buildURI(fmt.Sprintf("/accounts/%s/intel-phishing/predict", params.AccountID), params)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return PhishingScan{}, err
+	}
+
+	var phishingScanResponse PhishingScanResponse
+	if err := json.Unmarshal(res, &phishingScanResponse); err != nil {
+		return PhishingScan{}, err
+	}
+	return phishingScanResponse.Result, nil
+}
diff --git a/intelligence_whois.go b/intelligence_whois.go
new file mode 100644
index 000000000..a3bda99b9
--- /dev/null
+++ b/intelligence_whois.go
@@ -0,0 +1,59 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+// WHOIS represents whois information.
+type WHOIS struct {
+	Domain            string   `json:"domain,omitempty"`
+	CreatedDate       string   `json:"created_date,omitempty"`
+	UpdatedDate       string   `json:"updated_date,omitempty"`
+	Registrant        string   `json:"registrant,omitempty"`
+	RegistrantOrg     string   `json:"registrant_org,omitempty"`
+	RegistrantCountry string   `json:"registrant_country,omitempty"`
+	RegistrantEmail   string   `json:"registrant_email,omitempty"`
+	Registrar         string   `json:"registrar,omitempty"`
+	Nameservers       []string `json:"nameservers,omitempty"`
+}
+
+// WHOISParameters represents parameters for a who is request.
+type WHOISParameters struct {
+	AccountID string `url:"-"`
+	Domain    string `url:"domain"`
+}
+
+// WHOISResponse represents an API response for a whois request.
+type WHOISResponse struct {
+	Response
+	Result WHOIS `json:"result,omitempty"`
+}
+
+// IntelligenceWHOIS gets whois information for a domain.
+//
+// API Reference: https://api.cloudflare.com/#whois-record-get-whois-record
+func (api *API) IntelligenceWHOIS(ctx context.Context, params WHOISParameters) (WHOIS, error) {
+	if params.AccountID == "" {
+		return WHOIS{}, ErrMissingAccountID
+	}
+
+	if params.Domain == "" {
+		return WHOIS{}, ErrMissingDomain
+	}
+
+	uri := buildURI(fmt.Sprintf("/accounts/%s/intel/whois", params.AccountID), params)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return WHOIS{}, err
+	}
+
+	var whoisResponse WHOISResponse
+	if err := json.Unmarshal(res, &whoisResponse); err != nil {
+		return WHOIS{}, err
+	}
+
+	return whoisResponse.Result, nil
+}


Test patch:
diff --git a/intelligence_asn_test.go b/intelligence_asn_test.go
new file mode 100644
index 000000000..6d20dc6f3
--- /dev/null
+++ b/intelligence_asn_test.go
@@ -0,0 +1,110 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"testing"
+)
+
+const testASNNumber = "13335"
+
+func TestIntelligence_ASNOverview(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/intel/asn/"+testASNNumber, func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": [
+    {
+      "asn": 13335,
+      "description": "CLOUDFLARENET",
+      "country": "US",
+      "type": "hosting_provider",
+      "domain_count": 1,
+      "top_domains": [
+        "example.com"
+      ]
+    }
+  ]
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.IntelligenceASNOverview(context.Background(), IntelligenceASNOverviewParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+	// Make sure missing ASN is thrown
+	_, err = client.IntelligenceASNOverview(context.Background(), IntelligenceASNOverviewParameters{AccountID: testAccountID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingASN, err)
+	}
+	want := ASNInfo{
+		ASN:         13335,
+		Description: "CLOUDFLARENET",
+		Country:     "US",
+		Type:        "hosting_provider",
+		DomainCount: 1,
+		TopDomains:  []string{"example.com"},
+	}
+
+	out, err := client.IntelligenceASNOverview(context.Background(), IntelligenceASNOverviewParameters{AccountID: testAccountID, ASN: 13335})
+	if assert.NoError(t, err) {
+		assert.Equal(t, len(out), 1, "Length of ASN overview not expected")
+		assert.Equal(t, out[0], want, "structs not equal")
+	}
+}
+
+func TestIntelligence_ASNSubnet(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/intel/asn/"+testASNNumber+"/subnets", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+  "asn": 13335,
+  "ip_count_total": 1,
+  "subnets": [
+    "192.0.2.0/24",
+    "2001:DB8::/32"
+  ],
+  "count": 1,
+  "page": 1,
+  "per_page": 20
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.IntelligenceASNSubnets(context.Background(), IntelligenceASNSubnetsParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	// Make sure missing ASN is thrown
+	_, err = client.IntelligenceASNSubnets(context.Background(), IntelligenceASNSubnetsParameters{AccountID: testAccountID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingASN, err)
+	}
+
+	want := IntelligenceASNSubnetResponse{
+		ASN:          13335,
+		IPCountTotal: 1,
+		Subnets:      []string{"192.0.2.0/24", "2001:DB8::/32"},
+		Count:        1,
+		Page:         1,
+		PerPage:      20,
+	}
+
+	out, err := client.IntelligenceASNSubnets(context.Background(), IntelligenceASNSubnetsParameters{AccountID: testAccountID, ASN: 13335})
+	if assert.NoError(t, err) {
+		assert.Equal(t, out, want, "structs not equal")
+	}
+}
diff --git a/intelligence_domain_test.go b/intelligence_domain_test.go
new file mode 100644
index 000000000..f6ef9e436
--- /dev/null
+++ b/intelligence_domain_test.go
@@ -0,0 +1,226 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"testing"
+)
+
+func TestIntelligence_DomainDetails(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/intel/domain", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": {
+    "domain": "cloudflare.com",
+    "resolves_to_refs": [
+      {
+        "id": "ipv4-addr--baa568ec-6efe-5902-be55-0663833db537",
+        "value": "192.0.2.0"
+      }
+    ],
+    "popularity_rank": 18,
+    "application": {
+      "id": 1370,
+      "name": "CLOUDFLARE"
+    },
+    "risk_types": [],
+    "content_categories": [
+      {
+        "id": 155,
+        "super_category_id": 26,
+        "name": "Technology"
+      }
+    ],
+    "additional_information": {
+      "suspected_malware_family": ""
+    }
+  }
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.IntelligenceDomainDetails(context.Background(), GetDomainDetailsParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+	// Make sure missing domain is thrown
+	_, err = client.IntelligenceDomainDetails(context.Background(), GetDomainDetailsParameters{AccountID: testAccountID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingDomain, err)
+	}
+	want := DomainDetails{
+		Domain: "cloudflare.com",
+		ResolvesToRefs: []ResolvesToRefs{
+			{
+				ID:    "ipv4-addr--baa568ec-6efe-5902-be55-0663833db537",
+				Value: "192.0.2.0",
+			},
+		},
+		PopularityRank: 18,
+		Application: Application{
+			ID:   1370,
+			Name: "CLOUDFLARE",
+		},
+		RiskTypes: []interface{}{},
+		ContentCategories: []ContentCategories{
+			{
+				ID:              155,
+				SuperCategoryID: 26,
+				Name:            "Technology",
+			},
+		},
+		AdditionalInformation: AdditionalInformation{
+			SuspectedMalwareFamily: "",
+		},
+	}
+
+	out, err := client.IntelligenceDomainDetails(context.Background(), GetDomainDetailsParameters{AccountID: testAccountID, Domain: "cloudflare.com"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, out, want, "structs not equal")
+	}
+}
+
+func TestIntelligence_BulkDomainDetails(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/intel/domain/bulk", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": [
+    {
+      "domain": "cloudflare.com",
+      "popularity_rank": 18,
+      "application": {
+        "id": 1370,
+        "name": "CLOUDFLARE"
+      },
+      "risk_types": [],
+      "content_categories": [
+        {
+          "id": 155,
+          "super_category_id": 26,
+          "name": "Technology"
+        }
+      ],
+      "additional_information": {
+        "suspected_malware_family": ""
+      }
+    }
+  ]
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.IntelligenceBulkDomainDetails(context.Background(), GetBulkDomainDetailsParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+	// Make sure missing domain is thrown
+	_, err = client.IntelligenceBulkDomainDetails(context.Background(), GetBulkDomainDetailsParameters{AccountID: testAccountID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingDomain, err)
+	}
+	want := DomainDetails{
+		Domain:         "cloudflare.com",
+		PopularityRank: 18,
+		Application: Application{
+			ID:   1370,
+			Name: "CLOUDFLARE",
+		},
+		RiskTypes: []interface{}{},
+		ContentCategories: []ContentCategories{
+			{
+				ID:              155,
+				SuperCategoryID: 26,
+				Name:            "Technology",
+			},
+		},
+		AdditionalInformation: AdditionalInformation{
+			SuspectedMalwareFamily: "",
+		},
+	}
+
+	out, err := client.IntelligenceBulkDomainDetails(context.Background(), GetBulkDomainDetailsParameters{AccountID: testAccountID, Domains: []string{"cloudflare.com"}})
+	if assert.NoError(t, err) {
+		assert.Equal(t, len(out), 1, "Length of ASN overview not expected")
+		assert.Equal(t, out[0], want, "structs not equal")
+	}
+}
+
+func TestIntelligence_DomainHistory(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/intel/domain-history", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": [
+    {
+      "domain": "cloudflare.com",
+      "categorizations": [
+        {
+          "categories": [
+            {
+              "id": 155,
+              "name": "Technology"
+            }
+          ],
+          "start": "2021-04-01",
+          "end": "2021-04-30"
+        }
+      ]
+    }
+  ]
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.IntelligenceDomainHistory(context.Background(), GetDomainHistoryParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+	// Make sure missing domain is thrown
+	_, err = client.IntelligenceDomainHistory(context.Background(), GetDomainHistoryParameters{AccountID: testAccountID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingDomain, err)
+	}
+	want := DomainHistory{
+		Domain: "cloudflare.com",
+		Categorizations: []Categorizations{
+			{
+				Categories: []Categories{
+					{
+						ID:   155,
+						Name: "Technology",
+					},
+				},
+				Start: "2021-04-01",
+				End:   "2021-04-30",
+			},
+		},
+	}
+
+	out, err := client.IntelligenceDomainHistory(context.Background(), GetDomainHistoryParameters{AccountID: testAccountID, Domain: "cloudflare.com"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, len(out), 1, "Length of Domain History not expected")
+		assert.Equal(t, out[0], want, "structs not equal")
+	}
+}
diff --git a/intelligence_ip_test.go b/intelligence_ip_test.go
new file mode 100644
index 000000000..829b867de
--- /dev/null
+++ b/intelligence_ip_test.go
@@ -0,0 +1,162 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"testing"
+)
+
+func TestIntelligence_GetIPOverview(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/intel/ip", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": [
+    {
+      "ip": "192.0.2.0",
+      "belongs_to_ref": {
+        "id": "autonomous-system--2fa28d71-3549-5a38-af05-770b79ad6ea8",
+        "value": 13335,
+        "type": "hosting_provider",
+        "country": "US",
+        "description": "CLOUDFLARENET"
+      },
+      "risk_types": [
+        {
+          "id": 131,
+          "super_category_id": 21,
+          "name": "Phishing"
+        }
+      ]
+    }
+  ]
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.IntelligenceGetIPOverview(context.Background(), IPIntelligenceParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	want := IPIntelligence{
+		IP: "192.0.2.0",
+		BelongsToRef: BelongsToRef{
+			ID:          "autonomous-system--2fa28d71-3549-5a38-af05-770b79ad6ea8",
+			Value:       13335,
+			Type:        "hosting_provider",
+			Country:     "US",
+			Description: "CLOUDFLARENET",
+		},
+		RiskTypes: []RiskTypes{
+			{
+				ID:              131,
+				SuperCategoryID: 21,
+				Name:            "Phishing",
+			},
+		},
+	}
+
+	out, err := client.IntelligenceGetIPOverview(context.Background(), IPIntelligenceParameters{AccountID: testAccountID, IPv4: "192.0.2.0", IPv6: "2001:0DB8::"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, len(out), 1, "IP overview length mismatch")
+		assert.Equal(t, out[0], want, "structs not equal")
+	}
+}
+
+func TestIntelligence_GetIPLists(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/intel/ip-list", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": [
+    {
+      "id": 3,
+      "name": "Malware"
+    }
+]
+}
+  `)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.IntelligenceGetIPList(context.Background(), IPIntelligenceListParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	want := []IPIntelligenceItem{
+		{
+			ID:   3,
+			Name: "Malware",
+		},
+	}
+
+	out, err := client.IntelligenceGetIPList(context.Background(), IPIntelligenceListParameters{AccountID: testAccountID})
+	if assert.NoError(t, err) {
+		assert.Equal(t, out, want, "structs not equal")
+	}
+}
+
+func TestIntelligence_PassiveDNS(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/intel/dns", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": {
+    "reverse_records": [
+      {
+        "first_seen": "2021-04-01",
+        "last_seen": "2021-04-30",
+        "hostname": "cloudflare.com"
+      }
+    ],
+    "count": 1,
+    "page": 1,
+    "per_page": 20
+  }
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.IntelligencePassiveDNS(context.Background(), IPIntelligencePassiveDNSParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	want := IPPassiveDNS{
+		Count:   1,
+		Page:    1,
+		PerPage: 20,
+		ReverseRecords: []ReverseRecords{{
+			FirstSeen: "2021-04-01",
+			LastSeen:  "2021-04-30",
+			Hostname:  "cloudflare.com",
+		}},
+	}
+
+	out, err := client.IntelligencePassiveDNS(context.Background(), IPIntelligencePassiveDNSParameters{AccountID: testAccountID})
+	if assert.NoError(t, err) {
+		assert.Equal(t, out, want, "structs not equal")
+	}
+}
diff --git a/intelligence_phishing_test.go b/intelligence_phishing_test.go
new file mode 100644
index 000000000..67f7fcef4
--- /dev/null
+++ b/intelligence_phishing_test.go
@@ -0,0 +1,50 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"testing"
+)
+
+func TestIntelligence_PhishingScan(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/intel-phishing/predict", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": {
+    "url": "https://www.cloudflare.com",
+    "phishing": false,
+    "verified": false,
+    "score": 0.99,
+    "classifier": "MACHINE_LEARNING_v2"
+  }
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.IntelligencePhishingScan(context.Background(), PhishingScanParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	want := PhishingScan{
+		URL:        "https://www.cloudflare.com",
+		Phishing:   false,
+		Verified:   false,
+		Score:      0.99,
+		Classifier: "MACHINE_LEARNING_v2",
+	}
+
+	out, err := client.IntelligencePhishingScan(context.Background(), PhishingScanParameters{AccountID: testAccountID, URL: "https://www.cloudflare.com"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, out, want, "structs not equal")
+	}
+}
diff --git a/intelligence_whois_test.go b/intelligence_whois_test.go
new file mode 100644
index 000000000..16a83b854
--- /dev/null
+++ b/intelligence_whois_test.go
@@ -0,0 +1,76 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"testing"
+)
+
+func TestIntelligence_WHOIS(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/intel/whois", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": {
+    "domain": "cloudflare.com",
+    "created_date": "2009-02-17",
+    "updated_date": "2017-05-24",
+    "registrant": "DATA REDACTED",
+    "registrant_org": "DATA REDACTED",
+    "registrant_country": "United States",
+    "registrant_email": "https://domaincontact.cloudflareregistrar.com/cloudflare.com",
+    "registrar": "Cloudflare, Inc.",
+    "nameservers": [
+      "ns3.cloudflare.com",
+      "ns4.cloudflare.com",
+      "ns5.cloudflare.com",
+      "ns6.cloudflare.com",
+      "ns7.cloudflare.com"
+    ]
+  }
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.IntelligenceWHOIS(context.Background(), WHOISParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	// Make sure missing domain is thrown
+	_, err = client.IntelligenceWHOIS(context.Background(), WHOISParameters{AccountID: testAccountID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingDomain, err)
+	}
+
+	want := WHOIS{
+		Domain:            "cloudflare.com",
+		CreatedDate:       "2009-02-17",
+		UpdatedDate:       "2017-05-24",
+		Registrant:        "DATA REDACTED",
+		RegistrantOrg:     "DATA REDACTED",
+		RegistrantCountry: "United States",
+		RegistrantEmail:   "https://domaincontact.cloudflareregistrar.com/cloudflare.com",
+		Registrar:         "Cloudflare, Inc.",
+		Nameservers: []string{
+			"ns3.cloudflare.com",
+			"ns4.cloudflare.com",
+			"ns5.cloudflare.com",
+			"ns6.cloudflare.com",
+			"ns7.cloudflare.com",
+		},
+	}
+
+	out, err := client.IntelligenceWHOIS(context.Background(), WHOISParameters{AccountID: testAccountID, Domain: "cloudflare.com"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, out, want, "structs not equal")
+	}
+}