diff --git a/pkg/abuseipdb/client.go b/pkg/abuseipdb/client.go index 14870a1..45c4f65 100644 --- a/pkg/abuseipdb/client.go +++ b/pkg/abuseipdb/client.go @@ -27,6 +27,45 @@ const ( defaultUserAgent = "go-abuseipdb/" + Version ) +const ( + endpointCheck = "check" + endpointReports = "reports" + endpointBlacklist = "blacklist" + endpointReport = "report" + endpointCheckBlock = "check-block" + endpointBulkReport = "bulk-report" + endpointClearAddress = "clear-address" +) + +// ReportCategory is documented at https://www.abuseipdb.com/categories +type ReportCategory int + +const ( + DnsCompromise ReportCategory = iota + 1 + DnsPoisoning + FraudOrders + DDoSAttack + FtpBruteForce + PingOfDeath + Phishing + FraudVoIp + OpenProxy + WebSpam + EmailSpam + BlogSpam + VpnIp + PortScan + Hacking + SqlInjection + Spoofing + BruteForce + BadWebBot + ExploitedHost + WebAppAttack + Ssh + IotTargeted +) + type Client struct { client *http.Client BaseURL *url.URL @@ -51,6 +90,7 @@ func NewClient(client *http.Client) *Client { return c } +// AddApiKey pass api key from https://www.abuseipdb.com/account/api func (c *Client) AddApiKey(key string) { c.APIKey = key } @@ -126,6 +166,9 @@ func (c *Client) do(ctx context.Context, req *http.Request, v any) error { return err } defer resp.Body.Close() + if err := handleError(resp); err != nil { + return err + } switch v := v.(type) { case nil: @@ -170,16 +213,15 @@ type CheckResult struct { } type CheckResultReport struct { - ReportedAt time.Time `json:"reportedAt"` - Comment string `json:"comment"` - Categories []int `json:"categories"` - ReporterId int `json:"reporterId"` - ReporterCountryCode string `json:"reporterCountryCode"` - ReporterCountryName string `json:"reporterCountryName"` + ReportedAt time.Time `json:"reportedAt"` + Comment string `json:"comment"` + Categories []ReportCategory `json:"categories"` + ReporterId int `json:"reporterId"` + ReporterCountryCode string `json:"reporterCountryCode"` + ReporterCountryName string `json:"reporterCountryName"` } -var checkEndpoint = "check" - +// Check an IP for abuse and additional information func (c *Client) Check(ctx context.Context, ip net.IP, opts *CheckOptions) (*CheckResult, error) { ipAddress := html.EscapeString(ip.String()) parameters := map[string]string{ @@ -195,7 +237,7 @@ func (c *Client) Check(ctx context.Context, ip net.IP, opts *CheckOptions) (*Che } } - req, err := c.newRequest(http.MethodGet, checkEndpoint, parameters, nil) + req, err := c.newRequest(http.MethodGet, endpointCheck, parameters, nil) if err != nil { return nil, err } @@ -208,13 +250,15 @@ func (c *Client) Check(ctx context.Context, ip net.IP, opts *CheckOptions) (*Che return &result, nil } -// CheckCached uses the client cache and the ip as a key +// CheckCached same as Check but uses the client cache and the ip as a key (cache has to be added to client for this to work) func (c *Client) CheckCached(ctx context.Context, ip net.IP, opts *CheckOptions) (*CheckResult, error) { - key := fmt.Sprintf("%s:%s", checkEndpoint, ip.String()) + if c.cache == nil { + return nil, errors.New("cache is missing") + } + key := fmt.Sprintf("%s:%s", endpointCheck, ip.String()) if r, err := c.cache.Get(key); err == nil { result := CheckResult{} if jsonErr := json.NewDecoder(bytes.NewReader(r)).Decode(&result); jsonErr == nil { - fmt.Println("return cached") return &result, nil } } @@ -251,16 +295,16 @@ type ReportsResult struct { } `json:"data"` } type ReportResultReport struct { - ReportedAt time.Time `json:"reportedAt"` - Comment string `json:"comment"` - Categories []int `json:"categories"` - ReporterId int `json:"reporterId"` - ReporterCountryCode string `json:"reporterCountryCode"` - ReporterCountryName string `json:"reporterCountryName"` + ReportedAt time.Time `json:"reportedAt"` + Comment string `json:"comment"` + Categories []ReportCategory `json:"categories"` + ReporterId int `json:"reporterId"` + ReporterCountryCode string `json:"reporterCountryCode"` + ReporterCountryName string `json:"reporterCountryName"` } +// Reports returns reports for a certain IP func (c *Client) Reports(ctx context.Context, ip net.IP, opts *ReportsOptions) (*ReportsResult, error) { - var endpoint = "reports" ipAddress := html.EscapeString(ip.String()) parameters := map[string]string{ "ipAddress": ipAddress, @@ -278,7 +322,7 @@ func (c *Client) Reports(ctx context.Context, ip net.IP, opts *ReportsOptions) ( } } - req, err := c.newRequest(http.MethodGet, endpoint, parameters, nil) + req, err := c.newRequest(http.MethodGet, endpointReports, parameters, nil) if err != nil { return nil, err } @@ -332,12 +376,11 @@ func handleBlacklistOptions(opts *BlacklistOptions) map[string]string { return parameters } +// Blacklist will generate a blacklist based on the Options func (c *Client) Blacklist(ctx context.Context, opts *BlacklistOptions) (*BlacklistResult, error) { - var endpoint = "blacklist" - parameters := handleBlacklistOptions(opts) - req, err := c.newRequest(http.MethodGet, endpoint, parameters, nil) + req, err := c.newRequest(http.MethodGet, endpointBlacklist, parameters, nil) if err != nil { return nil, err } @@ -350,12 +393,11 @@ func (c *Client) Blacklist(ctx context.Context, opts *BlacklistOptions) (*Blackl return &result, nil } +// BlacklistPlain same as Blacklist but returns a newline separated string func (c *Client) BlacklistPlain(ctx context.Context, opts *BlacklistOptions) (io.Reader, error) { - var endpoint = "blacklist" - parameters := handleBlacklistOptions(opts) - req, err := c.newRequest(http.MethodGet, endpoint, parameters, nil) + req, err := c.newRequest(http.MethodGet, endpointBlacklist, parameters, nil) if err != nil { return nil, err } @@ -374,10 +416,11 @@ func (c *Client) BlacklistPlain(ctx context.Context, opts *BlacklistOptions) (io return &result, nil } +// ReportOptions at least Categories is required type ReportOptions struct { - Categories []int `json:"categories"` - Comment string `json:"comment,omitempty"` - Time *time.Time `json:"timestamp,omitempty"` + Categories []ReportCategory `json:"categories"` + Comment string `json:"comment,omitempty"` + Time *time.Time `json:"timestamp,omitempty"` } type ReportResult struct { @@ -387,9 +430,8 @@ type ReportResult struct { } `json:"data"` } +// Report sends a report for an IP with additional Information func (c *Client) Report(ctx context.Context, ip net.IP, opts *ReportOptions) (*ReportResult, error) { - var endpoint = "report" - ipAddress := html.EscapeString(ip.String()) parameters := map[string]string{ "ip": ipAddress, @@ -399,7 +441,7 @@ func (c *Client) Report(ctx context.Context, ip net.IP, opts *ReportOptions) (*R if opts.Categories != nil { categories := make([]string, len(opts.Categories)) for i, category := range opts.Categories { - categories[i] = strconv.Itoa(category) + categories[i] = strconv.Itoa(int(category)) } parameters["categories"] = strings.Join(categories, ",") } @@ -411,7 +453,7 @@ func (c *Client) Report(ctx context.Context, ip net.IP, opts *ReportOptions) (*R } } - req, err := c.newRequest(http.MethodPost, endpoint, parameters, nil) + req, err := c.newRequest(http.MethodPost, endpointReport, parameters, nil) if err != nil { return nil, err } @@ -445,13 +487,15 @@ type CheckBlockResult struct { } `json:"data"` } +// CheckBlock test an IPNet of up to /16 and returns all addresses with reports, it is recommended to check /24 networks func (c *Client) CheckBlock(ctx context.Context, ipnNet *net.IPNet, opts *CheckBlockOptions) (*CheckBlockResult, error) { - var endpoint = "check-block" - network := html.EscapeString(ipnNet.String()) parameters := map[string]string{ "network": network, } + if cidr, _ := ipnNet.Mask.Size(); cidr < 16 { + return nil, fmt.Errorf("CIDR must be less than or equal to 16") + } if opts != nil { if opts.MaxAgeInDays > 0 && opts.MaxAgeInDays <= 365 { @@ -459,7 +503,7 @@ func (c *Client) CheckBlock(ctx context.Context, ipnNet *net.IPNet, opts *CheckB } } - req, err := c.newRequest(http.MethodGet, endpoint, parameters, nil) + req, err := c.newRequest(http.MethodGet, endpointCheckBlock, parameters, nil) if err != nil { return nil, err } @@ -472,11 +516,37 @@ func (c *Client) CheckBlock(ctx context.Context, ipnNet *net.IPNet, opts *CheckB return &result, nil } +// CheckBlockCached same as CheckBlock but uses the client cache and the ipnet as a key (cache has to be added to client for this to work) +func (c *Client) CheckBlockCached(ctx context.Context, ipNet *net.IPNet, opts *CheckBlockOptions) (*CheckBlockResult, error) { + if c.cache == nil { + return nil, errors.New("cache is missing") + } + key := fmt.Sprintf("%s:%s", endpointCheck, ipNet.String()) + if r, err := c.cache.Get(key); err == nil { + result := CheckBlockResult{} + if jsonErr := json.NewDecoder(bytes.NewReader(r)).Decode(&result); jsonErr == nil { + return &result, nil + } + } + if result, err := c.CheckBlock(ctx, ipNet, opts); err == nil { + resultEncoded := bytes.Buffer{} + if jsonErr := json.NewEncoder(&resultEncoded).Encode(result); jsonErr != nil { + return nil, jsonErr + } + if re := resultEncoded.Bytes(); re != nil { + c.cache.Set(key, re) + } + return result, nil + } else { + return nil, err + } +} + type BulkReportData struct { - IpAddress string `csv:"IP"` - Categories []int `csv:"Categories"` - ReportDate time.Time `csv:"ReportDate"` - Comment string `csv:"Comment"` + IpAddress string `csv:"IP"` + Categories []ReportCategory `csv:"Categories"` + ReportDate time.Time `csv:"ReportDate"` + Comment string `csv:"Comment"` } type BulkReportDatas []BulkReportData @@ -510,7 +580,7 @@ func (b *BulkReportDatas) toSlice() [][]string { for _, data := range *b { categories := make([]string, len(data.Categories)) for i, category := range data.Categories { - categories[i] = strconv.Itoa(category) + categories[i] = strconv.Itoa(int(category)) } slice = append(slice, []string{ data.IpAddress, @@ -578,9 +648,8 @@ type BulkReportResult struct { } `json:"data"` } +// BulkReport takes a list of reports the success for ich reported entry func (c *Client) BulkReport(ctx context.Context, data *BulkReportDatas) (*BulkReportResult, error) { - var endpoint = "bulk-report" - if data == nil || len(*data) == 0 { return nil, errors.New("bulk report: no data") } @@ -593,7 +662,7 @@ func (c *Client) BulkReport(ctx context.Context, data *BulkReportDatas) (*BulkRe } requestBody, contentType := toMultipartCsv(csvData) - req, err := c.newRequest(http.MethodPost, endpoint, nil, requestBody) + req, err := c.newRequest(http.MethodPost, endpointBulkReport, nil, requestBody) if err != nil { return nil, err } @@ -613,14 +682,14 @@ type ClearAddressData struct { } `json:"data"` } +// ClearAddress allow one to delete all reports made from your account for a certain address func (c *Client) ClearAddress(ctx context.Context, ip net.IP) (*ClearAddressData, error) { - var endpoint = "clear-address" ipAddress := html.EscapeString(ip.String()) parameters := map[string]string{ "ipAddress": ipAddress, } - req, err := c.newRequest(http.MethodDelete, endpoint, parameters, nil) + req, err := c.newRequest(http.MethodDelete, endpointClearAddress, parameters, nil) if err != nil { return nil, err }