crowdsec/pkg/cticlient/client.go
Thibault "bui" Koechlin 4f29ce2ee7
CTI API Helpers in expr (#1851)
* Add CTI API helpers in expr
* Allow profiles to have an `on_error` option to profiles

Co-authored-by: Sebastien Blot <sebastien@crowdsec.net>
2023-01-19 08:45:50 +01:00

157 lines
3.6 KiB
Go

package cticlient
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
log "github.com/sirupsen/logrus"
)
const (
CTIBaseUrl = "https://cti.api.crowdsec.net/v2"
smokeEndpoint = "/smoke"
fireEndpoint = "/fire"
)
var (
ErrUnauthorized = errors.New("unauthorized")
ErrLimit = errors.New("request quota exceeded, please reduce your request rate")
ErrNotFound = errors.New("ip not found")
ErrDisabled = errors.New("cti is disabled")
ErrUnknown = errors.New("unknown error")
)
type CrowdsecCTIClient struct {
httpClient *http.Client
apiKey string
Logger *log.Entry
}
func (c *CrowdsecCTIClient) doRequest(method string, endpoint string, params map[string]string) ([]byte, error) {
url := CTIBaseUrl + endpoint
if len(params) > 0 {
url += "?"
for k, v := range params {
url += fmt.Sprintf("%s=%s&", k, v)
}
}
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("x-api-key", c.apiKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusForbidden {
return nil, ErrUnauthorized
}
if resp.StatusCode == http.StatusTooManyRequests {
return nil, ErrLimit
}
if resp.StatusCode == http.StatusNotFound {
return nil, ErrNotFound
}
return nil, fmt.Errorf("unexpected http code : %s", resp.Status)
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return respBody, nil
}
func (c *CrowdsecCTIClient) GetIPInfo(ip string) (*SmokeItem, error) {
body, err := c.doRequest(http.MethodGet, smokeEndpoint+"/"+ip, nil)
if err != nil {
if err == ErrNotFound {
return &SmokeItem{}, nil
}
return nil, err
}
item := SmokeItem{}
err = json.Unmarshal(body, &item)
if err != nil {
return nil, err
}
return &item, nil
}
func (c *CrowdsecCTIClient) SearchIPs(ips []string) (*SearchIPResponse, error) {
params := make(map[string]string)
params["ips"] = strings.Join(ips, ",")
body, err := c.doRequest(http.MethodGet, smokeEndpoint, params)
if err != nil {
return nil, err
}
searchIPResponse := SearchIPResponse{}
err = json.Unmarshal(body, &searchIPResponse)
if err != nil {
return nil, err
}
return &searchIPResponse, nil
}
func (c *CrowdsecCTIClient) Fire(params FireParams) (*FireResponse, error) {
paramsMap := make(map[string]string)
if params.Page != nil {
paramsMap["page"] = fmt.Sprintf("%d", *params.Page)
}
if params.Since != nil {
paramsMap["since"] = *params.Since
}
if params.Limit != nil {
paramsMap["limit"] = fmt.Sprintf("%d", *params.Limit)
}
body, err := c.doRequest(http.MethodGet, fireEndpoint, paramsMap)
if err != nil {
return nil, err
}
fireResponse := FireResponse{}
err = json.Unmarshal(body, &fireResponse)
if err != nil {
return nil, err
}
return &fireResponse, nil
}
func NewCrowdsecCTIClient(options ...func(*CrowdsecCTIClient)) *CrowdsecCTIClient {
client := &CrowdsecCTIClient{}
for _, option := range options {
option(client)
}
if client.httpClient == nil {
client.httpClient = &http.Client{}
}
// we cannot return with a ni logger, so we set a default one
if client.Logger == nil {
client.Logger = log.NewEntry(log.New())
}
return client
}
func WithLogger(logger *log.Entry) func(*CrowdsecCTIClient) {
return func(c *CrowdsecCTIClient) {
c.Logger = logger
}
}
func WithHTTPClient(httpClient *http.Client) func(*CrowdsecCTIClient) {
return func(c *CrowdsecCTIClient) {
c.httpClient = httpClient
}
}
func WithAPIKey(apiKey string) func(*CrowdsecCTIClient) {
return func(c *CrowdsecCTIClient) {
c.apiKey = apiKey
}
}