mirror of
https://github.com/mariocandela/beelzebub.git
synced 2025-07-01 18:47:26 -04:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
74cf64f628 | |||
d677cd20b9 | |||
16b012784c | |||
a79937c5ae | |||
bdabbe9adc | |||
db0da03baa | |||
b062416c00 |
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@ -2,13 +2,18 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ "main" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ "main" ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
CI:
|
CI:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
go-version:
|
||||||
|
- "1.24.1"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@ -16,7 +21,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: 1.20.0
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|
||||||
- name: Dependencies
|
- name: Dependencies
|
||||||
run: go mod download
|
run: go mod download
|
||||||
@ -35,12 +40,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Quality Gate - Test coverage shall be above threshold
|
- name: Quality Gate - Test coverage shall be above threshold
|
||||||
env:
|
env:
|
||||||
TESTCOVERAGE_THRESHOLD: 65
|
TESTCOVERAGE_THRESHOLD: 80
|
||||||
run: |
|
run: |
|
||||||
echo "Quality Gate: checking test coverage is above threshold ..."
|
echo "Quality Gate: checking test coverage is above threshold ..."
|
||||||
echo "Threshold : $TESTCOVERAGE_THRESHOLD %"
|
echo "Threshold : $TESTCOVERAGE_THRESHOLD %"
|
||||||
# Excluded the concrete strategy from the unit test coverage, because covered by integration tests
|
# Excluded the concrete strategy from the unit test coverage, because covered by integration tests
|
||||||
cat coverage.tmp.out | grep -v "ssh.go" | grep -v "http.go" | grep -v "tcp.go" > coverage.out
|
cat coverage.tmp.out | grep -v "ssh.go" | grep -v "http.go" | grep -v "tcp.go" | grep -v "builder.go" | grep -v "director.go" > coverage.out
|
||||||
totalCoverage=`go tool cover -func=coverage.out | grep total | grep -Eo '[0-9]+\.[0-9]+'`
|
totalCoverage=`go tool cover -func=coverage.out | grep total | grep -Eo '[0-9]+\.[0-9]+'`
|
||||||
echo "Current test coverage : $totalCoverage %"
|
echo "Current test coverage : $totalCoverage %"
|
||||||
if (( $(echo "$totalCoverage $TESTCOVERAGE_THRESHOLD" | awk '{print ($1 > $2)}') )); then
|
if (( $(echo "$totalCoverage $TESTCOVERAGE_THRESHOLD" | awk '{print ($1 > $2)}') )); then
|
||||||
@ -53,6 +58,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload coverage reports to Codecov
|
- name: Upload coverage reports to Codecov
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: ./coverage.out
|
||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
|
5
.github/workflows/codeql.yml
vendored
5
.github/workflows/codeql.yml
vendored
@ -26,6 +26,11 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: 1.24.1
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,4 +2,5 @@
|
|||||||
.idea
|
.idea
|
||||||
logs
|
logs
|
||||||
.vscode
|
.vscode
|
||||||
.history
|
.history
|
||||||
|
coverage*.out
|
||||||
|
@ -322,4 +322,4 @@ Happy hacking!
|
|||||||
Beelzebub is licensed under the [MIT License](LICENSE).
|
Beelzebub is licensed under the [MIT License](LICENSE).
|
||||||
|
|
||||||
## Supported by JetBrains
|
## Supported by JetBrains
|
||||||
[](https://jb.gg/OpenSourceSupport)
|

|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,5 +22,6 @@ commands:
|
|||||||
plugin: "LLMHoneypot"
|
plugin: "LLMHoneypot"
|
||||||
statusCode: 200
|
statusCode: 200
|
||||||
plugin:
|
plugin:
|
||||||
|
llmProvider: "openai"
|
||||||
llmModel: "gpt-4o"
|
llmModel: "gpt-4o"
|
||||||
openAISecretKey: "sk-proj-123456"
|
openAISecretKey: "sk-proj-123456"
|
||||||
|
12
go.mod
12
go.mod
@ -1,6 +1,8 @@
|
|||||||
module github.com/mariocandela/beelzebub/v3
|
module github.com/mariocandela/beelzebub/v3
|
||||||
|
|
||||||
go 1.20
|
go 1.24
|
||||||
|
|
||||||
|
toolchain go1.24.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gliderlabs/ssh v0.3.8
|
github.com/gliderlabs/ssh v0.3.8
|
||||||
@ -12,7 +14,8 @@ require (
|
|||||||
github.com/rabbitmq/amqp091-go v1.10.0
|
github.com/rabbitmq/amqp091-go v1.10.0
|
||||||
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.35.0
|
||||||
|
golang.org/x/term v0.30.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,8 +34,7 @@ require (
|
|||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.55.0 // indirect
|
github.com/prometheus/common v0.55.0 // indirect
|
||||||
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.36.0 // indirect
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.31.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
|
||||||
)
|
)
|
||||||
|
24
go.sum
24
go.sum
@ -13,6 +13,7 @@ github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1
|
|||||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
|
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
|
||||||
@ -22,10 +23,13 @@ github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ib
|
|||||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
|
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
|
||||||
|
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||||
github.com/melbahja/goph v1.4.0 h1:z0PgDbBFe66lRYl3v5dGb9aFgPy0kotuQ37QOwSQFqs=
|
github.com/melbahja/goph v1.4.0 h1:z0PgDbBFe66lRYl3v5dGb9aFgPy0kotuQ37QOwSQFqs=
|
||||||
github.com/melbahja/goph v1.4.0/go.mod h1:uG+VfK2Dlhk+O32zFrRlc3kYKTlV6+BtvPWd/kK7U68=
|
github.com/melbahja/goph v1.4.0/go.mod h1:uG+VfK2Dlhk+O32zFrRlc3kYKTlV6+BtvPWd/kK7U68=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
@ -47,6 +51,7 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG
|
|||||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@ -55,20 +60,21 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
|||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -80,19 +86,20 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||||
|
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
@ -101,6 +108,7 @@ google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6h
|
|||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
82
historystore/history_store.go
Normal file
82
historystore/history_store.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package historystore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mariocandela/beelzebub/v3/plugins"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
MaxHistoryAge = 60 * time.Minute
|
||||||
|
CleanerInterval = 1 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// HistoryStore is a thread-safe structure for storing Messages used to build LLM Context.
|
||||||
|
type HistoryStore struct {
|
||||||
|
sync.RWMutex
|
||||||
|
sessions map[string]HistoryEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistoryEvent is a container for storing messages
|
||||||
|
type HistoryEvent struct {
|
||||||
|
LastSeen time.Time
|
||||||
|
Messages []plugins.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHistoryStore returns a prepared HistoryStore
|
||||||
|
func NewHistoryStore() *HistoryStore {
|
||||||
|
return &HistoryStore{
|
||||||
|
sessions: make(map[string]HistoryEvent),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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].Messages
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]HistoryEvent)
|
||||||
|
}
|
||||||
|
e, ok := hs.sessions[key]
|
||||||
|
if !ok {
|
||||||
|
e = HistoryEvent{}
|
||||||
|
}
|
||||||
|
e.LastSeen = time.Now()
|
||||||
|
e.Messages = append(e.Messages, message...)
|
||||||
|
hs.sessions[key] = e
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistoryCleaner is a function that will periodically remove records from the HistoryStore
|
||||||
|
// that are older than MaxHistoryAge.
|
||||||
|
func (hs *HistoryStore) HistoryCleaner() {
|
||||||
|
cleanerTicker := time.NewTicker(CleanerInterval)
|
||||||
|
go func() {
|
||||||
|
for range cleanerTicker.C {
|
||||||
|
hs.Lock()
|
||||||
|
for k, v := range hs.sessions {
|
||||||
|
if time.Since(v.LastSeen) > MaxHistoryAge {
|
||||||
|
delete(hs.sessions, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hs.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
66
historystore/history_store_test.go
Normal file
66
historystore/history_store_test.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package historystore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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"] = HistoryEvent{Messages: []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"] = HistoryEvent{Messages: 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"].Messages)
|
||||||
|
hs.Append("testKey", message2)
|
||||||
|
assert.Equal(t, []plugins.Message{message1, message2}, hs.sessions["testKey"].Messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
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"].Messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHistoryCleaner(t *testing.T) {
|
||||||
|
hs := NewHistoryStore()
|
||||||
|
hs.Append("testKey", plugins.Message{Role: "user", Content: "Hello"})
|
||||||
|
hs.Append("testKey2", plugins.Message{Role: "user", Content: "Hello"})
|
||||||
|
|
||||||
|
// Make key older than MaxHistoryAge
|
||||||
|
e := hs.sessions["testKey"]
|
||||||
|
e.LastSeen = time.Now().Add(-MaxHistoryAge * 2)
|
||||||
|
hs.sessions["testKey"] = e
|
||||||
|
|
||||||
|
CleanerInterval = 5 * time.Second // Override for the test.
|
||||||
|
hs.HistoryCleaner()
|
||||||
|
time.Sleep(CleanerInterval + (1 * time.Second))
|
||||||
|
|
||||||
|
assert.False(t, hs.HasKey("testKey"))
|
||||||
|
assert.True(t, hs.HasKey("testKey2"))
|
||||||
|
}
|
10
main.go
10
main.go
@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
"github.com/mariocandela/beelzebub/v3/builder"
|
"github.com/mariocandela/beelzebub/v3/builder"
|
||||||
"github.com/mariocandela/beelzebub/v3/parser"
|
"github.com/mariocandela/beelzebub/v3/parser"
|
||||||
|
|
||||||
@ -13,12 +15,20 @@ func main() {
|
|||||||
quit = make(chan struct{})
|
quit = make(chan struct{})
|
||||||
configurationsCorePath string
|
configurationsCorePath string
|
||||||
configurationsServicesDirectory string
|
configurationsServicesDirectory string
|
||||||
|
memLimitMiB int
|
||||||
)
|
)
|
||||||
|
|
||||||
flag.StringVar(&configurationsCorePath, "confCore", "./configurations/beelzebub.yaml", "Provide the path of configurations core")
|
flag.StringVar(&configurationsCorePath, "confCore", "./configurations/beelzebub.yaml", "Provide the path of configurations core")
|
||||||
flag.StringVar(&configurationsServicesDirectory, "confServices", "./configurations/services/", "Directory config services")
|
flag.StringVar(&configurationsServicesDirectory, "confServices", "./configurations/services/", "Directory config services")
|
||||||
|
flag.IntVar(&memLimitMiB, "memLimitMiB", 100, "Process Memory in MiB (default 100, set to -1 to use system default)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if memLimitMiB > 0 {
|
||||||
|
// SetMemoryLimit takes an int64 value for the number of bytes.
|
||||||
|
// bytes value = MiB value * 1024 * 1024
|
||||||
|
debug.SetMemoryLimit(int64(memLimitMiB * 1024 * 1024))
|
||||||
|
}
|
||||||
|
|
||||||
parser := parser.Init(configurationsCorePath, configurationsServicesDirectory)
|
parser := parser.Init(configurationsCorePath, configurationsServicesDirectory)
|
||||||
|
|
||||||
coreConfigurations, err := parser.ReadConfigurationsCore()
|
coreConfigurations, err := parser.ReadConfigurationsCore()
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@ -62,6 +63,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"`
|
||||||
@ -75,11 +77,13 @@ type BeelzebubServiceConfiguration struct {
|
|||||||
|
|
||||||
// Command is the struct that contains the configurations of the commands
|
// Command is the struct that contains the configurations of the commands
|
||||||
type Command struct {
|
type Command struct {
|
||||||
Regex string `yaml:"regex"`
|
RegexStr string `yaml:"regex"`
|
||||||
Handler string `yaml:"handler"`
|
Regex *regexp.Regexp `yaml:"-"` // This field is parsed, not stored in the config itself.
|
||||||
Headers []string `yaml:"headers"`
|
Handler string `yaml:"handler"`
|
||||||
StatusCode int `yaml:"statusCode"`
|
Headers []string `yaml:"headers"`
|
||||||
Plugin string `yaml:"plugin"`
|
StatusCode int `yaml:"statusCode"`
|
||||||
|
Plugin string `yaml:"plugin"`
|
||||||
|
Name string `yaml:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type configurationsParser struct {
|
type configurationsParser struct {
|
||||||
@ -139,12 +143,29 @@ func (bp configurationsParser) ReadConfigurationsServices() ([]BeelzebubServiceC
|
|||||||
return nil, fmt.Errorf("in file %s: %v", filePath, err)
|
return nil, fmt.Errorf("in file %s: %v", filePath, err)
|
||||||
}
|
}
|
||||||
log.Debug(beelzebubServiceConfiguration)
|
log.Debug(beelzebubServiceConfiguration)
|
||||||
|
if err := beelzebubServiceConfiguration.CompileCommandRegex(); err != nil {
|
||||||
|
return nil, fmt.Errorf("in file %s: invalid regex: %v", filePath, err)
|
||||||
|
}
|
||||||
servicesConfiguration = append(servicesConfiguration, *beelzebubServiceConfiguration)
|
servicesConfiguration = append(servicesConfiguration, *beelzebubServiceConfiguration)
|
||||||
}
|
}
|
||||||
|
|
||||||
return servicesConfiguration, nil
|
return servicesConfiguration, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CompileCommandRegex is the method that compiles the regular expression for each configured Command.
|
||||||
|
func (c *BeelzebubServiceConfiguration) CompileCommandRegex() error {
|
||||||
|
for i, command := range c.Commands {
|
||||||
|
if command.RegexStr != "" {
|
||||||
|
rex, err := regexp.Compile(command.RegexStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Commands[i].Regex = rex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func gelAllFilesNameByDirName(dirName string) ([]string, error) {
|
func gelAllFilesNameByDirName(dirName string) ([]string, error) {
|
||||||
files, err := os.ReadDir(dirName)
|
files, err := os.ReadDir(dirName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -3,6 +3,7 @@ package parser
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -56,6 +57,14 @@ commands:
|
|||||||
handler: "login"
|
handler: "login"
|
||||||
headers:
|
headers:
|
||||||
- "Content-Type: text/html"
|
- "Content-Type: text/html"
|
||||||
|
- name: "wp-admin"
|
||||||
|
regex: "wp-admin"
|
||||||
|
handler: "login"
|
||||||
|
headers:
|
||||||
|
- "Content-Type: text/html"
|
||||||
|
fallbackCommand:
|
||||||
|
handler: "404 Not Found!"
|
||||||
|
statusCode: 404
|
||||||
plugin:
|
plugin:
|
||||||
openAISecretKey: "qwerty"
|
openAISecretKey: "qwerty"
|
||||||
llmModel: "llama3"
|
llmModel: "llama3"
|
||||||
@ -128,12 +137,16 @@ func TestReadConfigurationsServicesValid(t *testing.T) {
|
|||||||
assert.Equal(t, firstBeelzebubServiceConfiguration.Protocol, "http")
|
assert.Equal(t, firstBeelzebubServiceConfiguration.Protocol, "http")
|
||||||
assert.Equal(t, firstBeelzebubServiceConfiguration.ApiVersion, "v1")
|
assert.Equal(t, firstBeelzebubServiceConfiguration.ApiVersion, "v1")
|
||||||
assert.Equal(t, firstBeelzebubServiceConfiguration.Address, ":8080")
|
assert.Equal(t, firstBeelzebubServiceConfiguration.Address, ":8080")
|
||||||
assert.Equal(t, len(firstBeelzebubServiceConfiguration.Commands), 1)
|
assert.Equal(t, len(firstBeelzebubServiceConfiguration.Commands), 2)
|
||||||
assert.Equal(t, len(firstBeelzebubServiceConfiguration.Commands), 1)
|
assert.Equal(t, len(firstBeelzebubServiceConfiguration.Commands), 2)
|
||||||
assert.Equal(t, firstBeelzebubServiceConfiguration.Commands[0].Regex, "wp-admin")
|
assert.Equal(t, firstBeelzebubServiceConfiguration.Commands[0].RegexStr, "wp-admin")
|
||||||
|
assert.Equal(t, firstBeelzebubServiceConfiguration.Commands[0].Regex.String(), "wp-admin")
|
||||||
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.Commands[1].Name, "wp-admin")
|
||||||
|
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 +189,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) {
|
||||||
@ -193,3 +207,79 @@ func TestReadFileBytesByFilePath(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, "", string(bytes))
|
assert.Equal(t, "", string(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompileCommandRegex(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config BeelzebubServiceConfiguration
|
||||||
|
expectedError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid Regex",
|
||||||
|
config: BeelzebubServiceConfiguration{
|
||||||
|
Commands: []Command{
|
||||||
|
{RegexStr: "^/api/v1/.*$"},
|
||||||
|
{RegexStr: "wp-admin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty Regex",
|
||||||
|
config: BeelzebubServiceConfiguration{
|
||||||
|
Commands: []Command{
|
||||||
|
{RegexStr: ""},
|
||||||
|
{RegexStr: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid Regex",
|
||||||
|
config: BeelzebubServiceConfiguration{
|
||||||
|
Commands: []Command{
|
||||||
|
{RegexStr: "["},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mixed valid and Invalid Regex",
|
||||||
|
config: BeelzebubServiceConfiguration{
|
||||||
|
Commands: []Command{
|
||||||
|
{RegexStr: "^/api/v1/.*$"},
|
||||||
|
{RegexStr: "["},
|
||||||
|
{RegexStr: "test"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No commands",
|
||||||
|
config: BeelzebubServiceConfiguration{},
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := tt.config.CompileCommandRegex()
|
||||||
|
|
||||||
|
if tt.expectedError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
for _, command := range tt.config.Commands {
|
||||||
|
if command.RegexStr != "" {
|
||||||
|
assert.NotNil(t, command.Regex)
|
||||||
|
_, err := regexp.Compile(command.RegexStr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
assert.Nil(t, command.Regex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/mariocandela/beelzebub/v3/parser"
|
"github.com/mariocandela/beelzebub/v3/parser"
|
||||||
"github.com/mariocandela/beelzebub/v3/tracer"
|
"github.com/mariocandela/beelzebub/v3/tracer"
|
||||||
@ -91,6 +92,9 @@ func (beelzebubCloud *beelzebubCloud) GetHoneypotsConfigurations() ([]parser.Bee
|
|||||||
if err = yaml.Unmarshal([]byte(honeypotConfig.Config), &honeypotsConfig); err != nil {
|
if err = yaml.Unmarshal([]byte(honeypotConfig.Config), &honeypotsConfig); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := honeypotsConfig.CompileCommandRegex(); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to load service config from cloud: invalid regex: %v", err)
|
||||||
|
}
|
||||||
servicesConfiguration = append(servicesConfiguration, honeypotsConfig)
|
servicesConfiguration = append(servicesConfiguration, honeypotsConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,13 +2,15 @@ package plugins
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/jarcoal/httpmock"
|
"github.com/jarcoal/httpmock"
|
||||||
"github.com/mariocandela/beelzebub/v3/parser"
|
"github.com/mariocandela/beelzebub/v3/parser"
|
||||||
"github.com/mariocandela/beelzebub/v3/tracer"
|
"github.com/mariocandela/beelzebub/v3/tracer"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuildSendEventFailValidation(t *testing.T) {
|
func TestBuildSendEventFailValidation(t *testing.T) {
|
||||||
@ -111,8 +113,9 @@ func TestGetHoneypotsConfigurationsWithResults(t *testing.T) {
|
|||||||
Description: "SSH interactive ChatGPT",
|
Description: "SSH interactive ChatGPT",
|
||||||
Commands: []parser.Command{
|
Commands: []parser.Command{
|
||||||
{
|
{
|
||||||
Regex: "^(.+)$",
|
RegexStr: "^(.+)$",
|
||||||
Plugin: "LLMHoneypot",
|
Regex: regexp.MustCompile("^(.+)$"),
|
||||||
|
Plugin: "LLMHoneypot",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ServerVersion: "OpenSSH",
|
ServerVersion: "OpenSSH",
|
||||||
|
179
protocols/strategies/HTTP/http.go
Normal file
179
protocols/strategies/HTTP/http.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
225
protocols/strategies/SSH/ssh.go
Normal file
225
protocols/strategies/SSH/ssh.go
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
package SSH
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"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/google/uuid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SSHStrategy struct {
|
||||||
|
Sessions *historystore.HistoryStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sshStrategy *SSHStrategy) Init(servConf parser.BeelzebubServiceConfiguration, tr tracer.Tracer) error {
|
||||||
|
if sshStrategy.Sessions == nil {
|
||||||
|
sshStrategy.Sessions = historystore.NewHistoryStore()
|
||||||
|
}
|
||||||
|
go sshStrategy.Sessions.HistoryCleaner()
|
||||||
|
go func() {
|
||||||
|
server := &ssh.Server{
|
||||||
|
Addr: servConf.Address,
|
||||||
|
MaxTimeout: time.Duration(servConf.DeadlineTimeoutSeconds) * time.Second,
|
||||||
|
IdleTimeout: time.Duration(servConf.DeadlineTimeoutSeconds) * time.Second,
|
||||||
|
Version: servConf.ServerVersion,
|
||||||
|
Handler: func(sess ssh.Session) {
|
||||||
|
uuidSession := uuid.New()
|
||||||
|
|
||||||
|
host, port, _ := net.SplitHostPort(sess.RemoteAddr().String())
|
||||||
|
sessionKey := "SSH" + host + sess.User()
|
||||||
|
|
||||||
|
// Inline SSH command
|
||||||
|
if sess.RawCommand() != "" {
|
||||||
|
var histories []plugins.Message
|
||||||
|
if sshStrategy.Sessions.HasKey(sessionKey) {
|
||||||
|
histories = sshStrategy.Sessions.Query(sessionKey)
|
||||||
|
}
|
||||||
|
for _, command := range servConf.Commands {
|
||||||
|
if command.Regex.MatchString(sess.RawCommand()) {
|
||||||
|
commandOutput := command.Handler
|
||||||
|
if command.Plugin == plugins.LLMPluginName {
|
||||||
|
llmProvider, err := plugins.FromStringToLLMProvider(servConf.Plugin.LLMProvider)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error: %s", err.Error())
|
||||||
|
commandOutput = "command not found"
|
||||||
|
llmProvider = plugins.OpenAI
|
||||||
|
}
|
||||||
|
llmHoneypot := plugins.LLMHoneypot{
|
||||||
|
Histories: histories,
|
||||||
|
OpenAIKey: servConf.Plugin.OpenAISecretKey,
|
||||||
|
Protocol: tracer.SSH,
|
||||||
|
Host: servConf.Plugin.Host,
|
||||||
|
Model: servConf.Plugin.LLMModel,
|
||||||
|
Provider: llmProvider,
|
||||||
|
CustomPrompt: servConf.Plugin.Prompt,
|
||||||
|
}
|
||||||
|
llmHoneypotInstance := plugins.InitLLMHoneypot(llmHoneypot)
|
||||||
|
if commandOutput, err = llmHoneypotInstance.ExecuteModel(sess.RawCommand()); err != nil {
|
||||||
|
log.Errorf("error ExecuteModel: %s, %s", sess.RawCommand(), err.Error())
|
||||||
|
commandOutput = "command not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var newEntries []plugins.Message
|
||||||
|
newEntries = append(newEntries, plugins.Message{Role: plugins.USER.String(), Content: sess.RawCommand()})
|
||||||
|
newEntries = append(newEntries, plugins.Message{Role: plugins.ASSISTANT.String(), Content: commandOutput})
|
||||||
|
// Append the new entries to the store.
|
||||||
|
sshStrategy.Sessions.Append(sessionKey, newEntries...)
|
||||||
|
|
||||||
|
sess.Write(append([]byte(commandOutput), '\n'))
|
||||||
|
|
||||||
|
tr.TraceEvent(tracer.Event{
|
||||||
|
Msg: "SSH Raw Command",
|
||||||
|
Protocol: tracer.SSH.String(),
|
||||||
|
RemoteAddr: sess.RemoteAddr().String(),
|
||||||
|
SourceIp: host,
|
||||||
|
SourcePort: port,
|
||||||
|
Status: tracer.Start.String(),
|
||||||
|
ID: uuidSession.String(),
|
||||||
|
Environ: strings.Join(sess.Environ(), ","),
|
||||||
|
User: sess.User(),
|
||||||
|
Description: servConf.Description,
|
||||||
|
Command: sess.RawCommand(),
|
||||||
|
CommandOutput: commandOutput,
|
||||||
|
Handler: command.Name,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.TraceEvent(tracer.Event{
|
||||||
|
Msg: "New SSH Terminal Session",
|
||||||
|
Protocol: tracer.SSH.String(),
|
||||||
|
RemoteAddr: sess.RemoteAddr().String(),
|
||||||
|
SourceIp: host,
|
||||||
|
SourcePort: port,
|
||||||
|
Status: tracer.Start.String(),
|
||||||
|
ID: uuidSession.String(),
|
||||||
|
Environ: strings.Join(sess.Environ(), ","),
|
||||||
|
User: sess.User(),
|
||||||
|
Description: servConf.Description,
|
||||||
|
})
|
||||||
|
|
||||||
|
terminal := term.NewTerminal(sess, buildPrompt(sess.User(), servConf.ServerName))
|
||||||
|
var histories []plugins.Message
|
||||||
|
if sshStrategy.Sessions.HasKey(sessionKey) {
|
||||||
|
histories = sshStrategy.Sessions.Query(sessionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
commandInput, err := terminal.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if commandInput == "exit" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for _, command := range servConf.Commands {
|
||||||
|
if command.Regex.MatchString(commandInput) {
|
||||||
|
commandOutput := command.Handler
|
||||||
|
if command.Plugin == plugins.LLMPluginName {
|
||||||
|
llmProvider, err := plugins.FromStringToLLMProvider(servConf.Plugin.LLMProvider)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error: %s, fallback OpenAI", err.Error())
|
||||||
|
llmProvider = plugins.OpenAI
|
||||||
|
}
|
||||||
|
llmHoneypot := plugins.LLMHoneypot{
|
||||||
|
Histories: histories,
|
||||||
|
OpenAIKey: servConf.Plugin.OpenAISecretKey,
|
||||||
|
Protocol: tracer.SSH,
|
||||||
|
Host: servConf.Plugin.Host,
|
||||||
|
Model: servConf.Plugin.LLMModel,
|
||||||
|
Provider: llmProvider,
|
||||||
|
CustomPrompt: servConf.Plugin.Prompt,
|
||||||
|
}
|
||||||
|
llmHoneypotInstance := plugins.InitLLMHoneypot(llmHoneypot)
|
||||||
|
if commandOutput, err = llmHoneypotInstance.ExecuteModel(commandInput); err != nil {
|
||||||
|
log.Errorf("error ExecuteModel: %s, %s", commandInput, err.Error())
|
||||||
|
commandOutput = "command not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var newEntries []plugins.Message
|
||||||
|
newEntries = append(newEntries, plugins.Message{Role: plugins.USER.String(), Content: commandInput})
|
||||||
|
newEntries = append(newEntries, plugins.Message{Role: plugins.ASSISTANT.String(), Content: commandOutput})
|
||||||
|
// Stash the new entries to the store, and update the history for this running session.
|
||||||
|
sshStrategy.Sessions.Append(sessionKey, newEntries...)
|
||||||
|
histories = append(histories, newEntries...)
|
||||||
|
|
||||||
|
terminal.Write(append([]byte(commandOutput), '\n'))
|
||||||
|
|
||||||
|
tr.TraceEvent(tracer.Event{
|
||||||
|
Msg: "SSH Terminal Session Interaction",
|
||||||
|
RemoteAddr: sess.RemoteAddr().String(),
|
||||||
|
SourceIp: host,
|
||||||
|
SourcePort: port,
|
||||||
|
Status: tracer.Interaction.String(),
|
||||||
|
Command: commandInput,
|
||||||
|
CommandOutput: commandOutput,
|
||||||
|
ID: uuidSession.String(),
|
||||||
|
Protocol: tracer.SSH.String(),
|
||||||
|
Description: servConf.Description,
|
||||||
|
Handler: command.Name,
|
||||||
|
})
|
||||||
|
break // Inner range over commands.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.TraceEvent(tracer.Event{
|
||||||
|
Msg: "End SSH Session",
|
||||||
|
Status: tracer.End.String(),
|
||||||
|
ID: uuidSession.String(),
|
||||||
|
Protocol: tracer.SSH.String(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
PasswordHandler: func(ctx ssh.Context, password string) bool {
|
||||||
|
host, port, _ := net.SplitHostPort(ctx.RemoteAddr().String())
|
||||||
|
|
||||||
|
tr.TraceEvent(tracer.Event{
|
||||||
|
Msg: "New SSH Login Attempt",
|
||||||
|
Protocol: tracer.SSH.String(),
|
||||||
|
Status: tracer.Stateless.String(),
|
||||||
|
User: ctx.User(),
|
||||||
|
Password: password,
|
||||||
|
Client: ctx.ClientVersion(),
|
||||||
|
RemoteAddr: ctx.RemoteAddr().String(),
|
||||||
|
SourceIp: host,
|
||||||
|
SourcePort: port,
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
Description: servConf.Description,
|
||||||
|
})
|
||||||
|
matched, err := regexp.MatchString(servConf.PasswordRegex, password)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error regex: %s, %s", servConf.PasswordRegex, err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return matched
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := server.ListenAndServe()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error during init SSH Protocol: %s", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"port": servConf.Address,
|
||||||
|
"commands": len(servConf.Commands),
|
||||||
|
}).Infof("GetInstance service %s", servConf.Protocol)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPrompt(user string, serverName string) string {
|
||||||
|
return fmt.Sprintf("%s@%s:~$ ", user, serverName)
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,245 +0,0 @@
|
|||||||
package strategies
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/mariocandela/beelzebub/v3/parser"
|
|
||||||
"github.com/mariocandela/beelzebub/v3/plugins"
|
|
||||||
"github.com/mariocandela/beelzebub/v3/tracer"
|
|
||||||
"net"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gliderlabs/ssh"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SSHStrategy struct {
|
|
||||||
Sessions map[string][]plugins.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sshStrategy *SSHStrategy) Init(beelzebubServiceConfiguration parser.BeelzebubServiceConfiguration, tr tracer.Tracer) error {
|
|
||||||
sshStrategy.Sessions = make(map[string][]plugins.Message)
|
|
||||||
go func() {
|
|
||||||
server := &ssh.Server{
|
|
||||||
Addr: beelzebubServiceConfiguration.Address,
|
|
||||||
MaxTimeout: time.Duration(beelzebubServiceConfiguration.DeadlineTimeoutSeconds) * time.Second,
|
|
||||||
IdleTimeout: time.Duration(beelzebubServiceConfiguration.DeadlineTimeoutSeconds) * time.Second,
|
|
||||||
Version: beelzebubServiceConfiguration.ServerVersion,
|
|
||||||
Handler: func(sess ssh.Session) {
|
|
||||||
uuidSession := uuid.New()
|
|
||||||
|
|
||||||
host, port, _ := net.SplitHostPort(sess.RemoteAddr().String())
|
|
||||||
sessionKey := host + sess.User()
|
|
||||||
|
|
||||||
// Inline SSH command
|
|
||||||
if sess.RawCommand() != "" {
|
|
||||||
for _, command := range beelzebubServiceConfiguration.Commands {
|
|
||||||
matched, err := regexp.MatchString(command.Regex, sess.RawCommand())
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error regex: %s, %s", command.Regex, err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if matched {
|
|
||||||
commandOutput := command.Handler
|
|
||||||
|
|
||||||
if command.Plugin == plugins.LLMPluginName {
|
|
||||||
|
|
||||||
llmProvider, err := plugins.FromStringToLLMProvider(beelzebubServiceConfiguration.Plugin.LLMProvider)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error: %s", err.Error())
|
|
||||||
commandOutput = "command not found"
|
|
||||||
llmProvider = plugins.OpenAI
|
|
||||||
}
|
|
||||||
|
|
||||||
histories := make([]plugins.Message, 0)
|
|
||||||
|
|
||||||
if sshStrategy.Sessions[sessionKey] != nil {
|
|
||||||
histories = sshStrategy.Sessions[sessionKey]
|
|
||||||
}
|
|
||||||
|
|
||||||
llmHoneypot := plugins.LLMHoneypot{
|
|
||||||
Histories: histories,
|
|
||||||
OpenAIKey: beelzebubServiceConfiguration.Plugin.OpenAISecretKey,
|
|
||||||
Protocol: tracer.SSH,
|
|
||||||
Host: beelzebubServiceConfiguration.Plugin.Host,
|
|
||||||
Model: beelzebubServiceConfiguration.Plugin.LLMModel,
|
|
||||||
Provider: llmProvider,
|
|
||||||
CustomPrompt: beelzebubServiceConfiguration.Plugin.Prompt,
|
|
||||||
}
|
|
||||||
|
|
||||||
llmHoneypotInstance := plugins.InitLLMHoneypot(llmHoneypot)
|
|
||||||
|
|
||||||
if commandOutput, err = llmHoneypotInstance.ExecuteModel(sess.RawCommand()); err != nil {
|
|
||||||
log.Errorf("Error ExecuteModel: %s, %s", sess.RawCommand(), err.Error())
|
|
||||||
commandOutput = "command not found"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sess.Write(append([]byte(commandOutput), '\n'))
|
|
||||||
|
|
||||||
tr.TraceEvent(tracer.Event{
|
|
||||||
Msg: "New SSH Session",
|
|
||||||
Protocol: tracer.SSH.String(),
|
|
||||||
RemoteAddr: sess.RemoteAddr().String(),
|
|
||||||
SourceIp: host,
|
|
||||||
SourcePort: port,
|
|
||||||
Status: tracer.Start.String(),
|
|
||||||
ID: uuidSession.String(),
|
|
||||||
Environ: strings.Join(sess.Environ(), ","),
|
|
||||||
User: sess.User(),
|
|
||||||
Description: beelzebubServiceConfiguration.Description,
|
|
||||||
Command: sess.RawCommand(),
|
|
||||||
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{
|
|
||||||
Msg: "End SSH Session",
|
|
||||||
Status: tracer.End.String(),
|
|
||||||
ID: uuidSession.String(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.TraceEvent(tracer.Event{
|
|
||||||
Msg: "New SSH Session",
|
|
||||||
Protocol: tracer.SSH.String(),
|
|
||||||
RemoteAddr: sess.RemoteAddr().String(),
|
|
||||||
SourceIp: host,
|
|
||||||
SourcePort: port,
|
|
||||||
Status: tracer.Start.String(),
|
|
||||||
ID: uuidSession.String(),
|
|
||||||
Environ: strings.Join(sess.Environ(), ","),
|
|
||||||
User: sess.User(),
|
|
||||||
Description: beelzebubServiceConfiguration.Description,
|
|
||||||
})
|
|
||||||
|
|
||||||
terminal := term.NewTerminal(sess, buildPrompt(sess.User(), beelzebubServiceConfiguration.ServerName))
|
|
||||||
var histories []plugins.Message
|
|
||||||
if sshStrategy.Sessions[sessionKey] != nil {
|
|
||||||
histories = sshStrategy.Sessions[sessionKey]
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
commandInput, err := terminal.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if commandInput == "exit" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for _, command := range beelzebubServiceConfiguration.Commands {
|
|
||||||
matched, err := regexp.MatchString(command.Regex, commandInput)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error regex: %s, %s", command.Regex, err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if matched {
|
|
||||||
commandOutput := command.Handler
|
|
||||||
|
|
||||||
if command.Plugin == plugins.LLMPluginName {
|
|
||||||
|
|
||||||
llmProvider, err := plugins.FromStringToLLMProvider(beelzebubServiceConfiguration.Plugin.LLMProvider)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error: %s, fallback OpenAI", err.Error())
|
|
||||||
llmProvider = plugins.OpenAI
|
|
||||||
}
|
|
||||||
|
|
||||||
llmHoneypot := plugins.LLMHoneypot{
|
|
||||||
Histories: histories,
|
|
||||||
OpenAIKey: beelzebubServiceConfiguration.Plugin.OpenAISecretKey,
|
|
||||||
Protocol: tracer.SSH,
|
|
||||||
Host: beelzebubServiceConfiguration.Plugin.Host,
|
|
||||||
Model: beelzebubServiceConfiguration.Plugin.LLMModel,
|
|
||||||
Provider: llmProvider,
|
|
||||||
CustomPrompt: beelzebubServiceConfiguration.Plugin.Prompt,
|
|
||||||
}
|
|
||||||
|
|
||||||
llmHoneypotInstance := plugins.InitLLMHoneypot(llmHoneypot)
|
|
||||||
|
|
||||||
if commandOutput, err = llmHoneypotInstance.ExecuteModel(commandInput); err != nil {
|
|
||||||
log.Errorf("Error ExecuteModel: %s, %s", commandInput, err.Error())
|
|
||||||
commandOutput = "command not found"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
histories = append(histories, plugins.Message{Role: plugins.USER.String(), Content: commandInput})
|
|
||||||
histories = append(histories, plugins.Message{Role: plugins.ASSISTANT.String(), Content: commandOutput})
|
|
||||||
|
|
||||||
terminal.Write(append([]byte(commandOutput), '\n'))
|
|
||||||
|
|
||||||
tr.TraceEvent(tracer.Event{
|
|
||||||
Msg: "New SSH Terminal Session",
|
|
||||||
RemoteAddr: sess.RemoteAddr().String(),
|
|
||||||
SourceIp: host,
|
|
||||||
SourcePort: port,
|
|
||||||
Status: tracer.Interaction.String(),
|
|
||||||
Command: commandInput,
|
|
||||||
CommandOutput: commandOutput,
|
|
||||||
ID: uuidSession.String(),
|
|
||||||
Protocol: tracer.SSH.String(),
|
|
||||||
Description: beelzebubServiceConfiguration.Description,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sshStrategy.Sessions[sessionKey] = histories
|
|
||||||
tr.TraceEvent(tracer.Event{
|
|
||||||
Msg: "End SSH Session",
|
|
||||||
Status: tracer.End.String(),
|
|
||||||
ID: uuidSession.String(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
PasswordHandler: func(ctx ssh.Context, password string) bool {
|
|
||||||
host, port, _ := net.SplitHostPort(ctx.RemoteAddr().String())
|
|
||||||
|
|
||||||
tr.TraceEvent(tracer.Event{
|
|
||||||
Msg: "New SSH attempt",
|
|
||||||
Protocol: tracer.SSH.String(),
|
|
||||||
Status: tracer.Stateless.String(),
|
|
||||||
User: ctx.User(),
|
|
||||||
Password: password,
|
|
||||||
Client: ctx.ClientVersion(),
|
|
||||||
RemoteAddr: ctx.RemoteAddr().String(),
|
|
||||||
SourceIp: host,
|
|
||||||
SourcePort: port,
|
|
||||||
ID: uuid.New().String(),
|
|
||||||
Description: beelzebubServiceConfiguration.Description,
|
|
||||||
})
|
|
||||||
matched, err := regexp.MatchString(beelzebubServiceConfiguration.PasswordRegex, password)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error regex: %s, %s", beelzebubServiceConfiguration.PasswordRegex, err.Error())
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return matched
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := server.ListenAndServe()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error during init SSH Protocol: %s", err.Error())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"port": beelzebubServiceConfiguration.Address,
|
|
||||||
"commands": len(beelzebubServiceConfiguration.Commands),
|
|
||||||
}).Infof("GetInstance service %s", beelzebubServiceConfiguration.Protocol)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildPrompt(user string, serverName string) string {
|
|
||||||
return fmt.Sprintf("%s@%s:~$ ", user, serverName)
|
|
||||||
}
|
|
@ -5,10 +5,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Workers is the number of workers that will
|
// Workers is the number of workers that will
|
||||||
@ -38,6 +37,7 @@ type Event struct {
|
|||||||
SourceIp string
|
SourceIp string
|
||||||
SourcePort string
|
SourcePort string
|
||||||
TLSServerName string
|
TLSServerName string
|
||||||
|
Handler string
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
Reference in New Issue
Block a user