initial commit
This commit is contained in:
commit
00718515f8
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
env.sh
|
||||||
|
cfcleaner
|
||||||
|
*.exe
|
5
README.md
Normal file
5
README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Clownflare Cleaner
|
||||||
|
|
||||||
|
I personally use this tool to identify stale DNS records for stevenpolley.net. All it does is grab all your zones, then loop through each zone. For each zone, it grabs each record and loops through each record. For each record, it checks DNS query analytics and then provides a report showing records and their query metrics. This can then be used to manually identify stale records and then perhaps take manual action afterwards. It does not make any changes to Cloudflare directly and only requires read access.
|
||||||
|
|
||||||
|
The entire project makes use of only the Go standard library, no third party dependencies at all. Also CF's own golang SDK is missing features to retrieve analytics for individual DNS records.
|
3
go.mod
Normal file
3
go.mod
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module code.stevenpolley.net/steven/cfcleaner
|
||||||
|
|
||||||
|
go 1.23.2
|
230
main.go
Normal file
230
main.go
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/csv"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const apiURL = "https://api.cloudflare.com/client/v4"
|
||||||
|
|
||||||
|
var apiKey string
|
||||||
|
|
||||||
|
type Zone struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Record struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ZoneID string `json:"zone_id"`
|
||||||
|
ZoneName string `json:"zone_name"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Proxiable bool `json:"proxiable"`
|
||||||
|
Proxied bool `json:"proxied"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
CreatedOn time.Time `json:"created_on"`
|
||||||
|
ModifiedOn time.Time `json:"modified_on"`
|
||||||
|
NumberQueries int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZoneResult struct {
|
||||||
|
Result []Zone `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordResult struct {
|
||||||
|
Result []Record `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Analytic struct {
|
||||||
|
Result struct {
|
||||||
|
Rows int `json:"rows"`
|
||||||
|
Data []struct {
|
||||||
|
Metrics []int `json:"metrics"`
|
||||||
|
} `json:"data"`
|
||||||
|
DataLag int `json:"data_lag"`
|
||||||
|
Min struct {
|
||||||
|
} `json:"min"`
|
||||||
|
Max struct {
|
||||||
|
} `json:"max"`
|
||||||
|
Totals struct {
|
||||||
|
QueryCount int `json:"queryCount"`
|
||||||
|
} `json:"totals"`
|
||||||
|
Query struct {
|
||||||
|
Dimensions []any `json:"dimensions"`
|
||||||
|
Metrics []string `json:"metrics"`
|
||||||
|
Since time.Time `json:"since"`
|
||||||
|
Until time.Time `json:"until"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
} `json:"query"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
apiKey = os.Getenv("cfapikey")
|
||||||
|
|
||||||
|
zones, err := getZones()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to get zones: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outRows := make([]Record, 0)
|
||||||
|
|
||||||
|
for _, zone := range zones {
|
||||||
|
log.Printf("processing zone '%s' with ID '%s'", zone.Name, zone.ID)
|
||||||
|
|
||||||
|
records, err := getRecords(zone)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to get records for zone '%s' with ID '%s': %v", zone.Name, zone.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, record := range records {
|
||||||
|
analytic, err := getRecordAnalytic(zone, record)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to get record report for record '%s' in zone '%s' with ID '%s': %v", record.Name, zone.Name, zone.ID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
record.NumberQueries = analytic.Result.Totals.QueryCount
|
||||||
|
outRows = append(outRows, record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(outRows, func(i, j int) bool {
|
||||||
|
return outRows[i].NumberQueries < outRows[j].NumberQueries
|
||||||
|
})
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
writer := csv.NewWriter(buf)
|
||||||
|
writer.Write([]string{"Name", "Type", "NumberQueries", "CreatedOn", "ModifiedOn", "Comment", "Content"})
|
||||||
|
|
||||||
|
for _, row := range outRows {
|
||||||
|
writer.Write([]string{row.Name, row.Type, strconv.Itoa(row.NumberQueries), row.CreatedOn.Format(time.RFC3339), row.ModifiedOn.Format(time.RFC3339), row.Comment, row.Content})
|
||||||
|
}
|
||||||
|
writer.Flush()
|
||||||
|
|
||||||
|
b, err := io.ReadAll(buf)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read bytes from output buffer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(b))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getZones() ([]Zone, error) {
|
||||||
|
req, err := http.NewRequest("GET", fmt.Sprintf("%s/zones", apiURL), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create new http request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
//req.Header.Add("X-Auth-Email", apiKey)
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiKey))
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to execute http request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("http response has status '%d' but expected '%d'", resp.StatusCode, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read http response body: %v", err)
|
||||||
|
}
|
||||||
|
zoneResult := &ZoneResult{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(b, zoneResult)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal response body to struct: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return zoneResult.Result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRecords(zone Zone) ([]Record, error) {
|
||||||
|
req, err := http.NewRequest("GET", fmt.Sprintf("%s/zones/%s/dns_records", apiURL, zone.ID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create new http request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
//req.Header.Add("X-Auth-Email", apiKey)
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiKey))
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to execute http request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("http response has status '%d' but expected '%d'", resp.StatusCode, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read http response body: %v", err)
|
||||||
|
}
|
||||||
|
recordResult := &RecordResult{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(b, recordResult)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal response body to struct: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return recordResult.Result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRecordAnalytic(zone Zone, record Record) (*Analytic, error) {
|
||||||
|
req, err := http.NewRequest("GET", fmt.Sprintf("%s/zones/%s/dns_analytics/report", apiURL, zone.ID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create new http request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
//req.Header.Add("X-Auth-Email", apiKey)
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiKey))
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Add("filters", fmt.Sprintf("queryName==%s", record.Name))
|
||||||
|
//q.Add("filters", fmt.Sprintf("queryName==%s,queryType==%s", record.Name, record.Type))
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to execute http request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("http response has status '%d' but expected '%d'", resp.StatusCode, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read http response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
analytic := &Analytic{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(b, analytic)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal response body to struct: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return analytic, nil
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user