From a1e96738fb5cfd798cd7a546d54dedea51d6c6dd Mon Sep 17 00:00:00 2001 From: Mario Candela Date: Thu, 1 Aug 2024 20:05:05 +0200 Subject: [PATCH] Feat: beelzebub cloud integrations (#117) * improve beelzebub cloud integration * refactoring cloud integration, fix unit test * add unit test get honeypots * improve code coverage --- builder/builder.go | 16 +++ builder/director.go | 4 +- configurations/beelzebub.yaml | 9 +- docker-compose.yml | 1 + parser/configurations_parser.go | 22 +--- parser/configurations_parser_test.go | 28 ++--- plugins/beelzebub-cloud.go | 54 ++++++++- plugins/beelzebub-cloud_test.go | 163 ++++++++++++++++++++++++++- plugins/llm-integration.go | 12 ++ plugins/llm-integration_test.go | 13 +++ protocols/strategies/http.go | 2 +- protocols/strategies/ssh.go | 2 +- 12 files changed, 276 insertions(+), 50 deletions(-) diff --git a/builder/builder.go b/builder/builder.go index 09cfad7..25aa0a5 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "github.com/mariocandela/beelzebub/v3/parser" + "github.com/mariocandela/beelzebub/v3/plugins" "github.com/mariocandela/beelzebub/v3/protocols" "github.com/mariocandela/beelzebub/v3/protocols/strategies" "github.com/mariocandela/beelzebub/v3/tracer" @@ -112,6 +113,21 @@ Honeypot Framework, happy hacking!`) // Init Tracer strategies, and set the trace strategy default HTTP protocolManager := protocols.InitProtocolManager(b.traceStrategy, hypertextTransferProtocolStrategy) + if b.beelzebubCoreConfigurations.Core.BeelzebubCloud.Enabled { + conf := b.beelzebubCoreConfigurations.Core.BeelzebubCloud + + beelzebubCloud := plugins.InitBeelzebubCloud(conf.URI, conf.AuthToken) + + if honeypotsConfiguration, err := beelzebubCloud.GetHoneypotsConfigurations(); err != nil { + return err + } else { + if len(honeypotsConfiguration) == 0 { + return errors.New("No honeypots configuration found") + } + b.beelzebubServicesConfiguration = honeypotsConfiguration + } + } + for _, beelzebubServiceConfiguration := range b.beelzebubServicesConfiguration { switch beelzebubServiceConfiguration.Protocol { case "http": diff --git a/builder/director.go b/builder/director.go index 31d2c9a..882e957 100644 --- a/builder/director.go +++ b/builder/director.go @@ -38,7 +38,7 @@ func (d *Director) BuildBeelzebub(beelzebubCoreConfigurations *parser.BeelzebubC } } - if beelzebubCoreConfigurations.Core.Tracings.BeelzebubCloud.Enabled { + if beelzebubCoreConfigurations.Core.BeelzebubCloud.Enabled { d.builder.setTraceStrategy(d.beelzebubCloudStrategy) } @@ -58,7 +58,7 @@ func (d *Director) beelzebubCloudStrategy(event tracer.Event) { "event": event, }).Info("New Event") - conf := d.builder.beelzebubCoreConfigurations.Core.Tracings.BeelzebubCloud + conf := d.builder.beelzebubCoreConfigurations.Core.BeelzebubCloud beelzebubCloud := plugins.InitBeelzebubCloud(conf.URI, conf.AuthToken) diff --git a/configurations/beelzebub.yaml b/configurations/beelzebub.yaml index bc944ef..36c23c4 100644 --- a/configurations/beelzebub.yaml +++ b/configurations/beelzebub.yaml @@ -8,11 +8,10 @@ core: rabbit-mq: enabled: false uri: "" - beelzebub-cloud: - enabled: false - uri: "" - auth-token: "" prometheus: path: "/metrics" port: ":2112" - + beelzebub-cloud: + enabled: false + uri: "" + auth-token: "" diff --git a/docker-compose.yml b/docker-compose.yml index 39afd87..30b7006 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: - "22:22" - "2222:2222" - "8080:8080" + - "8081:8081" - "80:80" - "3306:3306" - "2112:2112" # Prometheus openmetrics diff --git a/parser/configurations_parser.go b/parser/configurations_parser.go index 911301b..f96fbc8 100644 --- a/parser/configurations_parser.go +++ b/parser/configurations_parser.go @@ -3,7 +3,6 @@ package parser import ( "fmt" - "github.com/mariocandela/beelzebub/v3/plugins" "os" "path/filepath" "strings" @@ -15,9 +14,10 @@ import ( // BeelzebubCoreConfigurations is the struct that contains the configurations of the core type BeelzebubCoreConfigurations struct { Core struct { - Logging Logging `yaml:"logging"` - Tracings Tracings `yaml:"tracings"` - Prometheus Prometheus `yaml:"prometheus"` + Logging Logging `yaml:"logging"` + Tracings Tracings `yaml:"tracings"` + Prometheus Prometheus `yaml:"prometheus"` + BeelzebubCloud BeelzebubCloud `yaml:"beelzebub-cloud"` } } @@ -31,8 +31,7 @@ type Logging struct { // Tracings is the struct that contains the configurations of the tracings type Tracings struct { - RabbitMQ `yaml:"rabbit-mq"` - BeelzebubCloud `yaml:"beelzebub-cloud"` + RabbitMQ `yaml:"rabbit-mq"` } type BeelzebubCloud struct { @@ -55,17 +54,6 @@ type Plugin struct { LLMModel string `yaml:"llmModel"` } -func FromString(llmModel string) (plugins.LLMModel, error) { - switch llmModel { - case "llama3": - return plugins.LLAMA3, nil - case "gpt4-o": - return plugins.GPT4O, nil - default: - return -1, fmt.Errorf("model %s not found", llmModel) - } -} - // BeelzebubServiceConfiguration is the struct that contains the configurations of the honeypot service type BeelzebubServiceConfiguration struct { ApiVersion string `yaml:"apiVersion"` diff --git a/parser/configurations_parser_test.go b/parser/configurations_parser_test.go index a17a60b..1b90922 100644 --- a/parser/configurations_parser_test.go +++ b/parser/configurations_parser_test.go @@ -2,7 +2,6 @@ package parser import ( "errors" - "github.com/mariocandela/beelzebub/v3/plugins" "os" "testing" @@ -21,10 +20,10 @@ core: rabbit-mq: enabled: true uri: "amqp://user:password@localhost/" - beelzebub-cloud: - enabled: true - uri: "amqp://user:password@localhost/" - auth-token: "iejfdjsl-aosdajosoidaj-dunfkjnfkjsdnkn"`) + beelzebub-cloud: + enabled: true + uri: "amqp://user:password@localhost/" + auth-token: "iejfdjsl-aosdajosoidaj-dunfkjnfkjsdnkn"`) return configurationsCoreBytes, nil } @@ -95,9 +94,9 @@ func TestReadConfigurationsCoreValid(t *testing.T) { assert.Equal(t, coreConfigurations.Core.Logging.LogsPath, "./logs") assert.Equal(t, coreConfigurations.Core.Tracings.RabbitMQ.Enabled, true) assert.Equal(t, coreConfigurations.Core.Tracings.RabbitMQ.URI, "amqp://user:password@localhost/") - assert.Equal(t, coreConfigurations.Core.Tracings.BeelzebubCloud.Enabled, true) - assert.Equal(t, coreConfigurations.Core.Tracings.BeelzebubCloud.URI, "amqp://user:password@localhost/") - assert.Equal(t, coreConfigurations.Core.Tracings.BeelzebubCloud.AuthToken, "iejfdjsl-aosdajosoidaj-dunfkjnfkjsdnkn") + assert.Equal(t, coreConfigurations.Core.BeelzebubCloud.Enabled, true) + assert.Equal(t, coreConfigurations.Core.BeelzebubCloud.URI, "amqp://user:password@localhost/") + assert.Equal(t, coreConfigurations.Core.BeelzebubCloud.AuthToken, "iejfdjsl-aosdajosoidaj-dunfkjnfkjsdnkn") } func TestReadConfigurationsServicesFail(t *testing.T) { @@ -186,16 +185,3 @@ func TestReadFileBytesByFilePath(t *testing.T) { assert.Equal(t, "", string(bytes)) } - -func TestFromString(t *testing.T) { - model, err := FromString("llama3") - assert.Nil(t, err) - assert.Equal(t, plugins.LLAMA3, model) - - model, err = FromString("gpt4-o") - assert.Nil(t, err) - assert.Equal(t, plugins.GPT4O, model) - - model, err = FromString("beelzebub-model") - assert.Errorf(t, err, "model beelzebub-model not found") -} diff --git a/plugins/beelzebub-cloud.go b/plugins/beelzebub-cloud.go index 9c228a5..0e397e8 100644 --- a/plugins/beelzebub-cloud.go +++ b/plugins/beelzebub-cloud.go @@ -3,9 +3,12 @@ package plugins import ( "encoding/json" "errors" + "fmt" "github.com/go-resty/resty/v2" + "github.com/mariocandela/beelzebub/v3/parser" "github.com/mariocandela/beelzebub/v3/tracer" log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" ) type beelzebubCloud struct { @@ -14,6 +17,13 @@ type beelzebubCloud struct { client *resty.Client } +type HoneypotConfigResponseDTO struct { + ID string `json:"id"` + Config string `json:"config"` + TokenID string `json:"tokenId"` + LastUpdatedOn string `json:"lastUpdatedOn"` +} + func InitBeelzebubCloud(uri, authToken string) *beelzebubCloud { return &beelzebubCloud{ URI: uri, @@ -36,7 +46,8 @@ func (beelzebubCloud *beelzebubCloud) SendEvent(event tracer.Event) (bool, error SetHeader("Content-Type", "application/json"). SetBody(requestJson). SetHeader("Authorization", beelzebubCloud.AuthToken). - Post(beelzebubCloud.URI) + SetResult(&tracer.Event{}). + Post(fmt.Sprintf("%s/events", beelzebubCloud.URI)) log.Debug(response) @@ -46,3 +57,44 @@ func (beelzebubCloud *beelzebubCloud) SendEvent(event tracer.Event) (bool, error return response.StatusCode() == 200, nil } + +func (beelzebubCloud *beelzebubCloud) GetHoneypotsConfigurations() ([]parser.BeelzebubServiceConfiguration, error) { + if beelzebubCloud.AuthToken == "" { + return nil, errors.New("authToken is empty") + } + + response, err := beelzebubCloud.client.R(). + SetHeader("Content-Type", "application/json"). + SetHeader("Authorization", beelzebubCloud.AuthToken). + SetResult([]HoneypotConfigResponseDTO{}). + Get(fmt.Sprintf("%s/honeypots", beelzebubCloud.URI)) + + if err != nil { + return nil, err + } + + if response.StatusCode() != 200 { + return nil, errors.New(fmt.Sprintf("Response code: %v, error: %s", response.StatusCode(), string(response.Body()))) + } + + var honeypotsConfig []HoneypotConfigResponseDTO + + if err = json.Unmarshal(response.Body(), &honeypotsConfig); err != nil { + return nil, err + } + + var servicesConfiguration = make([]parser.BeelzebubServiceConfiguration, 0) + + for _, honeypotConfig := range honeypotsConfig { + var honeypotsConfig parser.BeelzebubServiceConfiguration + + if err = yaml.Unmarshal([]byte(honeypotConfig.Config), &honeypotsConfig); err != nil { + return nil, err + } + servicesConfiguration = append(servicesConfiguration, honeypotsConfig) + } + + log.Debug(servicesConfiguration) + + return servicesConfiguration, nil +} diff --git a/plugins/beelzebub-cloud_test.go b/plugins/beelzebub-cloud_test.go index d7e5974..0cb207f 100644 --- a/plugins/beelzebub-cloud_test.go +++ b/plugins/beelzebub-cloud_test.go @@ -1,8 +1,10 @@ package plugins import ( + "fmt" "github.com/go-resty/resty/v2" "github.com/jarcoal/httpmock" + "github.com/mariocandela/beelzebub/v3/parser" "github.com/mariocandela/beelzebub/v3/tracer" "github.com/stretchr/testify/assert" "net/http" @@ -22,10 +24,10 @@ func TestBuildSendEventWithResults(t *testing.T) { httpmock.ActivateNonDefault(client.GetClient()) defer httpmock.DeactivateAndReset() - uri := "localhost:8081/events" + uri := "localhost:8081" // Given - httpmock.RegisterResponder("POST", uri, + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/events", uri), func(req *http.Request) (*http.Response, error) { resp, err := httpmock.NewJsonResponse(200, &tracer.Event{}) if err != nil { @@ -69,3 +71,160 @@ func TestBuildSendEventErro(t *testing.T) { //Then assert.Equal(t, false, result) } + +func TestGetHoneypotsConfigurationsWithResults(t *testing.T) { + client := resty.New() + httpmock.ActivateNonDefault(client.GetClient()) + defer httpmock.DeactivateAndReset() + + uri := "localhost:8081" + + // Given + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/honeypots", uri), + func(req *http.Request) (*http.Response, error) { + resp, err := httpmock.NewJsonResponse(200, &[]HoneypotConfigResponseDTO{ + { + ID: "123456", + Config: "apiVersion: \"v1\"\nprotocol: \"ssh\"\naddress: \":2222\"\ndescription: \"SSH interactive ChatGPT\"\ncommands:\n - regex: \"^(.+)$\"\n plugin: \"LLMHoneypot\"\nserverVersion: \"OpenSSH\"\nserverName: \"ubuntu\"\npasswordRegex: \"^(root|qwerty|Smoker666|123456|jenkins|minecraft|sinus|alex|postgres|Ly123456)$\"\ndeadlineTimeoutSeconds: 60\nplugin:\n llmModel: \"gpt4-o\"\n openAISecretKey: \"1234\"\n", + TokenID: "1234567", + }, + }) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + beelzebubCloud := InitBeelzebubCloud(uri, "sdjdnklfjndslkjanfk") + beelzebubCloud.client = client + + //When + result, err := beelzebubCloud.GetHoneypotsConfigurations() + + //Then + assert.Equal(t, &[]parser.BeelzebubServiceConfiguration{ + { + ApiVersion: "v1", + Protocol: "ssh", + Address: ":2222", + Description: "SSH interactive ChatGPT", + Commands: []parser.Command{ + { + Regex: "^(.+)$", + Plugin: "LLMHoneypot", + }, + }, + ServerVersion: "OpenSSH", + ServerName: "ubuntu", + PasswordRegex: "^(root|qwerty|Smoker666|123456|jenkins|minecraft|sinus|alex|postgres|Ly123456)$", + DeadlineTimeoutSeconds: 60, + Plugin: parser.Plugin{ + LLMModel: "gpt4-o", + OpenAISecretKey: "1234", + }, + }, + }, &result) + assert.Nil(t, err) +} + +func TestGetHoneypotsConfigurationsWithErrorValidation(t *testing.T) { + //Given + beelzebubCloud := InitBeelzebubCloud("", "") + + //When + result, err := beelzebubCloud.GetHoneypotsConfigurations() + + //Then + assert.Nil(t, result) + assert.Equal(t, "authToken is empty", err.Error()) +} + +func TestGetHoneypotsConfigurationsWithErrorAPI(t *testing.T) { + client := resty.New() + httpmock.ActivateNonDefault(client.GetClient()) + defer httpmock.DeactivateAndReset() + + uri := "localhost:8081" + + // Given + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/honeypots", uri), + func(req *http.Request) (*http.Response, error) { + return httpmock.NewStringResponse(500, ""), nil + }, + ) + + beelzebubCloud := InitBeelzebubCloud(uri, "sdjdnklfjndslkjanfk") + beelzebubCloud.client = client + + //When + result, err := beelzebubCloud.GetHoneypotsConfigurations() + + //Then + assert.Nil(t, result) + assert.Equal(t, "Response code: 500, error: ", err.Error()) +} + +func TestGetHoneypotsConfigurationsWithErrorUnmarshal(t *testing.T) { + client := resty.New() + httpmock.ActivateNonDefault(client.GetClient()) + defer httpmock.DeactivateAndReset() + + uri := "localhost:8081" + + // Given + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/honeypots", uri), + func(req *http.Request) (*http.Response, error) { + resp, err := httpmock.NewJsonResponse(200, "error") + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + beelzebubCloud := InitBeelzebubCloud(uri, "sdjdnklfjndslkjanfk") + beelzebubCloud.client = client + + //When + result, err := beelzebubCloud.GetHoneypotsConfigurations() + + //Then + assert.Nil(t, result) + assert.Equal(t, "json: cannot unmarshal string into Go value of type []plugins.HoneypotConfigResponseDTO", err.Error()) +} + +func TestGetHoneypotsConfigurationsWithErrorDeserializeYaml(t *testing.T) { + client := resty.New() + httpmock.ActivateNonDefault(client.GetClient()) + defer httpmock.DeactivateAndReset() + + uri := "localhost:8081" + + // Given + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/honeypots", uri), + func(req *http.Request) (*http.Response, error) { + resp, err := httpmock.NewJsonResponse(200, &[]HoneypotConfigResponseDTO{ + { + ID: "123456", + Config: "error", + TokenID: "1234567", + }, + }) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + beelzebubCloud := InitBeelzebubCloud(uri, "sdjdnklfjndslkjanfk") + beelzebubCloud.client = client + + //When + result, err := beelzebubCloud.GetHoneypotsConfigurations() + + //Then + assert.Nil(t, result) + assert.Equal(t, "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `error` into parser.BeelzebubServiceConfiguration", err.Error()) +} diff --git a/plugins/llm-integration.go b/plugins/llm-integration.go index 125c118..f63f44e 100644 --- a/plugins/llm-integration.go +++ b/plugins/llm-integration.go @@ -3,6 +3,7 @@ package plugins import ( "encoding/json" "errors" + "fmt" "github.com/go-resty/resty/v2" "github.com/mariocandela/beelzebub/v3/tracer" @@ -76,6 +77,17 @@ const ( GPT4O ) +func FromStringToLLMModel(llmModel string) (LLMModel, error) { + switch llmModel { + case "llama3": + return LLAMA3, nil + case "gpt4-o": + return GPT4O, nil + default: + return -1, fmt.Errorf("model %s not found", llmModel) + } +} + func InitLLMHoneypot(config LLMHoneypot) *LLMHoneypot { // Inject the dependencies config.client = resty.New() diff --git a/plugins/llm-integration_test.go b/plugins/llm-integration_test.go index ae5ce7b..a2aaa2e 100644 --- a/plugins/llm-integration_test.go +++ b/plugins/llm-integration_test.go @@ -285,3 +285,16 @@ func TestBuildExecuteModelHTTPWithoutResults(t *testing.T) { //Then assert.Equal(t, "no choices", err.Error()) } + +func TestFromString(t *testing.T) { + model, err := FromStringToLLMModel("llama3") + assert.Nil(t, err) + assert.Equal(t, LLAMA3, model) + + model, err = FromStringToLLMModel("gpt4-o") + assert.Nil(t, err) + assert.Equal(t, GPT4O, model) + + model, err = FromStringToLLMModel("beelzebub-model") + assert.Errorf(t, err, "model beelzebub-model not found") +} diff --git a/protocols/strategies/http.go b/protocols/strategies/http.go index 0df7491..432d474 100644 --- a/protocols/strategies/http.go +++ b/protocols/strategies/http.go @@ -36,7 +36,7 @@ func (httpStrategy HTTPStrategy) Init(beelzebubServiceConfiguration parser.Beelz if command.Plugin == plugins.LLMPluginName { - llmModel, err := parser.FromString(beelzebubServiceConfiguration.Plugin.LLMModel) + llmModel, err := plugins.FromStringToLLMModel(beelzebubServiceConfiguration.Plugin.LLMModel) if err != nil { log.Errorf("Error fromString: %s", err.Error()) diff --git a/protocols/strategies/ssh.go b/protocols/strategies/ssh.go index c059f81..19fc5e1 100644 --- a/protocols/strategies/ssh.go +++ b/protocols/strategies/ssh.go @@ -64,7 +64,7 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze if command.Plugin == plugins.LLMPluginName { - llmModel, err := parser.FromString(beelzebubServiceConfiguration.Plugin.LLMModel) + llmModel, err := plugins.FromStringToLLMModel(beelzebubServiceConfiguration.Plugin.LLMModel) if err != nil { log.Errorf("Error fromString: %s", err.Error())