Feat: Add FallbackCommand for HTTP Strategy, refactor packages strategies (#175)

Add FallbackCommand for HTTP Strategy, refactor packages strategies, improve histories implementations.
This commit is contained in:
Bryan Nolen
2025-03-13 12:36:46 +05:30
committed by GitHub
parent 933f02911b
commit b062416c00
10 changed files with 378 additions and 251 deletions

View File

@ -3,15 +3,18 @@ package builder
import ( import (
"errors" "errors"
"fmt" "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"
"io" "io"
"net/http" "net/http"
"os" "os"
"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/HTTP"
"github.com/mariocandela/beelzebub/v3/protocols/strategies/SSH"
"github.com/mariocandela/beelzebub/v3/protocols/strategies/TCP"
"github.com/mariocandela/beelzebub/v3/tracer"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
amqp "github.com/rabbitmq/amqp091-go" amqp "github.com/rabbitmq/amqp091-go"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -106,9 +109,9 @@ Honeypot Framework, happy hacking!`)
}() }()
// Init Protocol strategies // Init Protocol strategies
secureShellStrategy := &strategies.SSHStrategy{} secureShellStrategy := &SSH.SSHStrategy{}
hypertextTransferProtocolStrategy := &strategies.HTTPStrategy{} hypertextTransferProtocolStrategy := &HTTP.HTTPStrategy{}
transmissionControlProtocolStrategy := &strategies.TCPStrategy{} transmissionControlProtocolStrategy := &TCP.TCPStrategy{}
// Init Tracer strategies, and set the trace strategy default HTTP // Init Tracer strategies, and set the trace strategy default HTTP
protocolManager := protocols.InitProtocolManager(b.traceStrategy, hypertextTransferProtocolStrategy) protocolManager := protocols.InitProtocolManager(b.traceStrategy, hypertextTransferProtocolStrategy)
@ -122,7 +125,7 @@ Honeypot Framework, happy hacking!`)
return err return err
} else { } else {
if len(honeypotsConfiguration) == 0 { if len(honeypotsConfiguration) == 0 {
return errors.New("No honeypots configuration found") return errors.New("no honeypots configuration found")
} }
b.beelzebubServicesConfiguration = honeypotsConfiguration b.beelzebubServicesConfiguration = honeypotsConfiguration
} }
@ -132,20 +135,16 @@ Honeypot Framework, happy hacking!`)
switch beelzebubServiceConfiguration.Protocol { switch beelzebubServiceConfiguration.Protocol {
case "http": case "http":
protocolManager.SetProtocolStrategy(hypertextTransferProtocolStrategy) protocolManager.SetProtocolStrategy(hypertextTransferProtocolStrategy)
break
case "ssh": case "ssh":
protocolManager.SetProtocolStrategy(secureShellStrategy) protocolManager.SetProtocolStrategy(secureShellStrategy)
break
case "tcp": case "tcp":
protocolManager.SetProtocolStrategy(transmissionControlProtocolStrategy) protocolManager.SetProtocolStrategy(transmissionControlProtocolStrategy)
break
default: default:
log.Fatalf("Protocol %s not managed", beelzebubServiceConfiguration.Protocol) log.Fatalf("protocol %s not managed", beelzebubServiceConfiguration.Protocol)
continue
} }
if err := protocolManager.InitService(beelzebubServiceConfiguration); err != nil { if err := protocolManager.InitService(beelzebubServiceConfiguration); err != nil {
return errors.New(fmt.Sprintf("Error during init protocol: %s, %s", beelzebubServiceConfiguration.Protocol, err.Error())) return fmt.Errorf("error during init protocol: %s, %s", beelzebubServiceConfiguration.Protocol, err.Error())
} }
} }

2
go.mod
View File

@ -13,6 +13,7 @@ require (
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.33.0 golang.org/x/crypto v0.33.0
golang.org/x/term v0.29.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@ -33,6 +34,5 @@ require (
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
golang.org/x/net v0.33.0 // indirect golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.29.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.34.2 // indirect
) )

View File

@ -0,0 +1,47 @@
package historystore
import (
"sync"
"github.com/mariocandela/beelzebub/v3/plugins"
)
// HistoryStore is a thread-safe structure for storing Messages used to build LLM Context.
type HistoryStore struct {
sync.RWMutex
sessions map[string][]plugins.Message
}
// NewHistoryStore returns a prepared HistoryStore
func NewHistoryStore() *HistoryStore {
return &HistoryStore{
sessions: make(map[string][]plugins.Message),
}
}
// HasKey returns true if the supplied key exists in the map.
func (hs *HistoryStore) HasKey(key string) bool {
hs.RLock()
defer hs.RUnlock()
_, ok := hs.sessions[key]
return ok
}
// Query returns the value stored at the map
func (hs *HistoryStore) Query(key string) []plugins.Message {
hs.RLock()
defer hs.RUnlock()
return hs.sessions[key]
}
// Append will add the slice of Mesages to the entry for the key.
// If the map has not yet been initalised, then a new map is created.
func (hs *HistoryStore) Append(key string, message ...plugins.Message) {
hs.Lock()
defer hs.Unlock()
// In the unexpected case that the map has not yet been initalised, create it.
if hs.sessions == nil {
hs.sessions = make(map[string][]plugins.Message)
}
hs.sessions[key] = append(hs.sessions[key], message...)
}

View File

@ -0,0 +1,47 @@
package historystore
import (
"testing"
"github.com/mariocandela/beelzebub/v3/plugins"
"github.com/stretchr/testify/assert"
)
func TestNewHistoryStore(t *testing.T) {
hs := NewHistoryStore()
assert.NotNil(t, hs)
assert.NotNil(t, hs.sessions)
}
func TestHasKey(t *testing.T) {
hs := NewHistoryStore()
hs.sessions["testKey"] = []plugins.Message{}
assert.True(t, hs.HasKey("testKey"))
assert.False(t, hs.HasKey("nonExistentKey"))
}
func TestQuery(t *testing.T) {
hs := NewHistoryStore()
expectedMessages := []plugins.Message{{Role: "user", Content: "Hello"}}
hs.sessions["testKey"] = expectedMessages
actualMessages := hs.Query("testKey")
assert.Equal(t, expectedMessages, actualMessages)
}
func TestAppend(t *testing.T) {
hs := NewHistoryStore()
message1 := plugins.Message{Role: "user", Content: "Hello"}
message2 := plugins.Message{Role: "assistant", Content: "Hi"}
hs.Append("testKey", message1)
assert.Equal(t, []plugins.Message{message1}, hs.sessions["testKey"])
hs.Append("testKey", message2)
assert.Equal(t, []plugins.Message{message1, message2}, hs.sessions["testKey"])
}
func TestAppendNilSessions(t *testing.T) {
hs := &HistoryStore{}
message1 := plugins.Message{Role: "user", Content: "Hello"}
hs.Append("testKey", message1)
assert.NotNil(t, hs.sessions)
assert.Equal(t, []plugins.Message{message1}, hs.sessions["testKey"])
}

View File

@ -62,6 +62,7 @@ type BeelzebubServiceConfiguration struct {
Protocol string `yaml:"protocol"` Protocol string `yaml:"protocol"`
Address string `yaml:"address"` Address string `yaml:"address"`
Commands []Command `yaml:"commands"` Commands []Command `yaml:"commands"`
FallbackCommand Command `yaml:"fallbackCommand"`
ServerVersion string `yaml:"serverVersion"` ServerVersion string `yaml:"serverVersion"`
ServerName string `yaml:"serverName"` ServerName string `yaml:"serverName"`
DeadlineTimeoutSeconds int `yaml:"deadlineTimeoutSeconds"` DeadlineTimeoutSeconds int `yaml:"deadlineTimeoutSeconds"`

View File

@ -56,6 +56,9 @@ commands:
handler: "login" handler: "login"
headers: headers:
- "Content-Type: text/html" - "Content-Type: text/html"
fallbackCommand:
handler: "404 Not Found!"
statusCode: 404
plugin: plugin:
openAISecretKey: "qwerty" openAISecretKey: "qwerty"
llmModel: "llama3" llmModel: "llama3"
@ -134,6 +137,8 @@ func TestReadConfigurationsServicesValid(t *testing.T) {
assert.Equal(t, firstBeelzebubServiceConfiguration.Commands[0].Handler, "login") assert.Equal(t, firstBeelzebubServiceConfiguration.Commands[0].Handler, "login")
assert.Equal(t, len(firstBeelzebubServiceConfiguration.Commands[0].Headers), 1) assert.Equal(t, len(firstBeelzebubServiceConfiguration.Commands[0].Headers), 1)
assert.Equal(t, firstBeelzebubServiceConfiguration.Commands[0].Headers[0], "Content-Type: text/html") assert.Equal(t, firstBeelzebubServiceConfiguration.Commands[0].Headers[0], "Content-Type: text/html")
assert.Equal(t, firstBeelzebubServiceConfiguration.FallbackCommand.Handler, "404 Not Found!")
assert.Equal(t, firstBeelzebubServiceConfiguration.FallbackCommand.StatusCode, 404)
assert.Equal(t, firstBeelzebubServiceConfiguration.Plugin.OpenAISecretKey, "qwerty") assert.Equal(t, firstBeelzebubServiceConfiguration.Plugin.OpenAISecretKey, "qwerty")
assert.Equal(t, firstBeelzebubServiceConfiguration.Plugin.LLMModel, "llama3") assert.Equal(t, firstBeelzebubServiceConfiguration.Plugin.LLMModel, "llama3")
assert.Equal(t, firstBeelzebubServiceConfiguration.Plugin.LLMProvider, "ollama") assert.Equal(t, firstBeelzebubServiceConfiguration.Plugin.LLMProvider, "ollama")
@ -176,7 +181,8 @@ func TestGelAllFilesNameByDirNameError(t *testing.T) {
files, err := gelAllFilesNameByDirName("nosuchfile") files, err := gelAllFilesNameByDirName("nosuchfile")
assert.Nil(t, files) assert.Nil(t, files)
assert.Equal(t, "open nosuchfile: no such file or directory", err.Error()) // Windows and Linux return slightly different error strings, but share a common prefix, so check for that.
assert.Contains(t, err.Error(), "open nosuchfile: ")
} }
func TestReadFileBytesByFilePath(t *testing.T) { func TestReadFileBytesByFilePath(t *testing.T) {

View File

@ -0,0 +1,186 @@
package HTTP
import (
"fmt"
"io"
"net"
"net/http"
"regexp"
"strings"
"github.com/mariocandela/beelzebub/v3/parser"
"github.com/mariocandela/beelzebub/v3/plugins"
"github.com/mariocandela/beelzebub/v3/tracer"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)
type HTTPStrategy struct{}
type httpResponse struct {
StatusCode int
Headers []string
Body string
}
func (httpStrategy HTTPStrategy) Init(servConf parser.BeelzebubServiceConfiguration, tr tracer.Tracer) error {
serverMux := http.NewServeMux()
serverMux.HandleFunc("/", func(responseWriter http.ResponseWriter, request *http.Request) {
traceRequest(request, tr, servConf.Description)
var matched bool
var resp httpResponse
var err error
for _, command := range servConf.Commands {
var err error
matched, err = regexp.MatchString(command.Regex, request.RequestURI)
if err != nil {
log.Errorf("error parsing regex: %s, %s", command.Regex, err.Error())
resp.StatusCode = 500
resp.Body = "500 Internal Server Error"
continue
}
if matched {
resp, err = buildHTTPResponse(servConf, command, request)
if err != nil {
log.Errorf("error building http response: %s: %v", request.RequestURI, err)
resp.StatusCode = 500
resp.Body = "500 Internal Server Error"
}
break
}
}
// If none of the main commands matched, and we have a fallback command configured, process it here.
// The regexp is ignored for fallback commands, as they are catch-all for any request.
if !matched {
command := servConf.FallbackCommand
if command.Handler != "" || command.Plugin != "" {
resp, err = buildHTTPResponse(servConf, command, request)
if err != nil {
log.Errorf("error building http response: %s: %v", request.RequestURI, err)
resp.StatusCode = 500
resp.Body = "500 Internal Server Error"
}
}
}
setResponseHeaders(responseWriter, resp.Headers, resp.StatusCode)
fmt.Fprint(responseWriter, resp.Body)
})
go func() {
var err error
// Launch a TLS supporting server if we are supplied a TLS Key and Certificate.
// If relative paths are supplied, they are relative to the CWD of the binary.
// The can be self-signed, only the client will validate this (or not).
if servConf.TLSKeyPath != "" && servConf.TLSCertPath != "" {
err = http.ListenAndServeTLS(servConf.Address, servConf.TLSCertPath, servConf.TLSKeyPath, serverMux)
} else {
err = http.ListenAndServe(servConf.Address, serverMux)
}
if err != nil {
log.Errorf("error during init HTTP Protocol: %v", err)
return
}
}()
log.WithFields(log.Fields{
"port": servConf.Address,
"commands": len(servConf.Commands),
}).Infof("Init service: %s", servConf.Description)
return nil
}
func buildHTTPResponse(servConf parser.BeelzebubServiceConfiguration, command parser.Command, request *http.Request) (httpResponse, error) {
resp := httpResponse{
Body: command.Handler,
Headers: command.Headers,
StatusCode: command.StatusCode,
}
if command.Plugin == plugins.LLMPluginName {
llmProvider, err := plugins.FromStringToLLMProvider(servConf.Plugin.LLMProvider)
if err != nil {
log.Errorf("error: %v", err)
resp.Body = "404 Not Found!"
return resp, err
}
llmHoneypot := plugins.LLMHoneypot{
Histories: make([]plugins.Message, 0),
OpenAIKey: servConf.Plugin.OpenAISecretKey,
Protocol: tracer.HTTP,
Host: servConf.Plugin.Host,
Model: servConf.Plugin.LLMModel,
Provider: llmProvider,
CustomPrompt: servConf.Plugin.Prompt,
}
llmHoneypotInstance := plugins.InitLLMHoneypot(llmHoneypot)
command := fmt.Sprintf("%s %s", request.Method, request.RequestURI)
completions, err := llmHoneypotInstance.ExecuteModel(command)
if err != nil {
resp.Body = "404 Not Found!"
return resp, fmt.Errorf("ExecuteModel error: %s, %v", command, err)
}
resp.Body = completions
}
return resp, nil
}
func traceRequest(request *http.Request, tr tracer.Tracer, HoneypotDescription string) {
bodyBytes, err := io.ReadAll(request.Body)
body := ""
if err == nil {
body = string(bodyBytes)
}
host, port, _ := net.SplitHostPort(request.RemoteAddr)
event := tracer.Event{
Msg: "HTTP New request",
RequestURI: request.RequestURI,
Protocol: tracer.HTTP.String(),
HTTPMethod: request.Method,
Body: body,
HostHTTPRequest: request.Host,
UserAgent: request.UserAgent(),
Cookies: mapCookiesToString(request.Cookies()),
Headers: request.Header,
Status: tracer.Stateless.String(),
RemoteAddr: request.RemoteAddr,
SourceIp: host,
SourcePort: port,
ID: uuid.New().String(),
Description: HoneypotDescription,
}
// Capture the TLS details from the request, if provided.
if request.TLS != nil {
event.Msg = "HTTPS New Request"
event.TLSServerName = request.TLS.ServerName
}
tr.TraceEvent(event)
}
func mapCookiesToString(cookies []*http.Cookie) string {
cookiesString := ""
for _, cookie := range cookies {
cookiesString += cookie.String()
}
return cookiesString
}
func setResponseHeaders(responseWriter http.ResponseWriter, headers []string, statusCode int) {
for _, headerStr := range headers {
keyValue := strings.Split(headerStr, ":")
if len(keyValue) > 1 {
responseWriter.Header().Add(keyValue[0], keyValue[1])
}
}
// http.StatusText(statusCode): empty string if the code is unknown.
if len(http.StatusText(statusCode)) > 0 {
responseWriter.WriteHeader(statusCode)
}
}

View File

@ -1,15 +1,17 @@
package strategies package SSH
import ( import (
"fmt" "fmt"
"github.com/mariocandela/beelzebub/v3/parser"
"github.com/mariocandela/beelzebub/v3/plugins"
"github.com/mariocandela/beelzebub/v3/tracer"
"net" "net"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/mariocandela/beelzebub/v3/historystore"
"github.com/mariocandela/beelzebub/v3/parser"
"github.com/mariocandela/beelzebub/v3/plugins"
"github.com/mariocandela/beelzebub/v3/tracer"
"github.com/gliderlabs/ssh" "github.com/gliderlabs/ssh"
"github.com/google/uuid" "github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -17,65 +19,60 @@ import (
) )
type SSHStrategy struct { type SSHStrategy struct {
Sessions map[string][]plugins.Message Sessions *historystore.HistoryStore
} }
func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.BeelzebubServiceConfiguration, tr tracer.Tracer) error { func (sshStrategy *SSHStrategy) Init(servConf parser.BeelzebubServiceConfiguration, tr tracer.Tracer) error {
sshStrategy.Sessions = make(map[string][]plugins.Message) if sshStrategy.Sessions == nil {
sshStrategy.Sessions = historystore.NewHistoryStore()
}
go func() { go func() {
server := &ssh.Server{ server := &ssh.Server{
Addr: beelzebubServiceConfiguration.Address, Addr: servConf.Address,
MaxTimeout: time.Duration(beelzebubServiceConfiguration.DeadlineTimeoutSeconds) * time.Second, MaxTimeout: time.Duration(servConf.DeadlineTimeoutSeconds) * time.Second,
IdleTimeout: time.Duration(beelzebubServiceConfiguration.DeadlineTimeoutSeconds) * time.Second, IdleTimeout: time.Duration(servConf.DeadlineTimeoutSeconds) * time.Second,
Version: beelzebubServiceConfiguration.ServerVersion, Version: servConf.ServerVersion,
Handler: func(sess ssh.Session) { Handler: func(sess ssh.Session) {
uuidSession := uuid.New() uuidSession := uuid.New()
host, port, _ := net.SplitHostPort(sess.RemoteAddr().String()) host, port, _ := net.SplitHostPort(sess.RemoteAddr().String())
sessionKey := host + sess.User() sessionKey := "SSH" + host + sess.User()
// Inline SSH command // Inline SSH command
if sess.RawCommand() != "" { if sess.RawCommand() != "" {
for _, command := range beelzebubServiceConfiguration.Commands { for _, command := range servConf.Commands {
matched, err := regexp.MatchString(command.Regex, sess.RawCommand()) matched, err := regexp.MatchString(command.Regex, sess.RawCommand())
if err != nil { if err != nil {
log.Errorf("Error regex: %s, %s", command.Regex, err.Error()) log.Errorf("error regex: %s, %s", command.Regex, err.Error())
continue continue
} }
if matched { if matched {
commandOutput := command.Handler commandOutput := command.Handler
if command.Plugin == plugins.LLMPluginName { if command.Plugin == plugins.LLMPluginName {
llmProvider, err := plugins.FromStringToLLMProvider(servConf.Plugin.LLMProvider)
llmProvider, err := plugins.FromStringToLLMProvider(beelzebubServiceConfiguration.Plugin.LLMProvider)
if err != nil { if err != nil {
log.Errorf("Error: %s", err.Error()) log.Errorf("error: %s", err.Error())
commandOutput = "command not found" commandOutput = "command not found"
llmProvider = plugins.OpenAI llmProvider = plugins.OpenAI
} }
histories := make([]plugins.Message, 0) var histories []plugins.Message
if sshStrategy.Sessions.HasKey(sessionKey) {
if sshStrategy.Sessions[sessionKey] != nil { histories = sshStrategy.Sessions.Query(sessionKey)
histories = sshStrategy.Sessions[sessionKey]
} }
llmHoneypot := plugins.LLMHoneypot{ llmHoneypot := plugins.LLMHoneypot{
Histories: histories, Histories: histories,
OpenAIKey: beelzebubServiceConfiguration.Plugin.OpenAISecretKey, OpenAIKey: servConf.Plugin.OpenAISecretKey,
Protocol: tracer.SSH, Protocol: tracer.SSH,
Host: beelzebubServiceConfiguration.Plugin.Host, Host: servConf.Plugin.Host,
Model: beelzebubServiceConfiguration.Plugin.LLMModel, Model: servConf.Plugin.LLMModel,
Provider: llmProvider, Provider: llmProvider,
CustomPrompt: beelzebubServiceConfiguration.Plugin.Prompt, CustomPrompt: servConf.Plugin.Prompt,
} }
llmHoneypotInstance := plugins.InitLLMHoneypot(llmHoneypot) llmHoneypotInstance := plugins.InitLLMHoneypot(llmHoneypot)
if commandOutput, err = llmHoneypotInstance.ExecuteModel(sess.RawCommand()); err != nil { if commandOutput, err = llmHoneypotInstance.ExecuteModel(sess.RawCommand()); err != nil {
log.Errorf("Error ExecuteModel: %s, %s", sess.RawCommand(), err.Error()) log.Errorf("error ExecuteModel: %s, %s", sess.RawCommand(), err.Error())
commandOutput = "command not found" commandOutput = "command not found"
} }
} }
@ -83,7 +80,7 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
sess.Write(append([]byte(commandOutput), '\n')) sess.Write(append([]byte(commandOutput), '\n'))
tr.TraceEvent(tracer.Event{ tr.TraceEvent(tracer.Event{
Msg: "New SSH Session", Msg: "New SSH Raw Command Session",
Protocol: tracer.SSH.String(), Protocol: tracer.SSH.String(),
RemoteAddr: sess.RemoteAddr().String(), RemoteAddr: sess.RemoteAddr().String(),
SourceIp: host, SourceIp: host,
@ -92,16 +89,20 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
ID: uuidSession.String(), ID: uuidSession.String(),
Environ: strings.Join(sess.Environ(), ","), Environ: strings.Join(sess.Environ(), ","),
User: sess.User(), User: sess.User(),
Description: beelzebubServiceConfiguration.Description, Description: servConf.Description,
Command: sess.RawCommand(), Command: sess.RawCommand(),
CommandOutput: commandOutput, CommandOutput: commandOutput,
}) })
var histories []plugins.Message var histories []plugins.Message
if sshStrategy.Sessions.HasKey(sessionKey) {
histories = sshStrategy.Sessions.Query(sessionKey)
}
histories = append(histories, plugins.Message{Role: plugins.USER.String(), Content: sess.RawCommand()}) histories = append(histories, plugins.Message{Role: plugins.USER.String(), Content: sess.RawCommand()})
histories = append(histories, plugins.Message{Role: plugins.ASSISTANT.String(), Content: commandOutput}) histories = append(histories, plugins.Message{Role: plugins.ASSISTANT.String(), Content: commandOutput})
sshStrategy.Sessions[sessionKey] = histories sshStrategy.Sessions.Append(sessionKey, histories...)
tr.TraceEvent(tracer.Event{ tr.TraceEvent(tracer.Event{
Msg: "End SSH Session", Msg: "End SSH Raw Command Session",
Status: tracer.End.String(), Status: tracer.End.String(),
ID: uuidSession.String(), ID: uuidSession.String(),
}) })
@ -111,7 +112,7 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
} }
tr.TraceEvent(tracer.Event{ tr.TraceEvent(tracer.Event{
Msg: "New SSH Session", Msg: "New SSH Terminal Session",
Protocol: tracer.SSH.String(), Protocol: tracer.SSH.String(),
RemoteAddr: sess.RemoteAddr().String(), RemoteAddr: sess.RemoteAddr().String(),
SourceIp: host, SourceIp: host,
@ -120,13 +121,13 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
ID: uuidSession.String(), ID: uuidSession.String(),
Environ: strings.Join(sess.Environ(), ","), Environ: strings.Join(sess.Environ(), ","),
User: sess.User(), User: sess.User(),
Description: beelzebubServiceConfiguration.Description, Description: servConf.Description,
}) })
terminal := term.NewTerminal(sess, buildPrompt(sess.User(), beelzebubServiceConfiguration.ServerName)) terminal := term.NewTerminal(sess, buildPrompt(sess.User(), servConf.ServerName))
var histories []plugins.Message var histories []plugins.Message
if sshStrategy.Sessions[sessionKey] != nil { if sshStrategy.Sessions.HasKey(sessionKey) {
histories = sshStrategy.Sessions[sessionKey] histories = sshStrategy.Sessions.Query(sessionKey)
} }
for { for {
@ -134,43 +135,36 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
if err != nil { if err != nil {
break break
} }
if commandInput == "exit" { if commandInput == "exit" {
break break
} }
for _, command := range beelzebubServiceConfiguration.Commands { for _, command := range servConf.Commands {
matched, err := regexp.MatchString(command.Regex, commandInput) matched, err := regexp.MatchString(command.Regex, commandInput)
if err != nil { if err != nil {
log.Errorf("Error regex: %s, %s", command.Regex, err.Error()) log.Errorf("error regex: %s, %s", command.Regex, err.Error())
continue continue
} }
if matched { if matched {
commandOutput := command.Handler commandOutput := command.Handler
if command.Plugin == plugins.LLMPluginName { if command.Plugin == plugins.LLMPluginName {
llmProvider, err := plugins.FromStringToLLMProvider(servConf.Plugin.LLMProvider)
llmProvider, err := plugins.FromStringToLLMProvider(beelzebubServiceConfiguration.Plugin.LLMProvider)
if err != nil { if err != nil {
log.Errorf("Error: %s, fallback OpenAI", err.Error()) log.Errorf("error: %s, fallback OpenAI", err.Error())
llmProvider = plugins.OpenAI llmProvider = plugins.OpenAI
} }
llmHoneypot := plugins.LLMHoneypot{ llmHoneypot := plugins.LLMHoneypot{
Histories: histories, Histories: histories,
OpenAIKey: beelzebubServiceConfiguration.Plugin.OpenAISecretKey, OpenAIKey: servConf.Plugin.OpenAISecretKey,
Protocol: tracer.SSH, Protocol: tracer.SSH,
Host: beelzebubServiceConfiguration.Plugin.Host, Host: servConf.Plugin.Host,
Model: beelzebubServiceConfiguration.Plugin.LLMModel, Model: servConf.Plugin.LLMModel,
Provider: llmProvider, Provider: llmProvider,
CustomPrompt: beelzebubServiceConfiguration.Plugin.Prompt, CustomPrompt: servConf.Plugin.Prompt,
} }
llmHoneypotInstance := plugins.InitLLMHoneypot(llmHoneypot) llmHoneypotInstance := plugins.InitLLMHoneypot(llmHoneypot)
if commandOutput, err = llmHoneypotInstance.ExecuteModel(commandInput); err != nil { if commandOutput, err = llmHoneypotInstance.ExecuteModel(commandInput); err != nil {
log.Errorf("Error ExecuteModel: %s, %s", commandInput, err.Error()) log.Errorf("error ExecuteModel: %s, %s", commandInput, err.Error())
commandOutput = "command not found" commandOutput = "command not found"
} }
} }
@ -190,13 +184,16 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
CommandOutput: commandOutput, CommandOutput: commandOutput,
ID: uuidSession.String(), ID: uuidSession.String(),
Protocol: tracer.SSH.String(), Protocol: tracer.SSH.String(),
Description: beelzebubServiceConfiguration.Description, Description: servConf.Description,
}) })
break break
} }
} }
} }
sshStrategy.Sessions[sessionKey] = histories
// Add all history events for the terminal session to the store.
// This is done at the end of the session to avoid excess lock operations.
sshStrategy.Sessions.Append(sessionKey, histories...)
tr.TraceEvent(tracer.Event{ tr.TraceEvent(tracer.Event{
Msg: "End SSH Session", Msg: "End SSH Session",
Status: tracer.End.String(), Status: tracer.End.String(),
@ -207,7 +204,7 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
host, port, _ := net.SplitHostPort(ctx.RemoteAddr().String()) host, port, _ := net.SplitHostPort(ctx.RemoteAddr().String())
tr.TraceEvent(tracer.Event{ tr.TraceEvent(tracer.Event{
Msg: "New SSH attempt", Msg: "New SSH Login Attempt",
Protocol: tracer.SSH.String(), Protocol: tracer.SSH.String(),
Status: tracer.Stateless.String(), Status: tracer.Stateless.String(),
User: ctx.User(), User: ctx.User(),
@ -217,11 +214,11 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
SourceIp: host, SourceIp: host,
SourcePort: port, SourcePort: port,
ID: uuid.New().String(), ID: uuid.New().String(),
Description: beelzebubServiceConfiguration.Description, Description: servConf.Description,
}) })
matched, err := regexp.MatchString(beelzebubServiceConfiguration.PasswordRegex, password) matched, err := regexp.MatchString(servConf.PasswordRegex, password)
if err != nil { if err != nil {
log.Errorf("Error regex: %s, %s", beelzebubServiceConfiguration.PasswordRegex, err.Error()) log.Errorf("error regex: %s, %s", servConf.PasswordRegex, err.Error())
return false return false
} }
return matched return matched
@ -229,14 +226,14 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
} }
err := server.ListenAndServe() err := server.ListenAndServe()
if err != nil { if err != nil {
log.Errorf("Error during init SSH Protocol: %s", err.Error()) log.Errorf("error during init SSH Protocol: %s", err.Error())
} }
}() }()
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"port": beelzebubServiceConfiguration.Address, "port": servConf.Address,
"commands": len(beelzebubServiceConfiguration.Commands), "commands": len(servConf.Commands),
}).Infof("GetInstance service %s", beelzebubServiceConfiguration.Protocol) }).Infof("GetInstance service %s", servConf.Protocol)
return nil return nil
} }

View File

@ -1,12 +1,13 @@
package strategies package TCP
import ( import (
"fmt" "fmt"
"github.com/mariocandela/beelzebub/v3/parser"
"github.com/mariocandela/beelzebub/v3/tracer"
"net" "net"
"time" "time"
"github.com/mariocandela/beelzebub/v3/parser"
"github.com/mariocandela/beelzebub/v3/tracer"
"github.com/google/uuid" "github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -14,8 +15,8 @@ import (
type TCPStrategy struct { type TCPStrategy struct {
} }
func (tcpStrategy *TCPStrategy) Init(beelzebubServiceConfiguration parser.BeelzebubServiceConfiguration, tr tracer.Tracer) error { func (tcpStrategy *TCPStrategy) Init(servConf parser.BeelzebubServiceConfiguration, tr tracer.Tracer) error {
listen, err := net.Listen("tcp", beelzebubServiceConfiguration.Address) listen, err := net.Listen("tcp", servConf.Address)
if err != nil { if err != nil {
log.Errorf("Error during init TCP Protocol: %s", err.Error()) log.Errorf("Error during init TCP Protocol: %s", err.Error())
return err return err
@ -25,8 +26,8 @@ func (tcpStrategy *TCPStrategy) Init(beelzebubServiceConfiguration parser.Beelze
for { for {
if conn, err := listen.Accept(); err == nil { if conn, err := listen.Accept(); err == nil {
go func() { go func() {
conn.SetDeadline(time.Now().Add(time.Duration(beelzebubServiceConfiguration.DeadlineTimeoutSeconds) * time.Second)) conn.SetDeadline(time.Now().Add(time.Duration(servConf.DeadlineTimeoutSeconds) * time.Second))
conn.Write([]byte(fmt.Sprintf("%s\n", beelzebubServiceConfiguration.Banner))) conn.Write(fmt.Appendf([]byte{}, "%s\n", servConf.Banner))
buffer := make([]byte, 1024) buffer := make([]byte, 1024)
command := "" command := ""
@ -46,7 +47,7 @@ func (tcpStrategy *TCPStrategy) Init(beelzebubServiceConfiguration parser.Beelze
SourceIp: host, SourceIp: host,
SourcePort: port, SourcePort: port,
ID: uuid.New().String(), ID: uuid.New().String(),
Description: beelzebubServiceConfiguration.Description, Description: servConf.Description,
}) })
conn.Close() conn.Close()
}() }()
@ -55,8 +56,8 @@ func (tcpStrategy *TCPStrategy) Init(beelzebubServiceConfiguration parser.Beelze
}() }()
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"port": beelzebubServiceConfiguration.Address, "port": servConf.Address,
"banner": beelzebubServiceConfiguration.Banner, "banner": servConf.Banner,
}).Infof("Init service %s", beelzebubServiceConfiguration.Protocol) }).Infof("Init service %s", servConf.Protocol)
return nil return nil
} }

View File

@ -1,157 +0,0 @@
package strategies
import (
"fmt"
"io"
"net"
"net/http"
"regexp"
"strings"
"github.com/google/uuid"
"github.com/mariocandela/beelzebub/v3/parser"
"github.com/mariocandela/beelzebub/v3/plugins"
"github.com/mariocandela/beelzebub/v3/tracer"
log "github.com/sirupsen/logrus"
)
type HTTPStrategy struct {
beelzebubServiceConfiguration parser.BeelzebubServiceConfiguration
}
func (httpStrategy HTTPStrategy) Init(beelzebubServiceConfiguration parser.BeelzebubServiceConfiguration, tr tracer.Tracer) error {
httpStrategy.beelzebubServiceConfiguration = beelzebubServiceConfiguration
serverMux := http.NewServeMux()
serverMux.HandleFunc("/", func(responseWriter http.ResponseWriter, request *http.Request) {
traceRequest(request, tr, beelzebubServiceConfiguration.Description)
for _, command := range httpStrategy.beelzebubServiceConfiguration.Commands {
matched, err := regexp.MatchString(command.Regex, request.RequestURI)
if err != nil {
log.Errorf("Error regex: %s, %s", command.Regex, err.Error())
continue
}
if matched {
responseHTTPBody := command.Handler
if command.Plugin == plugins.LLMPluginName {
llmProvider, err := plugins.FromStringToLLMProvider(beelzebubServiceConfiguration.Plugin.LLMProvider)
if err != nil {
log.Errorf("Error: %s", err.Error())
responseHTTPBody = "404 Not Found!"
}
llmHoneypot := plugins.LLMHoneypot{
Histories: make([]plugins.Message, 0),
OpenAIKey: beelzebubServiceConfiguration.Plugin.OpenAISecretKey,
Protocol: tracer.HTTP,
Host: beelzebubServiceConfiguration.Plugin.Host,
Model: beelzebubServiceConfiguration.Plugin.LLMModel,
Provider: llmProvider,
CustomPrompt: beelzebubServiceConfiguration.Plugin.Prompt,
}
llmHoneypotInstance := plugins.InitLLMHoneypot(llmHoneypot)
command := fmt.Sprintf("%s %s", request.Method, request.RequestURI)
if completions, err := llmHoneypotInstance.ExecuteModel(command); err != nil {
log.Errorf("Error ExecuteModel: %s, %s", command, err.Error())
responseHTTPBody = "404 Not Found!"
} else {
responseHTTPBody = completions
}
}
setResponseHeaders(responseWriter, command.Headers, command.StatusCode)
fmt.Fprint(responseWriter, responseHTTPBody)
break
}
}
})
go func() {
var err error
// Launch a TLS supporting server if we are supplied a TLS Key and Certificate.
// If relative paths are supplied, they are relative to the CWD of the binary.
// The can be self-signed, only the client will validate this (or not).
if httpStrategy.beelzebubServiceConfiguration.TLSKeyPath != "" && httpStrategy.beelzebubServiceConfiguration.TLSCertPath != "" {
err = http.ListenAndServeTLS(
httpStrategy.beelzebubServiceConfiguration.Address,
httpStrategy.beelzebubServiceConfiguration.TLSCertPath,
httpStrategy.beelzebubServiceConfiguration.TLSKeyPath,
serverMux)
} else {
err = http.ListenAndServe(httpStrategy.beelzebubServiceConfiguration.Address, serverMux)
}
if err != nil {
log.Errorf("Error during init HTTP Protocol: %s", err.Error())
return
}
}()
log.WithFields(log.Fields{
"port": beelzebubServiceConfiguration.Address,
"commands": len(beelzebubServiceConfiguration.Commands),
}).Infof("Init service: %s", beelzebubServiceConfiguration.Description)
return nil
}
func traceRequest(request *http.Request, tr tracer.Tracer, HoneypotDescription string) {
bodyBytes, err := io.ReadAll(request.Body)
body := ""
if err == nil {
body = string(bodyBytes)
}
host, port, _ := net.SplitHostPort(request.RemoteAddr)
event := tracer.Event{
Msg: "HTTP New request",
RequestURI: request.RequestURI,
Protocol: tracer.HTTP.String(),
HTTPMethod: request.Method,
Body: body,
HostHTTPRequest: request.Host,
UserAgent: request.UserAgent(),
Cookies: mapCookiesToString(request.Cookies()),
Headers: request.Header,
Status: tracer.Stateless.String(),
RemoteAddr: request.RemoteAddr,
SourceIp: host,
SourcePort: port,
ID: uuid.New().String(),
Description: HoneypotDescription,
}
// Capture the TLS details from the request, if provided.
if request.TLS != nil {
event.Msg = "HTTPS New Request"
event.TLSServerName = request.TLS.ServerName
}
tr.TraceEvent(event)
}
func mapCookiesToString(cookies []*http.Cookie) string {
cookiesString := ""
for _, cookie := range cookies {
cookiesString += cookie.String()
}
return cookiesString
}
func setResponseHeaders(responseWriter http.ResponseWriter, headers []string, statusCode int) {
for _, headerStr := range headers {
keyValue := strings.Split(headerStr, ":")
if len(keyValue) > 1 {
responseWriter.Header().Add(keyValue[0], keyValue[1])
}
}
// http.StatusText(statusCode): empty string if the code is unknown.
if len(http.StatusText(statusCode)) > 0 {
responseWriter.WriteHeader(statusCode)
}
}