Compare commits

...

7 Commits

Author SHA1 Message Date
933f02911b feat: Improve SSH LLM honeypot, preserve session after attacker logout (#179)
* Migrate from deprecated library "golang.org/x/crypto/ssh/terminal" to "golang.org/x/term"

* Feat: Inject OpenAI secret key from environment variable

* Feat: Add test for OpenAI secret key injection from environment variable

* Fix: Correct llmModel value in http-80.yaml configuration

* Feat: Add OPEN_AI_SECRET_KEY environment variable to docker-compose.yml

* Feat: Implement session management for SSHStrategy with command history
2025-03-09 13:17:04 +01:00
ef07ca1203 Feat: continuous delivery pipeline add latest tag (#174)
Feat: continuous delivery pipeline add latest tag

Signed-off-by: James Hodgkinson <james@terminaloutcomes.com>
2025-03-02 05:30:36 +01:00
1f59685530 Feat: Improve HTTP Headers serializer json log #172 (#173)
* Changed Event struct, field headers from string to map[string][]string

* Add integration test for http Headers
2025-03-01 12:31:34 +01:00
f658a26b32 Feat: Update docker-image.yml to add multi-platform support (#171)
* Update docker-image.yml

Adds multi-arch support

Signed-off-by: James Hodgkinson <james@terminaloutcomes.com>
Co-authored-by: Mario Candela <mario.candela.personal@gmail.com>
2025-02-28 11:36:15 +01:00
3fb8a667b3 Update codeql.yml
Upgrade codeQL from v2 to v3

Signed-off-by: Mario Candela <mario.candela.personal@gmail.com>
2025-02-24 08:16:34 +01:00
8963bbc86d Fix: mapping LLMModel for SSH inline, removed old comments on docker-c… (#168)
Fix mapping LLMModel for SSH inline, removed old comments on docker-compose.yml
2025-02-20 22:41:28 +01:00
44ec44ea5c Fix LLM model name typo 2025-02-20 18:20:17 +01:00
13 changed files with 83 additions and 48 deletions

View File

@ -27,7 +27,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
@ -35,6 +35,6 @@ jobs:
run: go build ./... run: go build ./...
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@ -1,31 +1,32 @@
---
name: Docker Hub Image name: Docker Hub Image
on: on:
push: push:
tags: tags:
- 'v*.*.*' - 'v*.*.*'
jobs: jobs:
CD: CD:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: Checkout
name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- - name: Login to Docker Hub
name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }} password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
- - name: Set up QEMU
name: Set up Docker Buildx uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- - name: Build and push
name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: true push: true
tags: m4r10/beelzebub:${{ github.ref_name }} tags: |
m4r10/beelzebub:${{ github.ref_name }}
m4r10/beelzebub:latest
platforms: linux/amd64,linux/arm64

View File

@ -229,7 +229,7 @@ passwordRegex: "^(root|qwerty|Smoker666|123456|jenkins|minecraft|sinus|alex|post
deadlineTimeoutSeconds: 60 deadlineTimeoutSeconds: 60
plugin: plugin:
llmProvider: "openai" llmProvider: "openai"
llmModel: "gpt4-o" #Models https://platform.openai.com/docs/models llmModel: "gpt-4o" #Models https://platform.openai.com/docs/models
openAISecretKey: "sk-proj-123456" openAISecretKey: "sk-proj-123456"
``` ```
@ -268,7 +268,7 @@ passwordRegex: "^(root|qwerty|Smoker666|123456|jenkins|minecraft|sinus|alex|post
deadlineTimeoutSeconds: 60 deadlineTimeoutSeconds: 60
plugin: plugin:
llmProvider: "openai" llmProvider: "openai"
llmModel: "gpt4-o" llmModel: "gpt-4o"
openAISecretKey: "sk-proj-123456" openAISecretKey: "sk-proj-123456"
prompt: "You will act as an Ubuntu Linux terminal. The user will type commands, and you are to reply with what the terminal should show. Your responses must be contained within a single code block." prompt: "You will act as an Ubuntu Linux terminal. The user will type commands, and you are to reply with what the terminal should show. Your responses must be contained within a single code block."
``` ```

View File

@ -22,5 +22,5 @@ commands:
plugin: "LLMHoneypot" plugin: "LLMHoneypot"
statusCode: 200 statusCode: 200
plugin: plugin:
llmModel: "gpt4-o" llmModel: "gpt-4o"
openAISecretKey: "sk-proj-123456" openAISecretKey: "sk-proj-123456"

View File

@ -11,5 +11,5 @@ passwordRegex: "^(root|qwerty|Smoker666|123456|jenkins|minecraft|sinus|alex|post
deadlineTimeoutSeconds: 6000 deadlineTimeoutSeconds: 6000
plugin: plugin:
llmProvider: "openai" llmProvider: "openai"
llmModel: "gpt4-o" llmModel: "gpt-4o"
openAISecretKey: "sk-proj-12345" openAISecretKey: "sk-proj-12345"

View File

@ -3,18 +3,18 @@ version: "3.9"
services: services:
beelzebub: beelzebub:
build: . build: .
#network_mode: host # Not work on Mac OS
container_name: beelzebub container_name: beelzebub
restart: always restart: always
ports: # Remove me, if you use configuration network_mode: host ports:
- "22:22" - "22:22"
- "2222:2222" - "2222:2222"
- "8080:8080" - "8080:8080"
- "8081:8081" - "8081:8081"
- "80:80" - "80:80"
- "3306:3306" - "3306:3306"
- "2112:2112" # Prometheus openmetrics - "2112:2112" #Prometheus Open Metrics
environment: environment:
RABBITMQ_URI: ${RABBITMQ_URI} RABBITMQ_URI: ${RABBITMQ_URI}
OPEN_AI_SECRET_KEY: ${OPEN_AI_SECRET_KEY}
volumes: volumes:
- "./configurations:/configurations" - "./configurations:/configurations"

View File

@ -67,8 +67,11 @@ func (suite *IntegrationTestSuite) TestInvokeHTTPHoneypot() {
response, err := resty.New().R(). response, err := resty.New().R().
Get(suite.httpHoneypotHost + "/index.php") Get(suite.httpHoneypotHost + "/index.php")
response.Header().Del("Date")
suite.Require().NoError(err) suite.Require().NoError(err)
suite.Equal(http.StatusOK, response.StatusCode()) suite.Equal(http.StatusOK, response.StatusCode())
suite.Equal(http.Header{"Content-Length": []string{"15"}, "Content-Type": []string{"text/html"}, "Server": []string{"Apache/2.4.53 (Debian)"}, "X-Powered-By": []string{"PHP/7.4.29"}}, response.Header())
suite.Equal("mocked response", string(response.Body())) suite.Equal("mocked response", string(response.Body()))
response, err = resty.New().R(). response, err = resty.New().R().

View File

@ -85,7 +85,7 @@ func TestGetHoneypotsConfigurationsWithResults(t *testing.T) {
resp, err := httpmock.NewJsonResponse(200, &[]HoneypotConfigResponseDTO{ resp, err := httpmock.NewJsonResponse(200, &[]HoneypotConfigResponseDTO{
{ {
ID: "123456", 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", 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: \"gpt-4o\"\n openAISecretKey: \"1234\"\n",
TokenID: "1234567", TokenID: "1234567",
}, },
}) })
@ -120,7 +120,7 @@ func TestGetHoneypotsConfigurationsWithResults(t *testing.T) {
PasswordRegex: "^(root|qwerty|Smoker666|123456|jenkins|minecraft|sinus|alex|postgres|Ly123456)$", PasswordRegex: "^(root|qwerty|Smoker666|123456|jenkins|minecraft|sinus|alex|postgres|Ly123456)$",
DeadlineTimeoutSeconds: 60, DeadlineTimeoutSeconds: 60,
Plugin: parser.Plugin{ Plugin: parser.Plugin{
LLMModel: "gpt4-o", LLMModel: "gpt-4o",
OpenAISecretKey: "1234", OpenAISecretKey: "1234",
}, },
}, },

View File

@ -7,6 +7,7 @@ import (
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/mariocandela/beelzebub/v3/tracer" "github.com/mariocandela/beelzebub/v3/tracer"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"os"
"regexp" "regexp"
"strings" "strings"
) )
@ -95,6 +96,10 @@ func InitLLMHoneypot(config LLMHoneypot) *LLMHoneypot {
// Inject the dependencies // Inject the dependencies
config.client = resty.New() config.client = resty.New()
if os.Getenv("OPEN_AI_SECRET_KEY") != "" {
config.OpenAIKey = os.Getenv("OPEN_AI_SECRET_KEY")
}
return &config return &config
} }

View File

@ -6,6 +6,7 @@ import (
"github.com/mariocandela/beelzebub/v3/tracer" "github.com/mariocandela/beelzebub/v3/tracer"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"net/http" "net/http"
"os"
"testing" "testing"
) )
@ -85,7 +86,7 @@ func TestBuildExecuteModelFailValidation(t *testing.T) {
Histories: make([]Message, 0), Histories: make([]Message, 0),
OpenAIKey: "", OpenAIKey: "",
Protocol: tracer.SSH, Protocol: tracer.SSH,
Model: "gpt4-o", Model: "gpt-4o",
Provider: OpenAI, Provider: OpenAI,
} }
@ -96,6 +97,24 @@ func TestBuildExecuteModelFailValidation(t *testing.T) {
assert.Equal(t, "openAIKey is empty", err.Error()) assert.Equal(t, "openAIKey is empty", err.Error())
} }
func TestBuildExecuteModelOpenAISecretKeyFromEnv(t *testing.T) {
llmHoneypot := LLMHoneypot{
Histories: make([]Message, 0),
OpenAIKey: "",
Protocol: tracer.SSH,
Model: "gpt-4o",
Provider: OpenAI,
}
os.Setenv("OPEN_AI_SECRET_KEY", "sdjdnklfjndslkjanfk")
openAIGPTVirtualTerminal := InitLLMHoneypot(llmHoneypot)
assert.Equal(t, "sdjdnklfjndslkjanfk", openAIGPTVirtualTerminal.OpenAIKey)
}
func TestBuildExecuteModelWithCustomPrompt(t *testing.T) { func TestBuildExecuteModelWithCustomPrompt(t *testing.T) {
client := resty.New() client := resty.New()
httpmock.ActivateNonDefault(client.GetClient()) httpmock.ActivateNonDefault(client.GetClient())
@ -126,7 +145,7 @@ func TestBuildExecuteModelWithCustomPrompt(t *testing.T) {
Histories: make([]Message, 0), Histories: make([]Message, 0),
OpenAIKey: "sdjdnklfjndslkjanfk", OpenAIKey: "sdjdnklfjndslkjanfk",
Protocol: tracer.HTTP, Protocol: tracer.HTTP,
Model: "gpt4-o", Model: "gpt-4o",
Provider: OpenAI, Provider: OpenAI,
CustomPrompt: "hello world", CustomPrompt: "hello world",
} }
@ -148,7 +167,7 @@ func TestBuildExecuteModelFailValidationStrategyType(t *testing.T) {
Histories: make([]Message, 0), Histories: make([]Message, 0),
OpenAIKey: "", OpenAIKey: "",
Protocol: tracer.TCP, Protocol: tracer.TCP,
Model: "gpt4-o", Model: "gpt-4o",
Provider: OpenAI, Provider: OpenAI,
} }
@ -206,7 +225,7 @@ func TestBuildExecuteModelSSHWithResultsOpenAI(t *testing.T) {
Histories: make([]Message, 0), Histories: make([]Message, 0),
OpenAIKey: "sdjdnklfjndslkjanfk", OpenAIKey: "sdjdnklfjndslkjanfk",
Protocol: tracer.SSH, Protocol: tracer.SSH,
Model: "gpt4-o", Model: "gpt-4o",
Provider: OpenAI, Provider: OpenAI,
} }
@ -282,7 +301,7 @@ func TestBuildExecuteModelSSHWithoutResults(t *testing.T) {
Histories: make([]Message, 0), Histories: make([]Message, 0),
OpenAIKey: "sdjdnklfjndslkjanfk", OpenAIKey: "sdjdnklfjndslkjanfk",
Protocol: tracer.SSH, Protocol: tracer.SSH,
Model: "gpt4-o", Model: "gpt-4o",
Provider: OpenAI, Provider: OpenAI,
} }
@ -325,7 +344,7 @@ func TestBuildExecuteModelHTTPWithResults(t *testing.T) {
Histories: make([]Message, 0), Histories: make([]Message, 0),
OpenAIKey: "sdjdnklfjndslkjanfk", OpenAIKey: "sdjdnklfjndslkjanfk",
Protocol: tracer.HTTP, Protocol: tracer.HTTP,
Model: "gpt4-o", Model: "gpt-4o",
Provider: OpenAI, Provider: OpenAI,
} }
@ -362,7 +381,7 @@ func TestBuildExecuteModelHTTPWithoutResults(t *testing.T) {
Histories: make([]Message, 0), Histories: make([]Message, 0),
OpenAIKey: "sdjdnklfjndslkjanfk", OpenAIKey: "sdjdnklfjndslkjanfk",
Protocol: tracer.HTTP, Protocol: tracer.HTTP,
Model: "gpt4-o", Model: "gpt-4o",
Provider: OpenAI, Provider: OpenAI,
} }

View File

@ -117,7 +117,7 @@ func traceRequest(request *http.Request, tr tracer.Tracer, HoneypotDescription s
HostHTTPRequest: request.Host, HostHTTPRequest: request.Host,
UserAgent: request.UserAgent(), UserAgent: request.UserAgent(),
Cookies: mapCookiesToString(request.Cookies()), Cookies: mapCookiesToString(request.Cookies()),
Headers: mapHeaderToString(request.Header), Headers: request.Header,
Status: tracer.Stateless.String(), Status: tracer.Stateless.String(),
RemoteAddr: request.RemoteAddr, RemoteAddr: request.RemoteAddr,
SourceIp: host, SourceIp: host,
@ -133,18 +133,6 @@ func traceRequest(request *http.Request, tr tracer.Tracer, HoneypotDescription s
tr.TraceEvent(event) tr.TraceEvent(event)
} }
func mapHeaderToString(headers http.Header) string {
headersString := ""
for key := range headers {
for _, values := range headers[key] {
headersString += fmt.Sprintf("[Key: %s, values: %s],", key, values)
}
}
return headersString
}
func mapCookiesToString(cookies []*http.Cookie) string { func mapCookiesToString(cookies []*http.Cookie) string {
cookiesString := "" cookiesString := ""

View File

@ -13,13 +13,15 @@ import (
"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"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/term"
) )
type SSHStrategy struct { type SSHStrategy struct {
Sessions map[string][]plugins.Message
} }
func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.BeelzebubServiceConfiguration, tr tracer.Tracer) error { func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.BeelzebubServiceConfiguration, tr tracer.Tracer) error {
sshStrategy.Sessions = make(map[string][]plugins.Message)
go func() { go func() {
server := &ssh.Server{ server := &ssh.Server{
Addr: beelzebubServiceConfiguration.Address, Addr: beelzebubServiceConfiguration.Address,
@ -30,7 +32,9 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
uuidSession := uuid.New() uuidSession := uuid.New()
host, port, _ := net.SplitHostPort(sess.RemoteAddr().String()) host, port, _ := net.SplitHostPort(sess.RemoteAddr().String())
sessionKey := host + sess.User()
// Inline SSH command
if sess.RawCommand() != "" { if sess.RawCommand() != "" {
for _, command := range beelzebubServiceConfiguration.Commands { for _, command := range beelzebubServiceConfiguration.Commands {
matched, err := regexp.MatchString(command.Regex, sess.RawCommand()) matched, err := regexp.MatchString(command.Regex, sess.RawCommand())
@ -52,12 +56,18 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
llmProvider = plugins.OpenAI llmProvider = plugins.OpenAI
} }
histories := make([]plugins.Message, 0)
if sshStrategy.Sessions[sessionKey] != nil {
histories = sshStrategy.Sessions[sessionKey]
}
llmHoneypot := plugins.LLMHoneypot{ llmHoneypot := plugins.LLMHoneypot{
Histories: make([]plugins.Message, 0), Histories: histories,
OpenAIKey: beelzebubServiceConfiguration.Plugin.OpenAISecretKey, OpenAIKey: beelzebubServiceConfiguration.Plugin.OpenAISecretKey,
Protocol: tracer.SSH, Protocol: tracer.SSH,
Host: beelzebubServiceConfiguration.Plugin.Host, Host: beelzebubServiceConfiguration.Plugin.Host,
Model: beelzebubServiceConfiguration.Plugin.LLMProvider, Model: beelzebubServiceConfiguration.Plugin.LLMModel,
Provider: llmProvider, Provider: llmProvider,
CustomPrompt: beelzebubServiceConfiguration.Plugin.Prompt, CustomPrompt: beelzebubServiceConfiguration.Plugin.Prompt,
} }
@ -86,6 +96,10 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
Command: sess.RawCommand(), Command: sess.RawCommand(),
CommandOutput: commandOutput, CommandOutput: commandOutput,
}) })
var histories []plugins.Message
histories = append(histories, plugins.Message{Role: plugins.USER.String(), Content: sess.RawCommand()})
histories = append(histories, plugins.Message{Role: plugins.ASSISTANT.String(), Content: commandOutput})
sshStrategy.Sessions[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(),
@ -109,10 +123,14 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
Description: beelzebubServiceConfiguration.Description, Description: beelzebubServiceConfiguration.Description,
}) })
term := terminal.NewTerminal(sess, buildPrompt(sess.User(), beelzebubServiceConfiguration.ServerName)) terminal := term.NewTerminal(sess, buildPrompt(sess.User(), beelzebubServiceConfiguration.ServerName))
var histories []plugins.Message var histories []plugins.Message
if sshStrategy.Sessions[sessionKey] != nil {
histories = sshStrategy.Sessions[sessionKey]
}
for { for {
commandInput, err := term.ReadLine() commandInput, err := terminal.ReadLine()
if err != nil { if err != nil {
break break
} }
@ -160,7 +178,7 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
histories = append(histories, plugins.Message{Role: plugins.USER.String(), Content: commandInput}) histories = append(histories, plugins.Message{Role: plugins.USER.String(), Content: commandInput})
histories = append(histories, plugins.Message{Role: plugins.ASSISTANT.String(), Content: commandOutput}) histories = append(histories, plugins.Message{Role: plugins.ASSISTANT.String(), Content: commandOutput})
term.Write(append([]byte(commandOutput), '\n')) terminal.Write(append([]byte(commandOutput), '\n'))
tr.TraceEvent(tracer.Event{ tr.TraceEvent(tracer.Event{
Msg: "New SSH Terminal Session", Msg: "New SSH Terminal Session",
@ -178,6 +196,7 @@ func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.Beelze
} }
} }
} }
sshStrategy.Sessions[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(),

View File

@ -27,7 +27,7 @@ type Event struct {
User string User string
Password string Password string
Client string Client string
Headers string Headers map[string][]string
Cookies string Cookies string
UserAgent string UserAgent string
HostHTTPRequest string HostHTTPRequest string