initial commit

This commit is contained in:
Steven Polley 2024-11-05 17:09:54 -07:00
commit 00718515f8
4 changed files with 241 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
env.sh
cfcleaner
*.exe

5
README.md Normal file
View 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
View File

@ -0,0 +1,3 @@
module code.stevenpolley.net/steven/cfcleaner
go 1.23.2

230
main.go Normal file
View 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
}