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>
This commit is contained in:
parent
0c35d9d43c
commit
4f29ce2ee7
20 changed files with 2301 additions and 9 deletions
|
@ -284,6 +284,13 @@ func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) e
|
|||
log.Warningln("Exprhelpers loaded without database client.")
|
||||
}
|
||||
|
||||
if cConfig.API.CTI != nil && *cConfig.API.CTI.Enabled {
|
||||
log.Infof("Crowdsec CTI helper enabled")
|
||||
if err := exprhelpers.InitCrowdsecCTI(cConfig.API.CTI.Key, cConfig.API.CTI.CacheTimeout, cConfig.API.CTI.CacheSize, cConfig.API.CTI.LogLevel); err != nil {
|
||||
return errors.Wrap(err, "failed to init crowdsec cti")
|
||||
}
|
||||
}
|
||||
|
||||
if !cConfig.DisableAPI {
|
||||
if cConfig.API.Server.OnlineClient == nil || cConfig.API.Server.OnlineClient.Credentials == nil {
|
||||
log.Warningf("Communication with CrowdSec Central API disabled from configuration file")
|
||||
|
|
2
go.sum
2
go.sum
|
@ -120,6 +120,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
|
|||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/blackfireio/osinfo v1.0.3 h1:Yk2t2GTPjBcESv6nDSWZKO87bGMQgO+Hi9OoXPpxX8c=
|
||||
github.com/blackfireio/osinfo v1.0.3/go.mod h1:Pd987poVNmd5Wsx6PRPw4+w7kLlf9iJxoRKPtPAjOrA=
|
||||
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
||||
|
|
|
@ -159,12 +159,13 @@ func (c *Controller) CreateAlert(gctx *gin.Context) {
|
|||
}
|
||||
|
||||
alert.MachineID = machineID
|
||||
//if coming from cscli, alert already has decisions
|
||||
if len(alert.Decisions) != 0 {
|
||||
for pIdx, profile := range c.Profiles {
|
||||
_, matched, err := profile.EvaluateProfile(alert)
|
||||
if err != nil {
|
||||
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
return
|
||||
profile.Logger.Warningf("error while evaluating profile %s : %v", profile.Cfg.Name, err)
|
||||
continue
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
|
@ -183,9 +184,22 @@ func (c *Controller) CreateAlert(gctx *gin.Context) {
|
|||
|
||||
for pIdx, profile := range c.Profiles {
|
||||
profileDecisions, matched, err := profile.EvaluateProfile(alert)
|
||||
forceBreak := false
|
||||
if err != nil {
|
||||
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
return
|
||||
switch profile.Cfg.OnError {
|
||||
case "apply":
|
||||
profile.Logger.Warningf("applying profile %s despite error: %s", profile.Cfg.Name, err)
|
||||
matched = true
|
||||
case "continue":
|
||||
profile.Logger.Warningf("skipping %s profile due to error: %s", profile.Cfg.Name, err)
|
||||
case "break":
|
||||
forceBreak = true
|
||||
case "ignore":
|
||||
profile.Logger.Warningf("ignoring error: %s", err)
|
||||
default:
|
||||
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !matched {
|
||||
|
@ -197,7 +211,7 @@ func (c *Controller) CreateAlert(gctx *gin.Context) {
|
|||
}
|
||||
profileAlert := *alert
|
||||
c.sendAlertToPluginChannel(&profileAlert, uint(pIdx))
|
||||
if profile.Cfg.OnSuccess == "break" {
|
||||
if profile.Cfg.OnSuccess == "break" || forceBreak {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
type APICfg struct {
|
||||
Client *LocalApiClientCfg `yaml:"client"`
|
||||
Server *LocalApiServerCfg `yaml:"server"`
|
||||
CTI *CTICfg `yaml:"cti"`
|
||||
}
|
||||
|
||||
type ApiCredentialsCfg struct {
|
||||
|
@ -45,6 +46,37 @@ type LocalApiClientCfg struct {
|
|||
InsecureSkipVerify *bool `yaml:"insecure_skip_verify"` // check if api certificate is bad or not
|
||||
}
|
||||
|
||||
type CTICfg struct {
|
||||
Key *string `yaml:"key,omitempty"`
|
||||
CacheTimeout *time.Duration `yaml:"cache_timeout,omitempty"`
|
||||
CacheSize *int `yaml:"cache_size,omitempty"`
|
||||
Enabled *bool `yaml:"enabled,omitempty"`
|
||||
LogLevel *log.Level `yaml:"log_level,omitempty"`
|
||||
}
|
||||
|
||||
func (a *CTICfg) Load() error {
|
||||
|
||||
if a.Key == nil {
|
||||
*a.Enabled = false
|
||||
}
|
||||
if a.Key != nil && *a.Key == "" {
|
||||
return fmt.Errorf("empty cti key")
|
||||
}
|
||||
if a.Enabled == nil {
|
||||
a.Enabled = new(bool)
|
||||
*a.Enabled = true
|
||||
}
|
||||
if a.CacheTimeout == nil {
|
||||
a.CacheTimeout = new(time.Duration)
|
||||
*a.CacheTimeout = 10 * time.Minute
|
||||
}
|
||||
if a.CacheSize == nil {
|
||||
a.CacheSize = new(int)
|
||||
*a.CacheSize = 100
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OnlineApiClientCfg) Load() error {
|
||||
o.Credentials = new(ApiCredentialsCfg)
|
||||
fcontent, err := os.ReadFile(o.CredentialsFilePath)
|
||||
|
@ -92,7 +124,7 @@ func (l *LocalApiClientCfg) Load() error {
|
|||
apiclient.InsecureSkipVerify = *l.InsecureSkipVerify
|
||||
}
|
||||
|
||||
if l.Credentials.CACertPath != "" {
|
||||
if l.Credentials.CACertPath != "" {
|
||||
caCert, err := os.ReadFile(l.Credentials.CACertPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to load cacert")
|
||||
|
@ -230,6 +262,15 @@ func (c *Config) LoadAPIServer() error {
|
|||
return errors.Wrap(err, "loading online client credentials")
|
||||
}
|
||||
}
|
||||
if c.API.Server.OnlineClient == nil || c.API.Server.OnlineClient.Credentials == nil {
|
||||
log.Printf("push and pull to Central API disabled")
|
||||
}
|
||||
|
||||
if c.API.CTI != nil {
|
||||
if err := c.API.CTI.Load(); err != nil {
|
||||
return errors.Wrap(err, "loading CTI configuration")
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.LoadDBConfig(); err != nil {
|
||||
return err
|
||||
|
|
|
@ -110,6 +110,9 @@ func NewDefaultConfig() *Config {
|
|||
CredentialsFilePath: DefaultConfigPath("config", "online-api-secrets.yaml"),
|
||||
},
|
||||
},
|
||||
CTI: &CTICfg{
|
||||
Enabled: types.BoolPtr(false),
|
||||
},
|
||||
}
|
||||
|
||||
dbConfig := DatabaseCfg{
|
||||
|
|
|
@ -11,7 +11,13 @@ import (
|
|||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
//Profile structure(s) are used by the local API to "decide" what kind of decision should be applied when a scenario with an active remediation has been triggered
|
||||
// var OnErrorDefault = OnErrorIgnore
|
||||
// var OnErrorContinue = "continue"
|
||||
// var OnErrorBreak = "break"
|
||||
// var OnErrorApply = "apply"
|
||||
// var OnErrorIgnore = "ignore"
|
||||
|
||||
// Profile structure(s) are used by the local API to "decide" what kind of decision should be applied when a scenario with an active remediation has been triggered
|
||||
type ProfileCfg struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Debug *bool `yaml:"debug,omitempty"`
|
||||
|
@ -20,6 +26,7 @@ type ProfileCfg struct {
|
|||
DurationExpr string `yaml:"duration_expr,omitempty"`
|
||||
OnSuccess string `yaml:"on_success,omitempty"` //continue or break
|
||||
OnFailure string `yaml:"on_failure,omitempty"` //continue or break
|
||||
OnError string `yaml:"on_error,omitempty"` //continue, break, error, report, apply, ignore
|
||||
Notifications []string `yaml:"notifications,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package csplugin
|
|||
import (
|
||||
"text/template"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||
)
|
||||
|
||||
|
@ -18,6 +19,7 @@ var helpers = template.FuncMap{
|
|||
}
|
||||
return metaValues
|
||||
},
|
||||
"CrowdsecCTI": exprhelpers.CrowdsecCTI,
|
||||
}
|
||||
|
||||
func funcMap() template.FuncMap {
|
||||
|
|
|
@ -46,7 +46,12 @@ func NewProfile(profilesCfg []*csconfig.ProfileCfg) ([]*Runtime, error) {
|
|||
runtime.RuntimeFilters = make([]*vm.Program, len(profile.Filters))
|
||||
runtime.DebugFilters = make([]*exprhelpers.ExprDebugger, len(profile.Filters))
|
||||
runtime.Cfg = profile
|
||||
|
||||
if runtime.Cfg.OnSuccess != "" && runtime.Cfg.OnSuccess != "continue" && runtime.Cfg.OnSuccess != "break" {
|
||||
return []*Runtime{}, errors.Wrapf(err, "invalid 'on_success' for '%s' : %s", profile.Name, runtime.Cfg.OnSuccess)
|
||||
}
|
||||
if runtime.Cfg.OnFailure != "" && runtime.Cfg.OnFailure != "continue" && runtime.Cfg.OnFailure != "break" && runtime.Cfg.OnFailure != "apply" {
|
||||
return []*Runtime{}, errors.Wrapf(err, "invalid 'on_failure' for '%s' : %s", profile.Name, runtime.Cfg.OnFailure)
|
||||
}
|
||||
for fIdx, filter := range profile.Filters {
|
||||
if runtimeFilter, err = expr.Compile(filter, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"Alert": &models.Alert{}}))); err != nil {
|
||||
return []*Runtime{}, errors.Wrapf(err, "error compiling filter of '%s'", profile.Name)
|
||||
|
@ -153,7 +158,7 @@ func (Profile *Runtime) GenerateDecisionFromProfile(Alert *models.Alert) ([]*mod
|
|||
return decisions, nil
|
||||
}
|
||||
|
||||
//EvaluateProfile is going to evaluate an Alert against a profile to generate Decisions
|
||||
// EvaluateProfile is going to evaluate an Alert against a profile to generate Decisions
|
||||
func (Profile *Runtime) EvaluateProfile(Alert *models.Alert) ([]*models.Decision, bool, error) {
|
||||
var decisions []*models.Decision
|
||||
|
||||
|
|
157
pkg/cticlient/client.go
Normal file
157
pkg/cticlient/client.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
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
|
||||
}
|
||||
}
|
294
pkg/cticlient/client_test.go
Normal file
294
pkg/cticlient/client_test.go
Normal file
|
@ -0,0 +1,294 @@
|
|||
package cticlient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const validApiKey = "my-api-key"
|
||||
|
||||
// Copy pasted from actual API response
|
||||
var smokeResponses = map[string]string{
|
||||
"1.1.1.1": `{"ip_range_score": 0, "ip": "1.1.1.1", "ip_range": "1.1.1.0/24", "as_name": "CLOUDFLARENET", "as_num": 13335, "location": {"country": null, "city": null, "latitude": null, "longitude": null}, "reverse_dns": "one.one.one.one", "behaviors": [{"name": "ssh:bruteforce", "label": "SSH Bruteforce", "description": "IP has been reported for performing brute force on ssh services."}, {"name": "tcp:scan", "label": "TCP Scan", "description": "IP has been reported for performing TCP port scanning."}, {"name": "http:scan", "label": "HTTP Scan", "description": "IP has been reported for performing actions related to HTTP vulnerability scanning and discovery."}], "history": {"first_seen": "2021-04-18T18:00:00+00:00", "last_seen": "2022-11-23T13:00:00+00:00", "full_age": 583, "days_age": 583}, "classifications": {"false_positives": [], "classifications": [{"name": "profile:insecure_services", "label": "Dangerous Services Exposed", "description": "IP exposes dangerous services (vnc, telnet, rdp), possibly due to a misconfiguration or because it's a honeypot."}, {"name": "profile:many_services", "label": "Many Services Exposed", "description": "IP exposes many open port, possibly due to a misconfiguration or because it's a honeypot."}]}, "attack_details": [{"name": "crowdsecurity/ssh-bf", "label": "SSH Bruteforce", "description": "Detect ssh brute force", "references": []}, {"name": "crowdsecurity/iptables-scan-multi_ports", "label": "Port Scanner", "description": "Detect tcp port scan", "references": []}, {"name": "crowdsecurity/ssh-slow-bf", "label": "Slow SSH Bruteforce", "description": "Detect slow ssh brute force", "references": []}, {"name": "crowdsecurity/http-probing", "label": "HTTP Scanner", "description": "Detect site scanning/probing from a single ip", "references": []}, {"name": "crowdsecurity/http-path-traversal-probing", "label": "Path Traversal Scanner", "description": "Detect path traversal attempt", "references": []}, {"name": "crowdsecurity/http-bad-user-agent", "label": "Known Bad User-Agent", "description": "Detect bad user-agents", "references": []}], "target_countries": {"DE": 33, "FR": 25, "US": 12, "CA": 8, "JP": 8, "AT": 4, "GB": 4, "AE": 4}, "background_noise_score": 4, "scores": {"overall": {"aggressiveness": 2, "threat": 2, "trust": 1, "anomaly": 2, "total": 2}, "last_day": {"aggressiveness": 0, "threat": 0, "trust": 0, "anomaly": 2, "total": 0}, "last_week": {"aggressiveness": 1, "threat": 2, "trust": 0, "anomaly": 2, "total": 1}, "last_month": {"aggressiveness": 3, "threat": 2, "trust": 0, "anomaly": 2, "total": 2}}, "references": []}`,
|
||||
}
|
||||
|
||||
var fireResponses []string
|
||||
|
||||
// RoundTripFunc .
|
||||
type RoundTripFunc func(req *http.Request) *http.Response
|
||||
|
||||
// RoundTrip .
|
||||
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req), nil
|
||||
}
|
||||
|
||||
// wip
|
||||
func fireHandler(req *http.Request) *http.Response {
|
||||
var err error
|
||||
apiKey := req.Header.Get("x-api-key")
|
||||
if apiKey != validApiKey {
|
||||
log.Warningf("invalid api key: %s", apiKey)
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Body: nil,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
//unmarshal data
|
||||
if fireResponses == nil {
|
||||
page1, err := os.ReadFile("tests/fire-page1.json")
|
||||
if err != nil {
|
||||
panic("can't read file")
|
||||
}
|
||||
page2, err := os.ReadFile("tests/fire-page2.json")
|
||||
if err != nil {
|
||||
panic("can't read file")
|
||||
}
|
||||
fireResponses = []string{string(page1), string(page2)}
|
||||
}
|
||||
//let's assume we have two valid pages.
|
||||
page := 1
|
||||
if req.URL.Query().Get("page") != "" {
|
||||
page, err = strconv.Atoi(req.URL.Query().Get("page"))
|
||||
if err != nil {
|
||||
log.Warningf("no page ?!")
|
||||
return &http.Response{StatusCode: http.StatusInternalServerError}
|
||||
}
|
||||
}
|
||||
|
||||
//how to react if you give a page number that is too big ?
|
||||
if page > len(fireResponses) {
|
||||
log.Warningf(" page too big %d vs %d", page, len(fireResponses))
|
||||
emptyResponse := `{
|
||||
"_links": {
|
||||
"first": {
|
||||
"href": "https://cti.api.crowdsec.net/v1/fire/"
|
||||
},
|
||||
"self": {
|
||||
"href": "https://cti.api.crowdsec.net/v1/fire/?page=3&limit=3"
|
||||
}
|
||||
},
|
||||
"items": []
|
||||
}
|
||||
`
|
||||
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(emptyResponse))}
|
||||
}
|
||||
reader := io.NopCloser(strings.NewReader(fireResponses[page-1]))
|
||||
//we should care about limit too
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
// Send response to be tested
|
||||
Body: reader,
|
||||
Header: make(http.Header),
|
||||
ContentLength: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func smokeHandler(req *http.Request) *http.Response {
|
||||
apiKey := req.Header.Get("x-api-key")
|
||||
if apiKey != validApiKey {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Body: nil,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
requestedIP := strings.Split(req.URL.Path, "/")[3]
|
||||
response, ok := smokeResponses[requestedIP]
|
||||
if !ok {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: io.NopCloser(strings.NewReader(`{"message": "IP address information not found"}`)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
reader := io.NopCloser(strings.NewReader(response))
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
// Send response to be tested
|
||||
Body: reader,
|
||||
Header: make(http.Header),
|
||||
ContentLength: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func rateLimitedHandler(req *http.Request) *http.Response {
|
||||
apiKey := req.Header.Get("x-api-key")
|
||||
if apiKey != validApiKey {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Body: nil,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Body: nil,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
func searchHandler(req *http.Request) *http.Response {
|
||||
apiKey := req.Header.Get("x-api-key")
|
||||
if apiKey != validApiKey {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Body: nil,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
url, _ := url.Parse(req.URL.String())
|
||||
ipsParam := url.Query().Get("ips")
|
||||
if ipsParam == "" {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Body: nil,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
totalIps := 0
|
||||
notFound := 0
|
||||
ips := strings.Split(ipsParam, ",")
|
||||
for _, ip := range ips {
|
||||
_, ok := smokeResponses[ip]
|
||||
if ok {
|
||||
totalIps++
|
||||
} else {
|
||||
notFound++
|
||||
}
|
||||
}
|
||||
response := fmt.Sprintf(`{"total": %d, "not_found": %d, "items": [`, totalIps, notFound)
|
||||
for _, ip := range ips {
|
||||
response += smokeResponses[ip]
|
||||
}
|
||||
response += "]}"
|
||||
reader := io.NopCloser(strings.NewReader(response))
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: reader,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadFireAuth(t *testing.T) {
|
||||
ctiClient := NewCrowdsecCTIClient(WithAPIKey("asdasd"), WithHTTPClient(&http.Client{
|
||||
Transport: RoundTripFunc(fireHandler),
|
||||
}))
|
||||
_, err := ctiClient.Fire(FireParams{})
|
||||
assert.EqualError(t, err, ErrUnauthorized.Error())
|
||||
}
|
||||
|
||||
func TestFireOk(t *testing.T) {
|
||||
cticlient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
|
||||
Transport: RoundTripFunc(fireHandler),
|
||||
}))
|
||||
data, err := cticlient.Fire(FireParams{})
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, len(data.Items), 3)
|
||||
assert.Equal(t, data.Items[0].Ip, "1.2.3.4")
|
||||
//page 1 is the default
|
||||
data, err = cticlient.Fire(FireParams{Page: types.IntPtr(1)})
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, len(data.Items), 3)
|
||||
assert.Equal(t, data.Items[0].Ip, "1.2.3.4")
|
||||
//page 2
|
||||
data, err = cticlient.Fire(FireParams{Page: types.IntPtr(2)})
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, len(data.Items), 3)
|
||||
assert.Equal(t, data.Items[0].Ip, "4.2.3.4")
|
||||
}
|
||||
|
||||
func TestFirePaginator(t *testing.T) {
|
||||
cticlient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
|
||||
Transport: RoundTripFunc(fireHandler),
|
||||
}))
|
||||
paginator := NewFirePaginator(cticlient, FireParams{})
|
||||
items, err := paginator.Next()
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, len(items), 3)
|
||||
assert.Equal(t, items[0].Ip, "1.2.3.4")
|
||||
items, err = paginator.Next()
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, len(items), 3)
|
||||
assert.Equal(t, items[0].Ip, "4.2.3.4")
|
||||
items, err = paginator.Next()
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, len(items), 0)
|
||||
|
||||
}
|
||||
|
||||
func TestBadSmokeAuth(t *testing.T) {
|
||||
ctiClient := NewCrowdsecCTIClient(WithAPIKey("asdasd"), WithHTTPClient(&http.Client{
|
||||
Transport: RoundTripFunc(smokeHandler),
|
||||
}))
|
||||
_, err := ctiClient.GetIPInfo("1.1.1.1")
|
||||
assert.EqualError(t, err, ErrUnauthorized.Error())
|
||||
}
|
||||
|
||||
func TestSmokeInfoValidIP(t *testing.T) {
|
||||
ctiClient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
|
||||
Transport: RoundTripFunc(smokeHandler),
|
||||
}))
|
||||
resp, err := ctiClient.GetIPInfo("1.1.1.1")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get ip info: %s", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "1.1.1.1", resp.Ip)
|
||||
assert.Equal(t, types.StrPtr("1.1.1.0/24"), resp.IpRange)
|
||||
}
|
||||
|
||||
func TestSmokeUnknownIP(t *testing.T) {
|
||||
ctiClient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
|
||||
Transport: RoundTripFunc(smokeHandler),
|
||||
}))
|
||||
resp, err := ctiClient.GetIPInfo("42.42.42.42")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get ip info: %s", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", resp.Ip)
|
||||
}
|
||||
|
||||
func TestRateLimit(t *testing.T) {
|
||||
ctiClient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
|
||||
Transport: RoundTripFunc(rateLimitedHandler),
|
||||
}))
|
||||
_, err := ctiClient.GetIPInfo("1.1.1.1")
|
||||
assert.EqualError(t, err, ErrLimit.Error())
|
||||
}
|
||||
|
||||
func TestSearchIPs(t *testing.T) {
|
||||
ctiClient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
|
||||
Transport: RoundTripFunc(searchHandler),
|
||||
}))
|
||||
resp, err := ctiClient.SearchIPs([]string{"1.1.1.1", "42.42.42.42"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to search ips: %s", err)
|
||||
}
|
||||
assert.Equal(t, 1, resp.Total)
|
||||
assert.Equal(t, 1, resp.NotFound)
|
||||
assert.Equal(t, 1, len(resp.Items))
|
||||
assert.Equal(t, "1.1.1.1", resp.Items[0].Ip)
|
||||
}
|
||||
|
||||
//TODO: fire tests + pagination
|
||||
|
||||
func TestFireInit(t *testing.T) {
|
||||
|
||||
}
|
303
pkg/cticlient/cti_test.go
Normal file
303
pkg/cticlient/cti_test.go
Normal file
|
@ -0,0 +1,303 @@
|
|||
package cticlient
|
||||
|
||||
// import (
|
||||
// "encoding/json"
|
||||
// "net/http"
|
||||
// "net/http/httptest"
|
||||
// "net/url"
|
||||
// "strings"
|
||||
// "testing"
|
||||
// "time"
|
||||
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// )
|
||||
|
||||
// var sampledata = map[string]CTIResponse{
|
||||
// //1.2.3.4 is a known false positive
|
||||
// "1.2.3.4": {
|
||||
// Ip: "1.2.3.4",
|
||||
// Classifications: CTIClassifications{
|
||||
// FalsePositives: []CTIClassification{
|
||||
// {
|
||||
// Name: "example_false_positive",
|
||||
// Label: "Example False Positive",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// //1.2.3.5 is a known bad-guy, and part of FIRE
|
||||
// "1.2.3.5": {
|
||||
// Ip: "1.2.3.5",
|
||||
// Classifications: CTIClassifications{
|
||||
// Classifications: []CTIClassification{
|
||||
// {
|
||||
// Name: "community-blocklist",
|
||||
// Label: "CrowdSec Community Blocklist",
|
||||
// Description: "IP belong to the CrowdSec Community Blocklist",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// //1.2.3.6 is a bad guy (high bg noise), but not in FIRE
|
||||
// "1.2.3.6": {
|
||||
// Ip: "1.2.3.6",
|
||||
// BackgroundNoiseScore: new(int),
|
||||
// Behaviors: []*CTIBehavior{
|
||||
// {Name: "ssh:bruteforce", Label: "SSH Bruteforce", Description: "SSH Bruteforce"},
|
||||
// },
|
||||
// AttackDetails: []*CTIAttackDetails{
|
||||
// {Name: "crowdsecurity/ssh-bf", Label: "Example Attack"},
|
||||
// {Name: "crowdsecurity/ssh-slow-bf", Label: "Example Attack"},
|
||||
// },
|
||||
// },
|
||||
// //1.2.3.7 is a ok guy, but part of a bad range
|
||||
// "1.2.3.7": CTIResponse{},
|
||||
// }
|
||||
|
||||
// func EmptyCTIResponse(ip string) CTIResponse {
|
||||
// return CTIResponse{
|
||||
// IpRangeScore: 0,
|
||||
// Ip: ip,
|
||||
// Location: CTILocationInfo{},
|
||||
// }
|
||||
// }
|
||||
|
||||
// /*
|
||||
// TBD : Simulate correctly quotas exhaustion
|
||||
// */
|
||||
// func setup() (Router *http.ServeMux, serverURL string, teardown func()) {
|
||||
|
||||
// //set static values
|
||||
// *sampledata["1.2.3.6"].BackgroundNoiseScore = 10
|
||||
|
||||
// // mux is the HTTP request multiplexer used with the test server.
|
||||
// Router = http.NewServeMux()
|
||||
// baseURLPath := "/v2"
|
||||
|
||||
// apiHandler := http.NewServeMux()
|
||||
// apiHandler.Handle(baseURLPath+"/", http.StripPrefix(baseURLPath, Router))
|
||||
|
||||
// // server is a test HTTP server used to provide mock API responses.
|
||||
// server := httptest.NewServer(apiHandler)
|
||||
|
||||
// // let's mock the API endpoints
|
||||
// Router.HandleFunc("/smoke/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// //testMethod(t, r, "GET")
|
||||
// if r.Header.Get("X-Api-Key") != "EXAMPLE_API_KEY" {
|
||||
// w.WriteHeader(http.StatusForbidden)
|
||||
// w.Write([]byte(`{"message":"Forbidden"}`))
|
||||
// return
|
||||
// }
|
||||
|
||||
// frags := strings.Split(r.RequestURI, "/")
|
||||
// //[empty] [smoke] [v2] [actual_ip]
|
||||
// if len(frags) != 4 {
|
||||
// w.WriteHeader(http.StatusBadRequest)
|
||||
// w.Write([]byte(`{"message":"Bad Request"}`))
|
||||
// return
|
||||
// }
|
||||
// ip := frags[3]
|
||||
|
||||
// if ip == "" {
|
||||
// //to be fixed to stick w/ real behavior
|
||||
// panic("empty ip")
|
||||
|
||||
// }
|
||||
// // vars := mux.Vars(r)
|
||||
// if v, ok := sampledata[ip]; ok {
|
||||
// data, err := json.Marshal(v)
|
||||
// if err != nil {
|
||||
// panic("unable to marshal")
|
||||
// }
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
// w.Write(data)
|
||||
// return
|
||||
// }
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
// data, err := json.Marshal(EmptyCTIResponse(ip))
|
||||
// if err != nil {
|
||||
// panic("unable to marshal")
|
||||
// }
|
||||
// w.Write(data)
|
||||
// return
|
||||
// })
|
||||
// return Router, server.URL, server.Close
|
||||
// }
|
||||
|
||||
// func TestCTIAuthKO(t *testing.T) {
|
||||
|
||||
// _, urlx, teardown := setup()
|
||||
// apiURL, err := url.Parse(urlx + "/")
|
||||
// if err != nil {
|
||||
// t.Fatalf("parsing api url: %s", apiURL)
|
||||
// }
|
||||
|
||||
// defer teardown()
|
||||
// defer ShutdownCTI()
|
||||
// CTIUrl = urlx
|
||||
// key := "BAD_KEY"
|
||||
// if err := InitCTI(&key, nil, nil); err != nil {
|
||||
// t.Fatalf("InitCTI failed: %s", err)
|
||||
// }
|
||||
|
||||
// ret := IpCTI("1.2.3.4")
|
||||
// assert.Equal(t, false, ret.Ok(), "should be ko")
|
||||
// assert.Equal(t, CTIResponse{}, ret, "auth failed, empty answer")
|
||||
// assert.Equal(t, CTIApiEnabled, false, "auth failed, api disabled")
|
||||
// //auth is disabled, we should always receive empty object
|
||||
// ret = IpCTI("1.2.3.4")
|
||||
// assert.Equal(t, false, ret.Ok(), "should be ko")
|
||||
// assert.Equal(t, CTIResponse{}, ret, "auth failed, empty answer")
|
||||
// }
|
||||
|
||||
// func TestCTINoKey(t *testing.T) {
|
||||
|
||||
// _, urlx, teardown := setup()
|
||||
// apiURL, err := url.Parse(urlx + "/")
|
||||
// if err != nil {
|
||||
// t.Fatalf("parsing api url: %s", apiURL)
|
||||
// }
|
||||
|
||||
// defer teardown()
|
||||
// defer ShutdownCTI()
|
||||
// CTIUrl = urlx
|
||||
// //key := ""
|
||||
// err = InitCTI(nil, nil, nil)
|
||||
// assert.NotEqual(t, err, nil, "InitCTI should fail")
|
||||
// ret := IpCTI("1.2.3.4")
|
||||
// assert.Equal(t, false, ret.Ok(), "should be ko")
|
||||
// assert.Equal(t, CTIResponse{}, ret, "auth failed, empty answer")
|
||||
// assert.Equal(t, CTIApiEnabled, false, "auth failed, api disabled")
|
||||
// }
|
||||
|
||||
// func TestCTIAuthOK(t *testing.T) {
|
||||
|
||||
// _, urlx, teardown := setup()
|
||||
// apiURL, err := url.Parse(urlx + "/")
|
||||
// if err != nil {
|
||||
// t.Fatalf("parsing api url: %s", apiURL)
|
||||
// }
|
||||
|
||||
// defer teardown()
|
||||
// defer ShutdownCTI()
|
||||
|
||||
// CTIUrl = urlx
|
||||
// key := "EXAMPLE_API_KEY"
|
||||
// if err := InitCTI(&key, nil, nil); err != nil {
|
||||
// t.Fatalf("InitCTI failed: %s", err)
|
||||
// }
|
||||
|
||||
// ret := IpCTI("1.2.3.4")
|
||||
// assert.Equal(t, true, ret.Ok(), "should be ok")
|
||||
// assert.Equal(t, "1.2.3.4", ret.Ip, "auth failed, empty answer")
|
||||
// assert.Equal(t, CTIApiEnabled, true, "auth failed, api disabled")
|
||||
// }
|
||||
// func TestCTIKnownFP(t *testing.T) {
|
||||
// _, urlx, teardown := setup()
|
||||
// apiURL, err := url.Parse(urlx + "/")
|
||||
// if err != nil {
|
||||
// t.Fatalf("parsing api url: %s", apiURL)
|
||||
// }
|
||||
|
||||
// defer teardown()
|
||||
// defer ShutdownCTI()
|
||||
|
||||
// CTIUrl = urlx
|
||||
// key := "EXAMPLE_API_KEY"
|
||||
// if err := InitCTI(&key, nil, nil); err != nil {
|
||||
// t.Fatalf("InitCTI failed: %s", err)
|
||||
// }
|
||||
|
||||
// ret := IpCTI("1.2.3.4")
|
||||
// assert.Equal(t, true, ret.Ok(), "should be ok")
|
||||
// assert.Equal(t, "1.2.3.4", ret.Ip, "auth failed, empty answer")
|
||||
// assert.Equal(t, ret.IsFalsePositive(), true, "1.2.3.4 is a known false positive")
|
||||
// }
|
||||
|
||||
// func TestCTIBelongsToFire(t *testing.T) {
|
||||
// _, urlx, teardown := setup()
|
||||
// apiURL, err := url.Parse(urlx + "/")
|
||||
// if err != nil {
|
||||
// t.Fatalf("parsing api url: %s", apiURL)
|
||||
// }
|
||||
|
||||
// defer teardown()
|
||||
// defer ShutdownCTI()
|
||||
|
||||
// CTIUrl = urlx
|
||||
// key := "EXAMPLE_API_KEY"
|
||||
// if err := InitCTI(&key, nil, nil); err != nil {
|
||||
// t.Fatalf("InitCTI failed: %s", err)
|
||||
// }
|
||||
|
||||
// ret := IpCTI("1.2.3.5")
|
||||
// assert.Equal(t, true, ret.Ok(), "should be ok")
|
||||
// assert.Equal(t, "1.2.3.5", ret.Ip, "auth failed, empty answer")
|
||||
// assert.Equal(t, ret.IsPartOfCommunityBlocklist(), true, "1.2.3.5 is a known false positive")
|
||||
// }
|
||||
|
||||
// func TestCTIBehaviors(t *testing.T) {
|
||||
// _, urlx, teardown := setup()
|
||||
// apiURL, err := url.Parse(urlx + "/")
|
||||
// if err != nil {
|
||||
// t.Fatalf("parsing api url: %s", apiURL)
|
||||
// }
|
||||
|
||||
// defer teardown()
|
||||
// defer ShutdownCTI()
|
||||
|
||||
// CTIUrl = urlx
|
||||
// key := "EXAMPLE_API_KEY"
|
||||
// if err := InitCTI(&key, nil, nil); err != nil {
|
||||
// t.Fatalf("InitCTI failed: %s", err)
|
||||
// }
|
||||
|
||||
// ret := IpCTI("1.2.3.6")
|
||||
// assert.Equal(t, true, ret.Ok(), "should be ok")
|
||||
// assert.Equal(t, ret.Ip, "1.2.3.6", "auth failed, empty answer")
|
||||
// //ssh:bruteforce
|
||||
// assert.Equal(t, []string{"ssh:bruteforce"}, ret.GetBehaviors(), "error matching behaviors")
|
||||
// assert.Equal(t, []string{"crowdsecurity/ssh-bf", "crowdsecurity/ssh-slow-bf"}, ret.GetAttackDetails(), "error matching behaviors")
|
||||
// assert.Equal(t, 10, ret.GetBackgroundNoiseScore(), "error matching bg noise")
|
||||
// }
|
||||
|
||||
// func TestCacheFetch(t *testing.T) {
|
||||
// _, urlx, teardown := setup()
|
||||
// apiURL, err := url.Parse(urlx + "/")
|
||||
// if err != nil {
|
||||
// t.Fatalf("parsing api url: %s", apiURL)
|
||||
// }
|
||||
|
||||
// defer teardown()
|
||||
// defer ShutdownCTI()
|
||||
|
||||
// CTIUrl = urlx
|
||||
// key := "EXAMPLE_API_KEY"
|
||||
// ttl := 1 * time.Second
|
||||
// if err := InitCTI(&key, &ttl, nil); err != nil {
|
||||
// t.Fatalf("InitCTI failed: %s", err)
|
||||
// }
|
||||
|
||||
// ret := IpCTI("1.2.3.6")
|
||||
// assert.Equal(t, true, ret.Ok(), "should be ok")
|
||||
// assert.Equal(t, "1.2.3.6", ret.Ip, "initial fetch : bad item")
|
||||
// assert.Equal(t, 1, CTICache.Len(true), "initial fetch : bad cache size")
|
||||
// assert.Equal(t, "1.2.3.6", CTICache.Keys(true)[0].(string), "initial fetch : bad cache keys")
|
||||
// //get it a second time before it expires
|
||||
// ret = IpCTI("1.2.3.6")
|
||||
// assert.Equal(t, true, ret.Ok(), "should be ok")
|
||||
// assert.Equal(t, "1.2.3.6", ret.Ip, "initial fetch : bad item")
|
||||
// assert.Equal(t, 1, CTICache.Len(true), "initial fetch : bad cache size")
|
||||
// assert.Equal(t, "1.2.3.6", CTICache.Keys(true)[0].(string), "initial fetch : bad cache keys")
|
||||
// //let data expire
|
||||
// time.Sleep(1 * time.Second)
|
||||
// assert.Equal(t, 0, CTICache.Len(true), "after ttl : bad cache size")
|
||||
// //fetch again
|
||||
// ret = IpCTI("1.2.3.6")
|
||||
// assert.Equal(t, true, ret.Ok(), "should be ok")
|
||||
// assert.Equal(t, "1.2.3.6", ret.Ip, "second fetch : bad item")
|
||||
// assert.Equal(t, 1, CTICache.Len(true), "second fetch : bad cache size")
|
||||
// assert.Equal(t, "1.2.3.6", CTICache.Keys(true)[0].(string), "initial fetch : bad cache keys")
|
||||
// }
|
||||
|
||||
// //GetMaliciousnessScore
|
59
pkg/cticlient/example/fire.go
Normal file
59
pkg/cticlient/example/fire.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cticlient"
|
||||
)
|
||||
|
||||
func intPtr(i int) *int {
|
||||
return &i
|
||||
}
|
||||
|
||||
func main() {
|
||||
client := cticlient.NewCrowdsecCTIClient(cticlient.WithAPIKey(os.Getenv("CTI_API_KEY")))
|
||||
paginator := cticlient.NewFirePaginator(client, cticlient.FireParams{
|
||||
Limit: intPtr(1000),
|
||||
})
|
||||
|
||||
csvHeader := []string{
|
||||
"value",
|
||||
"reason",
|
||||
"type",
|
||||
"scope",
|
||||
"duration",
|
||||
}
|
||||
csvFile, err := os.Create("fire.csv")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer csvFile.Close()
|
||||
csvWriter := csv.NewWriter(csvFile)
|
||||
allItems := make([][]string, 0)
|
||||
|
||||
for {
|
||||
items, err := paginator.Next()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if items == nil {
|
||||
break
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
banDuration := time.Until(item.Expiration.Time)
|
||||
allItems = append(allItems, []string{
|
||||
item.Ip,
|
||||
"fire-import",
|
||||
"ban",
|
||||
"ip",
|
||||
fmt.Sprintf("%ds", int(banDuration.Seconds())),
|
||||
})
|
||||
}
|
||||
}
|
||||
csvWriter.Write(csvHeader)
|
||||
csvWriter.WriteAll(allItems)
|
||||
}
|
36
pkg/cticlient/pagination.go
Normal file
36
pkg/cticlient/pagination.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package cticlient
|
||||
|
||||
type FirePaginator struct {
|
||||
client *CrowdsecCTIClient
|
||||
params FireParams
|
||||
currentPage int
|
||||
done bool
|
||||
}
|
||||
|
||||
func (p *FirePaginator) Next() ([]FireItem, error) {
|
||||
if p.done {
|
||||
return nil, nil
|
||||
}
|
||||
p.params.Page = &p.currentPage
|
||||
resp, err := p.client.Fire(p.params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.currentPage++
|
||||
if resp.Links.Next == nil {
|
||||
p.done = true
|
||||
}
|
||||
return resp.Items, nil
|
||||
}
|
||||
|
||||
func NewFirePaginator(client *CrowdsecCTIClient, params FireParams) *FirePaginator {
|
||||
startPage := 1
|
||||
if params.Page != nil {
|
||||
startPage = *params.Page
|
||||
}
|
||||
return &FirePaginator{
|
||||
client: client,
|
||||
params: params,
|
||||
currentPage: startPage,
|
||||
}
|
||||
}
|
320
pkg/cticlient/tests/fire-page1.json
Normal file
320
pkg/cticlient/tests/fire-page1.json
Normal file
|
@ -0,0 +1,320 @@
|
|||
{
|
||||
"_links": {
|
||||
"first": {
|
||||
"href": "https://cti.api.crowdsec.net/v2/fire"
|
||||
},
|
||||
"self": {
|
||||
"href": "https://cti.api.crowdsec.net/v2/fire?page=1&limit=3"
|
||||
},
|
||||
"next": {
|
||||
"href": "https://cti.api.crowdsec.net/v2/fire?page=2&limit=3"
|
||||
}
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"ip_range_score": 5,
|
||||
"ip": "1.2.3.4",
|
||||
"ip_range": "1.2.3.0/24",
|
||||
"as_name": "AFFINITY-FTL",
|
||||
"as_num": 3064,
|
||||
"location": {
|
||||
"country": "US",
|
||||
"city": null,
|
||||
"latitude": 37.751,
|
||||
"longitude": -97.822
|
||||
},
|
||||
"reverse_dns": "lsxx.com",
|
||||
"behaviors": [
|
||||
{
|
||||
"name": "http:bruteforce",
|
||||
"label": "HTTP Bruteforce",
|
||||
"description": "IP has been reported for performing a HTTP brute force attack (either generic http probing or applicative related brute force)."
|
||||
},
|
||||
{
|
||||
"name": "http:scan",
|
||||
"label": "HTTP Scan",
|
||||
"description": "IP has been reported for performing actions related to HTTP vulnerability scanning and discovery."
|
||||
}
|
||||
],
|
||||
"history": {
|
||||
"first_seen": "2022-09-18T14:00:00+00:00",
|
||||
"last_seen": "2022-11-26T12:00:00+00:00",
|
||||
"full_age": 77,
|
||||
"days_age": 69
|
||||
},
|
||||
"classifications": {
|
||||
"false_positives": [],
|
||||
"classifications": []
|
||||
},
|
||||
"attack_details": [
|
||||
{
|
||||
"name": "crowdsecurity/http-wordpress_user-enum",
|
||||
"label": "WordPress Bruteforce",
|
||||
"description": "Detect wordpress brute force",
|
||||
"references": []
|
||||
},
|
||||
{
|
||||
"name": "crowdsecurity/http-probing",
|
||||
"label": "HTTP Scanner",
|
||||
"description": "Detect site scanning/probing from a single ip",
|
||||
"references": []
|
||||
},
|
||||
{
|
||||
"name": "crowdsecurity/http-bf-wordpress_bf_xmlrpc",
|
||||
"label": "WordPress XMLRPC Bruteforce",
|
||||
"description": "Detect wordpress brute force on xmlrpc",
|
||||
"references": []
|
||||
},
|
||||
{
|
||||
"name": "crowdsecurity/http-bad-user-agent",
|
||||
"label": "Known Bad User-Agent",
|
||||
"description": "Detect bad user-agents",
|
||||
"references": []
|
||||
}
|
||||
],
|
||||
"state": "validated",
|
||||
"expiration": "2022-12-11T14:15:47.553000",
|
||||
"target_countries": {
|
||||
"US": 43,
|
||||
"DE": 20,
|
||||
"NL": 8,
|
||||
"GB": 7,
|
||||
"FR": 6,
|
||||
"PL": 3,
|
||||
"SG": 2,
|
||||
"CA": 2,
|
||||
"DK": 2,
|
||||
"ZA": 1
|
||||
},
|
||||
"background_noise_score": 5,
|
||||
"scores": {
|
||||
"overall": {
|
||||
"aggressiveness": 5,
|
||||
"threat": 0,
|
||||
"trust": 5,
|
||||
"anomaly": 0,
|
||||
"total": 3
|
||||
},
|
||||
"last_day": {
|
||||
"aggressiveness": 0,
|
||||
"threat": 0,
|
||||
"trust": 0,
|
||||
"anomaly": 0,
|
||||
"total": 0
|
||||
},
|
||||
"last_week": {
|
||||
"aggressiveness": 0,
|
||||
"threat": 0,
|
||||
"trust": 0,
|
||||
"anomaly": 0,
|
||||
"total": 0
|
||||
},
|
||||
"last_month": {
|
||||
"aggressiveness": 0,
|
||||
"threat": 0,
|
||||
"trust": 0,
|
||||
"anomaly": 0,
|
||||
"total": 0
|
||||
}
|
||||
},
|
||||
"references": []
|
||||
},
|
||||
{
|
||||
"ip_range_score": 5,
|
||||
"ip": "2.3.4.5",
|
||||
"ip_range": "2.3.0./16",
|
||||
"as_name": "Linode, LLC",
|
||||
"as_num": 63949,
|
||||
"location": {
|
||||
"country": "DE",
|
||||
"city": "Frankfurt am Main",
|
||||
"latitude": 50.1188,
|
||||
"longitude": 8.6843
|
||||
},
|
||||
"reverse_dns": "172xxent.com",
|
||||
"behaviors": [
|
||||
{
|
||||
"name": "http:exploit",
|
||||
"label": "HTTP Exploit",
|
||||
"description": "IP has been reported for attempting to exploit a vulnerability in a web application."
|
||||
},
|
||||
{
|
||||
"name": "http:scan",
|
||||
"label": "HTTP Scan",
|
||||
"description": "IP has been reported for performing actions related to HTTP vulnerability scanning and discovery."
|
||||
},
|
||||
{
|
||||
"name": "http:crawl",
|
||||
"label": "HTTP Crawl",
|
||||
"description": "IP has been reported for performing aggressive crawling of web applications."
|
||||
}
|
||||
],
|
||||
"history": {
|
||||
"first_seen": "2022-10-15T16:00:00+00:00",
|
||||
"last_seen": "2022-11-18T18:15:00+00:00",
|
||||
"full_age": 50,
|
||||
"days_age": 35
|
||||
},
|
||||
"classifications": {
|
||||
"false_positives": [],
|
||||
"classifications": []
|
||||
},
|
||||
"attack_details": [
|
||||
{
|
||||
"name": "crowdsecurity/jira_cve-2021-26086",
|
||||
"label": "Atlassian Jira CVE-2021-26086",
|
||||
"description": "Detect Atlassian Jira CVE-2021-26086 exploitation attemps",
|
||||
"references": []
|
||||
},
|
||||
{
|
||||
"name": "crowdsecurity/http-probing",
|
||||
"label": "HTTP Scanner",
|
||||
"description": "Detect site scanning/probing from a single ip",
|
||||
"references": []
|
||||
},
|
||||
{
|
||||
"name": "crowdsecurity/CVE-2022-40684",
|
||||
"label": "CVE-2022-40684",
|
||||
"description": "Detect CVE-2022-40684 exploitation attempts (fortinet)",
|
||||
"references": [
|
||||
"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-40684"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "crowdsecurity/http-crawl-non_statics",
|
||||
"label": "HTTP Crawler",
|
||||
"description": "Detect aggressive crawl from single ip",
|
||||
"references": []
|
||||
}
|
||||
],
|
||||
"state": "validated",
|
||||
"expiration": "2022-12-14T16:16:46.507000",
|
||||
"target_countries": {
|
||||
"US": 36,
|
||||
"DE": 19,
|
||||
"FR": 17,
|
||||
"RU": 8,
|
||||
"NL": 5,
|
||||
"GB": 4,
|
||||
"CA": 2,
|
||||
"RO": 2,
|
||||
"IT": 1,
|
||||
"BR": 1
|
||||
},
|
||||
"background_noise_score": 9,
|
||||
"scores": {
|
||||
"overall": {
|
||||
"aggressiveness": 5,
|
||||
"threat": 2,
|
||||
"trust": 5,
|
||||
"anomaly": 0,
|
||||
"total": 4
|
||||
},
|
||||
"last_day": {
|
||||
"aggressiveness": 0,
|
||||
"threat": 0,
|
||||
"trust": 0,
|
||||
"anomaly": 0,
|
||||
"total": 0
|
||||
},
|
||||
"last_week": {
|
||||
"aggressiveness": 0,
|
||||
"threat": 0,
|
||||
"trust": 0,
|
||||
"anomaly": 0,
|
||||
"total": 0
|
||||
},
|
||||
"last_month": {
|
||||
"aggressiveness": 2,
|
||||
"threat": 2,
|
||||
"trust": 0,
|
||||
"anomaly": 0,
|
||||
"total": 1
|
||||
}
|
||||
},
|
||||
"references": []
|
||||
},
|
||||
{
|
||||
"ip_range_score": 0,
|
||||
"ip": "3.2.3.4",
|
||||
"ip_range": "3.2.3.0/24",
|
||||
"as_name": "TOTxxited",
|
||||
"as_num": 23969,
|
||||
"location": {
|
||||
"country": "TH",
|
||||
"city": "Bangkok",
|
||||
"latitude": 13.7366,
|
||||
"longitude": 100.4995
|
||||
},
|
||||
"reverse_dns": "nxxxt.net",
|
||||
"behaviors": [
|
||||
{
|
||||
"name": "smb:bruteforce",
|
||||
"label": "SMB Bruteforce",
|
||||
"description": "IP has been reported for performing brute force on samba services."
|
||||
}
|
||||
],
|
||||
"history": {
|
||||
"first_seen": "2022-11-26T05:15:00+00:00",
|
||||
"last_seen": "2022-11-26T12:00:00+00:00",
|
||||
"full_age": 9,
|
||||
"days_age": 1
|
||||
},
|
||||
"classifications": {
|
||||
"false_positives": [],
|
||||
"classifications": [
|
||||
{
|
||||
"name": "profile:insecure_services",
|
||||
"label": "Dangerous Services Exposed",
|
||||
"description": "IP exposes dangerous services (vnc, telnet, rdp), possibly due to a misconfiguration or because it's a honeypot."
|
||||
}
|
||||
]
|
||||
},
|
||||
"attack_details": [
|
||||
{
|
||||
"name": "crowdsecurity/smb-bf",
|
||||
"label": "Samba Bruteforce",
|
||||
"description": "Detect smb brute force",
|
||||
"references": []
|
||||
}
|
||||
],
|
||||
"state": "validated",
|
||||
"expiration": "2022-12-14T16:18:00.671000",
|
||||
"target_countries": {
|
||||
"GB": 100
|
||||
},
|
||||
"background_noise_score": 5,
|
||||
"scores": {
|
||||
"overall": {
|
||||
"aggressiveness": 2,
|
||||
"threat": 4,
|
||||
"trust": 5,
|
||||
"anomaly": 1,
|
||||
"total": 4
|
||||
},
|
||||
"last_day": {
|
||||
"aggressiveness": 0,
|
||||
"threat": 0,
|
||||
"trust": 0,
|
||||
"anomaly": 1,
|
||||
"total": 0
|
||||
},
|
||||
"last_week": {
|
||||
"aggressiveness": 0,
|
||||
"threat": 0,
|
||||
"trust": 0,
|
||||
"anomaly": 1,
|
||||
"total": 0
|
||||
},
|
||||
"last_month": {
|
||||
"aggressiveness": 2,
|
||||
"threat": 4,
|
||||
"trust": 5,
|
||||
"anomaly": 1,
|
||||
"total": 4
|
||||
}
|
||||
},
|
||||
"references": []
|
||||
}
|
||||
]
|
||||
}
|
315
pkg/cticlient/tests/fire-page2.json
Normal file
315
pkg/cticlient/tests/fire-page2.json
Normal file
|
@ -0,0 +1,315 @@
|
|||
{
|
||||
"_links": {
|
||||
"first": {
|
||||
"href": "https://cti.api.crowdsec.net/v2/fire"
|
||||
},
|
||||
"self": {
|
||||
"href": "https://cti.api.crowdsec.net/v2/fire?page=2&limit=3"
|
||||
},
|
||||
"prev": {
|
||||
"href": "https://cti.api.crowdsec.net/v2/fire?page=1&limit=3"
|
||||
},
|
||||
"next": {
|
||||
"href": "https://cti.api.crowdsec.net/v2/fire?page=3&limit=3"
|
||||
}
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"ip_range_score": 0,
|
||||
"ip": "4.2.3.4",
|
||||
"ip_range": "4.2.0.0/16",
|
||||
"as_name": "Chxxoup",
|
||||
"as_num": 4812,
|
||||
"location": {
|
||||
"country": "CN",
|
||||
"city": null,
|
||||
"latitude": 34.7732,
|
||||
"longitude": 113.722
|
||||
},
|
||||
"reverse_dns": "xxxweqwwe.com.cn",
|
||||
"behaviors": [
|
||||
{
|
||||
"name": "smb:bruteforce",
|
||||
"label": "SMB Bruteforce",
|
||||
"description": "IP has been reported for performing brute force on samba services."
|
||||
},
|
||||
{
|
||||
"name": "windows:bruteforce",
|
||||
"label": "SMB/RDP bruteforce",
|
||||
"description": "IP has been reported for performing brute force on Windows (samba, remote desktop) services."
|
||||
}
|
||||
],
|
||||
"history": {
|
||||
"first_seen": "2022-11-25T04:15:00+00:00",
|
||||
"last_seen": "2022-11-25T13:30:00+00:00",
|
||||
"full_age": 9,
|
||||
"days_age": 1
|
||||
},
|
||||
"classifications": {
|
||||
"false_positives": [],
|
||||
"classifications": [
|
||||
{
|
||||
"name": "proxy:vpn",
|
||||
"label": "VPN",
|
||||
"description": "IP exposes a VPN service or is being flagged as one."
|
||||
}
|
||||
]
|
||||
},
|
||||
"attack_details": [
|
||||
{
|
||||
"name": "crowdsecurity/smb-bf",
|
||||
"label": "Samba Bruteforce",
|
||||
"description": "Detect smb brute force",
|
||||
"references": []
|
||||
},
|
||||
{
|
||||
"name": "crowdsecurity/windows-bf",
|
||||
"label": "SMB/RDP brute force",
|
||||
"description": "Detect samba/remote-desktop user brute force",
|
||||
"references": []
|
||||
}
|
||||
],
|
||||
"state": "validated",
|
||||
"expiration": "2022-12-14T16:17:24.865000",
|
||||
"target_countries": {
|
||||
"FR": 100
|
||||
},
|
||||
"background_noise_score": 6,
|
||||
"scores": {
|
||||
"overall": {
|
||||
"aggressiveness": 2,
|
||||
"threat": 4,
|
||||
"trust": 5,
|
||||
"anomaly": 1,
|
||||
"total": 4
|
||||
},
|
||||
"last_day": {
|
||||
"aggressiveness": 0,
|
||||
"threat": 0,
|
||||
"trust": 0,
|
||||
"anomaly": 1,
|
||||
"total": 0
|
||||
},
|
||||
"last_week": {
|
||||
"aggressiveness": 0,
|
||||
"threat": 0,
|
||||
"trust": 0,
|
||||
"anomaly": 1,
|
||||
"total": 0
|
||||
},
|
||||
"last_month": {
|
||||
"aggressiveness": 2,
|
||||
"threat": 4,
|
||||
"trust": 5,
|
||||
"anomaly": 1,
|
||||
"total": 4
|
||||
}
|
||||
},
|
||||
"references": []
|
||||
},
|
||||
{
|
||||
"ip_range_score": 2,
|
||||
"ip": "5.2.3.4",
|
||||
"ip_range": "5.2.3.0/24",
|
||||
"as_name": "Turxxri A.s.",
|
||||
"as_num": 16135,
|
||||
"location": {
|
||||
"country": "TR",
|
||||
"city": "Istanbul",
|
||||
"latitude": 41.0551,
|
||||
"longitude": 28.9347
|
||||
},
|
||||
"reverse_dns": null,
|
||||
"behaviors": [
|
||||
{
|
||||
"name": "ssh:bruteforce",
|
||||
"label": "SSH Bruteforce",
|
||||
"description": "IP has been reported for performing brute force on ssh services."
|
||||
},
|
||||
{
|
||||
"name": "tcp:scan",
|
||||
"label": "TCP Scan",
|
||||
"description": "IP has been reported for performing TCP port scanning."
|
||||
}
|
||||
],
|
||||
"history": {
|
||||
"first_seen": "2022-08-26T02:00:00+00:00",
|
||||
"last_seen": "2022-11-18T09:45:00+00:00",
|
||||
"full_age": 100,
|
||||
"days_age": 85
|
||||
},
|
||||
"classifications": {
|
||||
"false_positives": [],
|
||||
"classifications": [
|
||||
{
|
||||
"name": "profile:insecure_services",
|
||||
"label": "Dangerous Services Exposed",
|
||||
"description": "IP exposes dangerous services (vnc, telnet, rdp), possibly due to a misconfiguration or because it's a honeypot."
|
||||
},
|
||||
{
|
||||
"name": "profile:many_services",
|
||||
"label": "Many Services Exposed",
|
||||
"description": "IP exposes many open port, possibly due to a misconfiguration or because it's a honeypot."
|
||||
}
|
||||
]
|
||||
},
|
||||
"attack_details": [
|
||||
{
|
||||
"name": "crowdsecurity/ssh-slow-bf",
|
||||
"label": "Slow SSH Bruteforce",
|
||||
"description": "Detect slow ssh brute force",
|
||||
"references": []
|
||||
},
|
||||
{
|
||||
"name": "crowdsecurity/ssh-bf",
|
||||
"label": "SSH Bruteforce",
|
||||
"description": "Detect ssh brute force",
|
||||
"references": []
|
||||
},
|
||||
{
|
||||
"name": "crowdsecurity/iptables-scan-multi_ports",
|
||||
"label": "Port Scanner",
|
||||
"description": "Detect tcp port scan",
|
||||
"references": []
|
||||
}
|
||||
],
|
||||
"state": "validated",
|
||||
"expiration": "2022-12-12T15:16:33.246000",
|
||||
"target_countries": {
|
||||
"FR": 21,
|
||||
"HK": 19,
|
||||
"US": 19,
|
||||
"DE": 11,
|
||||
"AU": 7,
|
||||
"GB": 4,
|
||||
"RU": 4,
|
||||
"BR": 4,
|
||||
"CA": 4,
|
||||
"VE": 2
|
||||
},
|
||||
"background_noise_score": 4,
|
||||
"scores": {
|
||||
"overall": {
|
||||
"aggressiveness": 2,
|
||||
"threat": 3,
|
||||
"trust": 2,
|
||||
"anomaly": 3,
|
||||
"total": 3
|
||||
},
|
||||
"last_day": {
|
||||
"aggressiveness": 0,
|
||||
"threat": 0,
|
||||
"trust": 0,
|
||||
"anomaly": 3,
|
||||
"total": 0
|
||||
},
|
||||
"last_week": {
|
||||
"aggressiveness": 0,
|
||||
"threat": 0,
|
||||
"trust": 0,
|
||||
"anomaly": 3,
|
||||
"total": 0
|
||||
},
|
||||
"last_month": {
|
||||
"aggressiveness": 1,
|
||||
"threat": 3,
|
||||
"trust": 1,
|
||||
"anomaly": 3,
|
||||
"total": 2
|
||||
}
|
||||
},
|
||||
"references": []
|
||||
},
|
||||
{
|
||||
"ip_range_score": 5,
|
||||
"ip": "6.2.3.4",
|
||||
"ip_range": "6.2.0.0/17",
|
||||
"as_name": "SMILESERV",
|
||||
"as_num": 38700,
|
||||
"location": {
|
||||
"country": "KR",
|
||||
"city": null,
|
||||
"latitude": 37.5112,
|
||||
"longitude": 126.9741
|
||||
},
|
||||
"reverse_dns": null,
|
||||
"behaviors": [
|
||||
{
|
||||
"name": "ssh:bruteforce",
|
||||
"label": "SSH Bruteforce",
|
||||
"description": "IP has been reported for performing brute force on ssh services."
|
||||
}
|
||||
],
|
||||
"history": {
|
||||
"first_seen": "2022-09-20T15:30:00+00:00",
|
||||
"last_seen": "2022-11-25T11:30:00+00:00",
|
||||
"full_age": 74,
|
||||
"days_age": 66
|
||||
},
|
||||
"classifications": {
|
||||
"false_positives": [],
|
||||
"classifications": []
|
||||
},
|
||||
"attack_details": [
|
||||
{
|
||||
"name": "crowdsecurity/ssh-slow-bf",
|
||||
"label": "Slow SSH Bruteforce",
|
||||
"description": "Detect slow ssh brute force",
|
||||
"references": []
|
||||
},
|
||||
{
|
||||
"name": "crowdsecurity/ssh-bf",
|
||||
"label": "SSH Bruteforce",
|
||||
"description": "Detect ssh brute force",
|
||||
"references": []
|
||||
}
|
||||
],
|
||||
"state": "validated",
|
||||
"expiration": "2022-12-14T16:19:30.654000",
|
||||
"target_countries": {
|
||||
"FR": 32,
|
||||
"US": 21,
|
||||
"DE": 17,
|
||||
"NL": 5,
|
||||
"FI": 5,
|
||||
"RU": 3,
|
||||
"GB": 3,
|
||||
"SI": 2,
|
||||
"RO": 2,
|
||||
"HK": 2
|
||||
},
|
||||
"background_noise_score": 4,
|
||||
"scores": {
|
||||
"overall": {
|
||||
"aggressiveness": 4,
|
||||
"threat": 4,
|
||||
"trust": 5,
|
||||
"anomaly": 1,
|
||||
"total": 4
|
||||
},
|
||||
"last_day": {
|
||||
"aggressiveness": 0,
|
||||
"threat": 0,
|
||||
"trust": 0,
|
||||
"anomaly": 1,
|
||||
"total": 0
|
||||
},
|
||||
"last_week": {
|
||||
"aggressiveness": 0,
|
||||
"threat": 0,
|
||||
"trust": 0,
|
||||
"anomaly": 1,
|
||||
"total": 0
|
||||
},
|
||||
"last_month": {
|
||||
"aggressiveness": 3,
|
||||
"threat": 4,
|
||||
"trust": 1,
|
||||
"anomaly": 1,
|
||||
"total": 3
|
||||
}
|
||||
},
|
||||
"references": []
|
||||
}
|
||||
]
|
||||
}
|
295
pkg/cticlient/types.go
Normal file
295
pkg/cticlient/types.go
Normal file
|
@ -0,0 +1,295 @@
|
|||
package cticlient
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type CTIScores struct {
|
||||
Overall CTIScore `json:"overall"`
|
||||
LastDay CTIScore `json:"last_day"`
|
||||
LastWeek CTIScore `json:"last_week"`
|
||||
LastMonth CTIScore `json:"last_month"`
|
||||
}
|
||||
|
||||
type CTIScore struct {
|
||||
Aggressiveness int `json:"aggressiveness"`
|
||||
Threat int `json:"threat"`
|
||||
Trust int `json:"trust"`
|
||||
Anomaly int `json:"anomaly"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type CTIAttackDetails struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Description string `json:"description"`
|
||||
References []string `json:"references"`
|
||||
}
|
||||
|
||||
type CTIClassifications struct {
|
||||
FalsePositives []CTIClassification `json:"false_positives"`
|
||||
Classifications []CTIClassification `json:"classifications"`
|
||||
}
|
||||
|
||||
type CTIClassification struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
type CTIHistory struct {
|
||||
FirstSeen *string `json:"first_seen"`
|
||||
LastSeen *string `json:"last_seen"`
|
||||
FullAge int `json:"full_age"`
|
||||
DaysAge int `json:"days_age"`
|
||||
}
|
||||
|
||||
type CTIBehavior struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
type CTILocationInfo struct {
|
||||
Country *string `json:"country"`
|
||||
City *string `json:"city"`
|
||||
Latitude *float64 `json:"latitude"`
|
||||
Longitude *float64 `json:"longitude"`
|
||||
}
|
||||
|
||||
type CTIReferences struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type SmokeItem struct {
|
||||
IpRangeScore int `json:"ip_range_score"`
|
||||
Ip string `json:"ip"`
|
||||
IpRange *string `json:"ip_range"`
|
||||
AsName *string `json:"as_name"`
|
||||
AsNum *int `json:"as_num"`
|
||||
Location CTILocationInfo `json:"location"`
|
||||
ReverseDNS *string `json:"reverse_dns"`
|
||||
Behaviors []*CTIBehavior `json:"behaviors"`
|
||||
History CTIHistory `json:"history"`
|
||||
Classifications CTIClassifications `json:"classifications"`
|
||||
AttackDetails []*CTIAttackDetails `json:"attack_details"`
|
||||
TargetCountries map[string]int `json:"target_countries"`
|
||||
BackgroundNoiseScore *int `json:"background_noise_score"`
|
||||
Scores CTIScores `json:"scores"`
|
||||
References []CTIReferences `json:"references"`
|
||||
IsOk bool `json:"-"`
|
||||
}
|
||||
|
||||
type SearchIPResponse struct {
|
||||
Total int `json:"total"`
|
||||
NotFound int `json:"not_found"`
|
||||
Items []SmokeItem `json:"items"`
|
||||
}
|
||||
|
||||
type CustomTime struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
func (ct *CustomTime) UnmarshalJSON(b []byte) error {
|
||||
if string(b) == "null" {
|
||||
return nil
|
||||
}
|
||||
|
||||
t, err := time.Parse(`"2006-01-02T15:04:05.999999999"`, string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ct.Time = t
|
||||
return nil
|
||||
}
|
||||
|
||||
type FireItem struct {
|
||||
IpRangeScore int `json:"ip_range_score"`
|
||||
Ip string `json:"ip"`
|
||||
IpRange *string `json:"ip_range"`
|
||||
AsName *string `json:"as_name"`
|
||||
AsNum *int `json:"as_num"`
|
||||
Location CTILocationInfo `json:"location"`
|
||||
ReverseDNS *string `json:"reverse_dns"`
|
||||
Behaviors []*CTIBehavior `json:"behaviors"`
|
||||
History CTIHistory `json:"history"`
|
||||
Classifications CTIClassifications `json:"classifications"`
|
||||
AttackDetails []*CTIAttackDetails `json:"attack_details"`
|
||||
TargetCountries map[string]int `json:"target_countries"`
|
||||
BackgroundNoiseScore *int `json:"background_noise_score"`
|
||||
Scores CTIScores `json:"scores"`
|
||||
References []CTIReferences `json:"references"`
|
||||
Status string `json:"status"`
|
||||
Expiration CustomTime `json:"expiration"`
|
||||
}
|
||||
|
||||
type FireParams struct {
|
||||
Since *string `json:"since"`
|
||||
Page *int `json:"page"`
|
||||
Limit *int `json:"limit"`
|
||||
}
|
||||
|
||||
type Href struct {
|
||||
Href string `json:"href"`
|
||||
}
|
||||
|
||||
type Links struct {
|
||||
First *Href `json:"first"`
|
||||
Self *Href `json:"self"`
|
||||
Prev *Href `json:"prev"`
|
||||
Next *Href `json:"next"`
|
||||
}
|
||||
|
||||
type FireResponse struct {
|
||||
Links Links `json:"_links"`
|
||||
Items []FireItem `json:"items"`
|
||||
}
|
||||
|
||||
func (c *SmokeItem) GetAttackDetails() []string {
|
||||
var ret []string = make([]string, 0)
|
||||
|
||||
if c.AttackDetails != nil {
|
||||
for _, b := range c.AttackDetails {
|
||||
ret = append(ret, b.Name)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *SmokeItem) GetBehaviors() []string {
|
||||
var ret []string = make([]string, 0)
|
||||
|
||||
if c.Behaviors != nil {
|
||||
for _, b := range c.Behaviors {
|
||||
ret = append(ret, b.Name)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Provide the likelihood of the IP being bad
|
||||
func (c *SmokeItem) GetMaliciousnessScore() float32 {
|
||||
if c.IsPartOfCommunityBlocklist() {
|
||||
return 1.0
|
||||
}
|
||||
if c.Scores.LastDay.Total > 0 {
|
||||
return float32(c.Scores.LastDay.Total) / 10.0
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
func (c *SmokeItem) IsPartOfCommunityBlocklist() bool {
|
||||
if c.Classifications.Classifications != nil {
|
||||
for _, v := range c.Classifications.Classifications {
|
||||
if v.Name == "community-blocklist" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *SmokeItem) GetBackgroundNoiseScore() int {
|
||||
if c.BackgroundNoiseScore != nil {
|
||||
return *c.BackgroundNoiseScore
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *SmokeItem) GetFalsePositives() []string {
|
||||
var ret []string = make([]string, 0)
|
||||
|
||||
if c.Classifications.FalsePositives != nil {
|
||||
for _, b := range c.Classifications.FalsePositives {
|
||||
ret = append(ret, b.Name)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *SmokeItem) IsFalsePositive() bool {
|
||||
|
||||
if c.Classifications.FalsePositives != nil {
|
||||
if len(c.Classifications.FalsePositives) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *FireItem) GetAttackDetails() []string {
|
||||
var ret []string = make([]string, 0)
|
||||
|
||||
if c.AttackDetails != nil {
|
||||
for _, b := range c.AttackDetails {
|
||||
ret = append(ret, b.Name)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *FireItem) GetBehaviors() []string {
|
||||
var ret []string = make([]string, 0)
|
||||
|
||||
if c.Behaviors != nil {
|
||||
for _, b := range c.Behaviors {
|
||||
ret = append(ret, b.Name)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Provide the likelihood of the IP being bad
|
||||
func (c *FireItem) GetMaliciousnessScore() float32 {
|
||||
if c.IsPartOfCommunityBlocklist() {
|
||||
return 1.0
|
||||
}
|
||||
if c.Scores.LastDay.Total > 0 {
|
||||
return float32(c.Scores.LastDay.Total) / 10.0
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
func (c *FireItem) IsPartOfCommunityBlocklist() bool {
|
||||
if c.Classifications.Classifications != nil {
|
||||
for _, v := range c.Classifications.Classifications {
|
||||
if v.Name == "community-blocklist" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *FireItem) GetBackgroundNoiseScore() int {
|
||||
if c.BackgroundNoiseScore != nil {
|
||||
return *c.BackgroundNoiseScore
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *FireItem) GetFalsePositives() []string {
|
||||
var ret []string = make([]string, 0)
|
||||
|
||||
if c.Classifications.FalsePositives != nil {
|
||||
for _, b := range c.Classifications.FalsePositives {
|
||||
ret = append(ret, b.Name)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *FireItem) IsFalsePositive() bool {
|
||||
|
||||
if c.Classifications.FalsePositives != nil {
|
||||
if len(c.Classifications.FalsePositives) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
114
pkg/cticlient/types_test.go
Normal file
114
pkg/cticlient/types_test.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package cticlient
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
)
|
||||
|
||||
//func (c *SmokeItem) GetAttackDetails() []string {
|
||||
|
||||
func getSampleSmokeItem() SmokeItem {
|
||||
lat := 48.8566
|
||||
long := 2.3522
|
||||
emptyItem := SmokeItem{
|
||||
IpRangeScore: 2.0,
|
||||
Ip: "1.2.3.4",
|
||||
IpRange: types.StrPtr("1.2.3.0/24"),
|
||||
AsName: types.StrPtr("AS1234"),
|
||||
AsNum: types.IntPtr(1234),
|
||||
Location: CTILocationInfo{
|
||||
Country: types.StrPtr("FR"),
|
||||
City: types.StrPtr("Paris"),
|
||||
Latitude: &lat,
|
||||
Longitude: &long,
|
||||
},
|
||||
ReverseDNS: types.StrPtr("foo.bar.com"),
|
||||
Behaviors: []*CTIBehavior{
|
||||
{
|
||||
Name: "ssh:bruteforce",
|
||||
Label: "SSH Bruteforce",
|
||||
Description: "IP has been reported for performing brute force on ssh services.",
|
||||
},
|
||||
},
|
||||
History: CTIHistory{
|
||||
FirstSeen: types.StrPtr("2022-12-05T17:45:00+00:00"),
|
||||
LastSeen: types.StrPtr("2022-12-06T19:15:00+00:00"),
|
||||
FullAge: 3,
|
||||
DaysAge: 1,
|
||||
},
|
||||
Classifications: CTIClassifications{
|
||||
FalsePositives: []CTIClassification{},
|
||||
Classifications: []CTIClassification{},
|
||||
},
|
||||
AttackDetails: []*CTIAttackDetails{
|
||||
{
|
||||
Name: "ssh:bruteforce",
|
||||
Label: "SSH Bruteforce",
|
||||
Description: "Detect ssh brute force",
|
||||
References: []string{},
|
||||
},
|
||||
},
|
||||
TargetCountries: map[string]int{
|
||||
"HK": 71,
|
||||
"GB": 14,
|
||||
"US": 14,
|
||||
},
|
||||
BackgroundNoiseScore: types.IntPtr(3),
|
||||
Scores: CTIScores{
|
||||
Overall: CTIScore{
|
||||
Aggressiveness: 2,
|
||||
Threat: 1,
|
||||
Trust: 1,
|
||||
Anomaly: 0,
|
||||
Total: 1,
|
||||
},
|
||||
LastDay: CTIScore{
|
||||
Aggressiveness: 2,
|
||||
Threat: 1,
|
||||
Trust: 1,
|
||||
Anomaly: 0,
|
||||
Total: 1,
|
||||
},
|
||||
LastWeek: CTIScore{
|
||||
Aggressiveness: 2,
|
||||
Threat: 1,
|
||||
Trust: 1,
|
||||
Anomaly: 0,
|
||||
Total: 1,
|
||||
},
|
||||
LastMonth: CTIScore{
|
||||
Aggressiveness: 2,
|
||||
Threat: 1,
|
||||
Trust: 1,
|
||||
Anomaly: 0,
|
||||
Total: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
return emptyItem
|
||||
}
|
||||
|
||||
func TestBasicSmokeItem(t *testing.T) {
|
||||
item := getSampleSmokeItem()
|
||||
assert.Equal(t, item.GetAttackDetails(), []string{"ssh:bruteforce"})
|
||||
assert.Equal(t, item.GetBehaviors(), []string{"ssh:bruteforce"})
|
||||
assert.Equal(t, item.GetMaliciousnessScore(), float32(0.1))
|
||||
assert.Equal(t, item.IsPartOfCommunityBlocklist(), false)
|
||||
assert.Equal(t, item.GetBackgroundNoiseScore(), int(3))
|
||||
assert.Equal(t, item.GetFalsePositives(), []string{})
|
||||
assert.Equal(t, item.IsFalsePositive(), false)
|
||||
}
|
||||
|
||||
func TestEmptySmokeItem(t *testing.T) {
|
||||
item := SmokeItem{}
|
||||
assert.Equal(t, item.GetAttackDetails(), []string{})
|
||||
assert.Equal(t, item.GetBehaviors(), []string{})
|
||||
assert.Equal(t, item.GetMaliciousnessScore(), float32(0.0))
|
||||
assert.Equal(t, item.IsPartOfCommunityBlocklist(), false)
|
||||
assert.Equal(t, item.GetBackgroundNoiseScore(), int(0))
|
||||
assert.Equal(t, item.GetFalsePositives(), []string{})
|
||||
assert.Equal(t, item.IsFalsePositive(), false)
|
||||
}
|
135
pkg/exprhelpers/crowdsec_cti.go
Normal file
135
pkg/exprhelpers/crowdsec_cti.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package exprhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/bluele/gcache"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cticlient"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var CTIUrl = "https://cti.api.crowdsec.net"
|
||||
var CTIUrlSuffix = "/v2/smoke/"
|
||||
var CTIApiKey = ""
|
||||
|
||||
// this is set for non-recoverable errors, such as 403 when querying API or empty API key
|
||||
var CTIApiEnabled = true
|
||||
|
||||
// when hitting quotas or auth errors, we temporarily disable the API
|
||||
var CTIBackOffUntil time.Time
|
||||
var CTIBackOffDuration time.Duration = 5 * time.Minute
|
||||
|
||||
var ctiClient *cticlient.CrowdsecCTIClient
|
||||
|
||||
func InitCrowdsecCTI(Key *string, TTL *time.Duration, Size *int, LogLevel *log.Level) error {
|
||||
if Key != nil {
|
||||
CTIApiKey = *Key
|
||||
} else {
|
||||
CTIApiEnabled = false
|
||||
return fmt.Errorf("CTI API key not set, CTI will not be available")
|
||||
}
|
||||
if Size == nil {
|
||||
Size = new(int)
|
||||
*Size = 1000
|
||||
}
|
||||
if TTL == nil {
|
||||
TTL = new(time.Duration)
|
||||
*TTL = 5 * time.Minute
|
||||
}
|
||||
//dedicated logger
|
||||
clog := log.New()
|
||||
if err := types.ConfigureLogger(clog); err != nil {
|
||||
return errors.Wrap(err, "while configuring datasource logger")
|
||||
}
|
||||
if LogLevel != nil {
|
||||
clog.SetLevel(*LogLevel)
|
||||
}
|
||||
customLog := log.Fields{
|
||||
"type": "crowdsec-cti",
|
||||
}
|
||||
subLogger := clog.WithFields(customLog)
|
||||
CrowdsecCTIInitCache(*Size, *TTL)
|
||||
ctiClient = cticlient.NewCrowdsecCTIClient(cticlient.WithAPIKey(CTIApiKey), cticlient.WithLogger(subLogger))
|
||||
return nil
|
||||
}
|
||||
|
||||
func ShutdownCrowdsecCTI() {
|
||||
if CTICache != nil {
|
||||
CTICache.Purge()
|
||||
}
|
||||
CTIApiKey = ""
|
||||
CTIApiEnabled = true
|
||||
}
|
||||
|
||||
// Cache for responses
|
||||
var CTICache gcache.Cache
|
||||
var CacheExpiration time.Duration
|
||||
|
||||
func CrowdsecCTIInitCache(size int, ttl time.Duration) {
|
||||
CTICache = gcache.New(size).LRU().Build()
|
||||
CacheExpiration = ttl
|
||||
}
|
||||
|
||||
func CrowdsecCTI(ip string) (*cticlient.SmokeItem, error) {
|
||||
if !CTIApiEnabled {
|
||||
ctiClient.Logger.Warningf("Crowdsec CTI API is disabled, please check your configuration")
|
||||
return &cticlient.SmokeItem{}, cticlient.ErrDisabled
|
||||
}
|
||||
|
||||
if CTIApiKey == "" {
|
||||
ctiClient.Logger.Warningf("CrowdsecCTI : no key provided, skipping")
|
||||
return &cticlient.SmokeItem{}, cticlient.ErrDisabled
|
||||
}
|
||||
|
||||
if ctiClient == nil {
|
||||
ctiClient.Logger.Warningf("CrowdsecCTI: no client, skipping")
|
||||
return &cticlient.SmokeItem{}, cticlient.ErrDisabled
|
||||
}
|
||||
|
||||
if val, err := CTICache.Get(ip); err == nil && val != nil {
|
||||
ctiClient.Logger.Debugf("cti cache fetch for %s", ip)
|
||||
ret, ok := val.(*cticlient.SmokeItem)
|
||||
if !ok {
|
||||
ctiClient.Logger.Warningf("CrowdsecCTI: invalid type in cache, removing")
|
||||
CTICache.Remove(ip)
|
||||
} else {
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
if !CTIBackOffUntil.IsZero() && time.Now().Before(CTIBackOffUntil) {
|
||||
//ctiClient.Logger.Warningf("Crowdsec CTI client is in backoff mode, ending in %s", time.Until(CTIBackOffUntil))
|
||||
return &cticlient.SmokeItem{}, cticlient.ErrLimit
|
||||
}
|
||||
|
||||
ctiClient.Logger.Infof("cti call for %s", ip)
|
||||
before := time.Now()
|
||||
ctiResp, err := ctiClient.GetIPInfo(ip)
|
||||
ctiClient.Logger.Debugf("request for %s took %v", ip, time.Since(before))
|
||||
if err != nil {
|
||||
if err == cticlient.ErrUnauthorized {
|
||||
CTIApiEnabled = false
|
||||
ctiClient.Logger.Errorf("Invalid API key provided, disabling CTI API")
|
||||
return &cticlient.SmokeItem{}, cticlient.ErrUnauthorized
|
||||
} else if err == cticlient.ErrLimit {
|
||||
CTIBackOffUntil = time.Now().Add(CTIBackOffDuration)
|
||||
ctiClient.Logger.Errorf("CTI API is throttled, will try again in %s", CTIBackOffDuration)
|
||||
return &cticlient.SmokeItem{}, cticlient.ErrLimit
|
||||
} else {
|
||||
ctiClient.Logger.Warnf("CTI API error : %s", err)
|
||||
return &cticlient.SmokeItem{}, fmt.Errorf("unexpected error : %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := CTICache.SetWithExpire(ip, ctiResp, CacheExpiration); err != nil {
|
||||
ctiClient.Logger.Warningf("IpCTI : error while caching CTI : %s", err)
|
||||
return &cticlient.SmokeItem{}, cticlient.ErrUnknown
|
||||
}
|
||||
|
||||
ctiClient.Logger.Tracef("CTI response : %v", *ctiResp)
|
||||
|
||||
return ctiResp, nil
|
||||
}
|
181
pkg/exprhelpers/crowdsec_cti_test.go
Normal file
181
pkg/exprhelpers/crowdsec_cti_test.go
Normal file
|
@ -0,0 +1,181 @@
|
|||
package exprhelpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cticlient"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var sampledata = map[string]cticlient.SmokeItem{
|
||||
//1.2.3.4 is a known false positive
|
||||
"1.2.3.4": {
|
||||
Ip: "1.2.3.4",
|
||||
Classifications: cticlient.CTIClassifications{
|
||||
FalsePositives: []cticlient.CTIClassification{
|
||||
{
|
||||
Name: "example_false_positive",
|
||||
Label: "Example False Positive",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
//1.2.3.5 is a known bad-guy, and part of FIRE
|
||||
"1.2.3.5": {
|
||||
Ip: "1.2.3.5",
|
||||
Classifications: cticlient.CTIClassifications{
|
||||
Classifications: []cticlient.CTIClassification{
|
||||
{
|
||||
Name: "community-blocklist",
|
||||
Label: "CrowdSec Community Blocklist",
|
||||
Description: "IP belong to the CrowdSec Community Blocklist",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
//1.2.3.6 is a bad guy (high bg noise), but not in FIRE
|
||||
"1.2.3.6": {
|
||||
Ip: "1.2.3.6",
|
||||
BackgroundNoiseScore: new(int),
|
||||
Behaviors: []*cticlient.CTIBehavior{
|
||||
{Name: "ssh:bruteforce", Label: "SSH Bruteforce", Description: "SSH Bruteforce"},
|
||||
},
|
||||
AttackDetails: []*cticlient.CTIAttackDetails{
|
||||
{Name: "crowdsecurity/ssh-bf", Label: "Example Attack"},
|
||||
{Name: "crowdsecurity/ssh-slow-bf", Label: "Example Attack"},
|
||||
},
|
||||
},
|
||||
//1.2.3.7 is a ok guy, but part of a bad range
|
||||
"1.2.3.7": cticlient.SmokeItem{},
|
||||
}
|
||||
|
||||
const validApiKey = "my-api-key"
|
||||
|
||||
type RoundTripFunc func(req *http.Request) *http.Response
|
||||
|
||||
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req), nil
|
||||
}
|
||||
|
||||
func smokeHandler(req *http.Request) *http.Response {
|
||||
apiKey := req.Header.Get("x-api-key")
|
||||
if apiKey != validApiKey {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Body: nil,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
requestedIP := strings.Split(req.URL.Path, "/")[3]
|
||||
sample, ok := sampledata[requestedIP]
|
||||
if !ok {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: nil,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
body, err := json.Marshal(sample)
|
||||
if err != nil {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Body: nil,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
reader := io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
// Send response to be tested
|
||||
Body: reader,
|
||||
Header: make(http.Header),
|
||||
ContentLength: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidAuth(t *testing.T) {
|
||||
defer ShutdownCrowdsecCTI()
|
||||
if err := InitCrowdsecCTI(types.StrPtr("asdasd"), nil, nil, nil); err != nil {
|
||||
t.Fatalf("failed to init CTI : %s", err)
|
||||
}
|
||||
//Replace the client created by InitCrowdsecCTI with one that uses a custom transport
|
||||
ctiClient = cticlient.NewCrowdsecCTIClient(cticlient.WithAPIKey("asdasd"), cticlient.WithHTTPClient(&http.Client{
|
||||
Transport: RoundTripFunc(smokeHandler),
|
||||
}))
|
||||
|
||||
item, err := CrowdsecCTI("1.2.3.4")
|
||||
assert.Equal(t, item, &cticlient.SmokeItem{})
|
||||
assert.Equal(t, CTIApiEnabled, false)
|
||||
assert.Equal(t, err, cticlient.ErrUnauthorized)
|
||||
|
||||
//CTI is now disabled, all requests should return empty
|
||||
ctiClient = cticlient.NewCrowdsecCTIClient(cticlient.WithAPIKey(validApiKey), cticlient.WithHTTPClient(&http.Client{
|
||||
Transport: RoundTripFunc(smokeHandler),
|
||||
}))
|
||||
|
||||
item, err = CrowdsecCTI("1.2.3.4")
|
||||
assert.Equal(t, item, &cticlient.SmokeItem{})
|
||||
assert.Equal(t, CTIApiEnabled, false)
|
||||
assert.Equal(t, err, cticlient.ErrDisabled)
|
||||
}
|
||||
|
||||
func TestNoKey(t *testing.T) {
|
||||
defer ShutdownCrowdsecCTI()
|
||||
err := InitCrowdsecCTI(nil, nil, nil, nil)
|
||||
assert.ErrorContains(t, err, "CTI API key not set")
|
||||
//Replace the client created by InitCrowdsecCTI with one that uses a custom transport
|
||||
ctiClient = cticlient.NewCrowdsecCTIClient(cticlient.WithAPIKey("asdasd"), cticlient.WithHTTPClient(&http.Client{
|
||||
Transport: RoundTripFunc(smokeHandler),
|
||||
}))
|
||||
|
||||
item, err := CrowdsecCTI("1.2.3.4")
|
||||
assert.Equal(t, item, &cticlient.SmokeItem{})
|
||||
assert.Equal(t, CTIApiEnabled, false)
|
||||
assert.Equal(t, err, cticlient.ErrDisabled)
|
||||
}
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
defer ShutdownCrowdsecCTI()
|
||||
cacheDuration := 1 * time.Second
|
||||
if err := InitCrowdsecCTI(types.StrPtr(validApiKey), &cacheDuration, nil, nil); err != nil {
|
||||
t.Fatalf("failed to init CTI : %s", err)
|
||||
}
|
||||
//Replace the client created by InitCrowdsecCTI with one that uses a custom transport
|
||||
ctiClient = cticlient.NewCrowdsecCTIClient(cticlient.WithAPIKey(validApiKey), cticlient.WithHTTPClient(&http.Client{
|
||||
Transport: RoundTripFunc(smokeHandler),
|
||||
}))
|
||||
|
||||
item, err := CrowdsecCTI("1.2.3.4")
|
||||
assert.Equal(t, "1.2.3.4", item.Ip)
|
||||
assert.Equal(t, CTIApiEnabled, true)
|
||||
assert.Equal(t, CTICache.Len(true), 1)
|
||||
assert.Equal(t, err, nil)
|
||||
|
||||
item, err = CrowdsecCTI("1.2.3.4")
|
||||
assert.Equal(t, "1.2.3.4", item.Ip)
|
||||
assert.Equal(t, CTIApiEnabled, true)
|
||||
assert.Equal(t, CTICache.Len(true), 1)
|
||||
assert.Equal(t, err, nil)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
assert.Equal(t, CTICache.Len(true), 0)
|
||||
|
||||
item, err = CrowdsecCTI("1.2.3.4")
|
||||
assert.Equal(t, "1.2.3.4", item.Ip)
|
||||
assert.Equal(t, CTIApiEnabled, true)
|
||||
assert.Equal(t, CTICache.Len(true), 1)
|
||||
assert.Equal(t, err, nil)
|
||||
|
||||
}
|
|
@ -69,6 +69,7 @@ func GetExprEnv(ctx map[string]interface{}) map[string]interface{} {
|
|||
"GetDecisionsCount": GetDecisionsCount,
|
||||
"GetDecisionsSinceCount": GetDecisionsSinceCount,
|
||||
"Sprintf": fmt.Sprintf,
|
||||
"CrowdsecCTI": CrowdsecCTI,
|
||||
"ParseUnix": ParseUnix,
|
||||
"GetFromStash": cache.GetKey,
|
||||
"SetInStash": cache.SetKey,
|
||||
|
@ -258,6 +259,7 @@ func GetDecisionsCount(value string) int {
|
|||
if dbClient == nil {
|
||||
log.Error("No database config to call GetDecisionsCount()")
|
||||
return 0
|
||||
|
||||
}
|
||||
count, err := dbClient.CountDecisionsByValue(value)
|
||||
if err != nil {
|
||||
|
|
Loading…
Reference in a new issue