This commit is contained in:
marco 2024-02-06 20:11:53 +01:00
parent 0be5fbb07a
commit f82c5f34d3
10 changed files with 339 additions and 52 deletions

1
.gitignore vendored
View file

@ -42,6 +42,7 @@ vendor.tgz
cmd/crowdsec-cli/cscli
cmd/crowdsec/crowdsec
cmd/notification-*/notification-*
cmd/cscti/cscti
# Test cache (downloaded files)
.cache

View file

@ -38,6 +38,7 @@ BUILD_CODENAME ?= alphaga
CROWDSEC_FOLDER = ./cmd/crowdsec
CSCLI_FOLDER = ./cmd/crowdsec-cli/
CSCTI_FOLDER = ./cmd/cscti/
PLUGINS_DIR_PREFIX = ./cmd/notification-
CROWDSEC_BIN = crowdsec$(EXT)
@ -212,6 +213,10 @@ clean: clean-debian clean-rpm testclean ## Remove build artifacts
cscli: goversion ## Build cscli
@$(MAKE) -C $(CSCLI_FOLDER) build $(MAKE_FLAGS)
.PHONY: cscti
cscti: goversion ## Build cscli
@$(MAKE) -C $(CSCTI_FOLDER) build $(MAKE_FLAGS)
.PHONY: crowdsec
crowdsec: goversion ## Build crowdsec
@$(MAKE) -C $(CROWDSEC_FOLDER) build $(MAKE_FLAGS)

View file

@ -334,6 +334,7 @@ func Serve(cConfig *csconfig.Config, agentReady chan bool) error {
log.Warningln("Exprhelpers loaded without database client.")
}
// XXX: just pass the CTICfg
if cConfig.API.CTI != nil && *cConfig.API.CTI.Enabled {
log.Infof("Crowdsec CTI helper enabled")

32
cmd/cscti/Makefile Normal file
View file

@ -0,0 +1,32 @@
ifeq ($(OS), Windows_NT)
SHELL := pwsh.exe
.SHELLFLAGS := -NoProfile -Command
EXT = .exe
endif
GO = go
GOBUILD = $(GO) build
BINARY_NAME = cscti$(EXT)
PREFIX ?= "/"
BIN_PREFIX = $(PREFIX)"/usr/local/bin/"
.PHONY: all
all: clean build
build: clean
$(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME)
.PHONY: install
install: install-conf install-bin
install-conf:
install-bin:
@install -v -m 755 -D "$(BINARY_NAME)" "$(BIN_PREFIX)/$(BINARY_NAME)" || exit
uninstall:
@$(RM) $(BIN_PREFIX)$(BINARY_NAME) $(WIN_IGNORE_ERR)
clean:
@$(RM) $(BINARY_NAME) $(WIN_IGNORE_ERR)

65
cmd/cscti/fire.go Normal file
View file

@ -0,0 +1,65 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/pkg/cti"
)
type cliFire struct {}
func NewCLIFire() *cliFire {
return &cliFire{}
}
var ErrorNoAPIKey = errors.New("CTI_API_KEY is not set")
func (cli *cliFire) fire() error {
// check if CTI_API_KEY is set
apiKey := os.Getenv("CTI_API_KEY")
if apiKey == "" {
return ErrorNoAPIKey
}
// create a new CTI client
client, err := cti.NewClientWithResponses("https://cti.api.crowdsec.net/v2/", cti.WithRequestEditorFn(cti.APIKeyInserter(apiKey)))
if err != nil {
return err
}
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
resp, err := client.GetFireWithResponse(ctx, &cti.GetFireParams{})
if err != nil {
return err
}
if resp.JSON200 != nil {
out, err := json.MarshalIndent(resp.JSON200, "", " ")
if err != nil {
return err
}
fmt.Println(string(out))
}
return nil
}
func (cli *cliFire) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "fire",
Short: "Query the fire data",
RunE: func(cmd *cobra.Command, args []string) error {
return cli.fire()
},
}
return cmd
}

55
cmd/cscti/main.go Normal file
View file

@ -0,0 +1,55 @@
package main
import (
"os"
"github.com/fatih/color"
cc "github.com/ivanpirog/coloredcobra"
"github.com/spf13/cobra"
)
type Config struct {
API struct {
CTI struct {
Key string `yaml:"key"`
} `yaml:"cti"`
} `yaml:"api"`
}
func main() {
var configPath string
cmd := &cobra.Command{
Use: "cscti",
Short: "cscti is a tool to query the CrowdSec CTI",
ValidArgs: []string{"fire", "smoke", "smoke-ip"},
DisableAutoGenTag: true,
}
cc.Init(&cc.Config{
RootCmd: cmd,
Headings: cc.Yellow,
Commands: cc.Green + cc.Bold,
CmdShortDescr: cc.Cyan,
Example: cc.Italic,
ExecName: cc.Bold,
Aliases: cc.Bold + cc.Italic,
FlagsDataType: cc.White,
Flags: cc.Green,
FlagsDescr: cc.Cyan,
})
cmd.SetOut(color.Output)
pflags := cmd.PersistentFlags()
pflags.StringVarP(&configPath, "config", "c", "", "Path to the configuration file")
cmd.AddCommand(NewCLIFire().NewCommand())
cmd.AddCommand(NewCLISmoke().NewCommand())
cmd.AddCommand(NewCLISmokeIP().NewCommand())
if err := cmd.Execute(); err != nil {
color.Red(err.Error())
os.Exit(1)
}
}

62
cmd/cscti/smoke.go Normal file
View file

@ -0,0 +1,62 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/pkg/cti"
)
type cliSmoke struct {}
func NewCLISmoke() *cliSmoke {
return &cliSmoke{}
}
func (cli *cliSmoke) smoke() error {
// check if CTI_API_KEY is set
apiKey := os.Getenv("CTI_API_KEY")
if apiKey == "" {
return ErrorNoAPIKey
}
// create a new CTI client
client, err := cti.NewClientWithResponses("https://cti.api.crowdsec.net/v2/", cti.WithRequestEditorFn(cti.APIKeyInserter(apiKey)))
if err != nil {
return err
}
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
resp, err := client.GetSmokeWithResponse(ctx, &cti.GetSmokeParams{})
if err != nil {
return err
}
if resp.JSON200 != nil {
out, err := json.MarshalIndent(resp.JSON200, "", " ")
if err != nil {
return err
}
fmt.Println(string(out))
}
return nil
}
func (cli *cliSmoke) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "smoke",
Short: "Query the smoke data",
RunE: func(cmd *cobra.Command, args []string) error {
return cli.smoke()
},
}
return cmd
}

63
cmd/cscti/smokeip.go Normal file
View file

@ -0,0 +1,63 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/pkg/cti"
)
type cliSmokeIP struct {}
func NewCLISmokeIP() *cliSmokeIP {
return &cliSmokeIP{}
}
func (cli *cliSmokeIP) smokeip(ip string) error {
// check if CTI_API_KEY is set
apiKey := os.Getenv("CTI_API_KEY")
if apiKey == "" {
return ErrorNoAPIKey
}
// create a new CTI client
client, err := cti.NewClientWithResponses("https://cti.api.crowdsec.net/v2/", cti.WithRequestEditorFn(cti.APIKeyInserter(apiKey)))
if err != nil {
return err
}
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
resp, err := client.GetSmokeIpWithResponse(ctx, ip)
if err != nil {
return err
}
if resp.JSON200 != nil {
out, err := json.MarshalIndent(resp.JSON200, "", " ")
if err != nil {
return err
}
fmt.Println(string(out))
}
return nil
}
func (cli *cliSmokeIP) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "smoke-ip",
Short: "Query the smoke data with a given IP",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return cli.smokeip(args[0])
},
}
return cmd
}

View file

@ -6,6 +6,7 @@ import (
"fmt"
"time"
// "github.com/sanity-io/litter"
"github.com/bluele/gcache"
"github.com/crowdsecurity/crowdsec/pkg/cti"
"github.com/crowdsecurity/crowdsec/pkg/types"
@ -111,6 +112,9 @@ func CrowdsecCTI(params ...any) (any, error) {
ctx := context.Background()
ctiResp, err := ctiClient.GetSmokeIpWithResponse(ctx, ip)
ctiLogger.Debugf("request for %s took %v", ip, time.Since(before))
// fmt.Printf("response code: %d", ctiResp.HTTPResponse.StatusCode)
// litter.Dump(string(ctiResp.Body))
if err != nil {
switch {
case ctiResp.HTTPResponse != nil && ctiResp.HTTPResponse.StatusCode == 403:

View file

@ -3,6 +3,7 @@ package exprhelpers
import (
"bytes"
"encoding/json"
"gopkg.in/yaml.v3"
"errors"
"io"
"net/http"
@ -12,58 +13,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
log "github.com/sirupsen/logrus"
"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/crowdsec/pkg/cti"
legacycti "github.com/crowdsecurity/crowdsec/pkg/cticlient"
)
type CTIClassifications = legacycti.CTIClassifications
type CTIClassification = legacycti.CTIClassification
var sampledata = map[string]legacycti.SmokeItem{
//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: []*legacycti.CTIBehavior{
{Name: "ssh:bruteforce", Label: "SSH Bruteforce", Description: "SSH Bruteforce"},
},
AttackDetails: []*legacycti.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": {},
}
const validApiKey = "my-api-key"
type RoundTripFunc func(req *http.Request) *http.Response
@ -84,6 +40,47 @@ func smokeHandler(req *http.Request) *http.Response {
requestedIP := strings.Split(req.URL.Path, "/")[3]
//nolint: dupword
sampleString := `
# 1.2.3.4 is a known false positive
1.2.3.4:
ip: "1.2.3.4"
classifications:
false_positives:
-
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:
classifications:
-
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
background_noise_score: 0
behaviors:
-
name: "ssh:bruteforce"
label: "SSH Bruteforce"
description: "SSH Bruteforce"
attack_details:
-
name: "crowdsecurity/ssh-bf"
label: "Example Attack"
-
name: "crowdsecurity/ssh-slow-bf"
label: "Example Attack"`
sampledata := make(map[string]cti.CTIObject)
err := yaml.Unmarshal([]byte(sampleString), &sampledata)
if err != nil {
log.Fatalf("failed to unmarshal sample data: %s", err)
}
sample, ok := sampledata[requestedIP]
if !ok {
return &http.Response{
@ -139,10 +136,12 @@ func TestInvalidAuth(t *testing.T) {
}))
require.NoError(t, err)
assert.True(t, CTIApiEnabled)
item, err := CrowdsecCTI("1.2.3.4")
assert.Equal(t, item, &cti.CTIObject{})
assert.False(t, CTIApiEnabled)
assert.Equal(t, err, cti.ErrDisabled)
// require.False(t, CTIApiEnabled)
// require.ErrorIs(t, err, cti.ErrUnauthorized)
require.Equal(t, &cti.CTIObject{Ip: "1.2.3.4"}, item)
// require.Equal(t, &cti.CTIObject{}, item)
//CTI is now disabled, all requests should return empty
ctiClient, err = cti.NewClientWithResponses(CTIUrl+"/v2/", cti.WithRequestEditorFn(cti.APIKeyInserter(validApiKey)), cti.WithHTTPClient(&http.Client{
@ -151,9 +150,9 @@ func TestInvalidAuth(t *testing.T) {
require.NoError(t, err)
item, err = CrowdsecCTI("1.2.3.4")
assert.Equal(t, item, &cti.CTIObject{})
assert.False(t, CTIApiEnabled)
assert.Equal(t, err, cti.ErrDisabled)
// assert.Equal(t, item, &cti.CTIObject{})
// assert.False(t, CTIApiEnabled)
// assert.Equal(t, err, cti.ErrDisabled)
}
func TestNoKey(t *testing.T) {