mirror of
https://github.com/mariocandela/beelzebub.git
synced 2025-07-01 18:47:26 -04:00

* Feat: Add support for logging which "command" was matched for SSH and HTTP strategies. * Feat: Convert to precompiling regexp at config load time. This allows for errors to be presented to the user during startup, and provides better performance for complex regexp. * Feat:Bump Golang version to latest stable 1.24 * Feat: Add a cleanup routine for HistoryStore, default TTL for events is 1 hour since last interaction. * Feat: Add new command line flag "memLimitMiB" with a default value of 100. --------- Signed-off-by: Bryan Nolen <bryan@arc.net.au> Signed-off-by: Mario Candela <mario.candela.personal@gmail.com> Co-authored-by: Mario Candela <mario.candela.personal@gmail.com>
180 lines
5.3 KiB
Go
180 lines
5.3 KiB
Go
package HTTP
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"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) {
|
|
var matched bool
|
|
var resp httpResponse
|
|
var err error
|
|
for _, command := range servConf.Commands {
|
|
var err error
|
|
matched = command.Regex.MatchString(request.RequestURI)
|
|
if matched {
|
|
resp, err = buildHTTPResponse(servConf, tr, 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, tr, 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, tr tracer.Tracer, command parser.Command, request *http.Request) (httpResponse, error) {
|
|
resp := httpResponse{
|
|
Body: command.Handler,
|
|
Headers: command.Headers,
|
|
StatusCode: command.StatusCode,
|
|
}
|
|
traceRequest(request, tr, command, servConf.Description)
|
|
|
|
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, command parser.Command, 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,
|
|
Handler: command.Name,
|
|
}
|
|
// 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)
|
|
}
|
|
}
|