Add 6 new Prometheus metrics for richer observability: - auth_attempts_by_country_total (counter by country) - commands_executed_total (counter by shell via OnCommand callback) - human_score (histogram of final detection scores) - storage_login_attempts_total, storage_unique_ips, storage_sessions_total (gauges via custom collector querying GetDashboardStats on each scrape) Add optional bearer token authentication for the /metrics endpoint via web.metrics_token config option. Uses crypto/subtle.ConstantTimeCompare. Empty token (default) means no auth for backwards compatibility. Also adds "cisco" to pre-initialized session/command metric labels. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
109 lines
2.3 KiB
Go
109 lines
2.3 KiB
Go
package bash
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.t-juice.club/torjus/oubliette/internal/shell"
|
|
)
|
|
|
|
const sessionTimeout = 5 * time.Minute
|
|
|
|
// BashShell emulates a basic bash-like shell.
|
|
type BashShell struct{}
|
|
|
|
// NewBashShell returns a new BashShell instance.
|
|
func NewBashShell() *BashShell {
|
|
return &BashShell{}
|
|
}
|
|
|
|
func (b *BashShell) Name() string { return "bash" }
|
|
func (b *BashShell) Description() string { return "Basic bash-like shell emulator" }
|
|
|
|
func (b *BashShell) Handle(ctx context.Context, sess *shell.SessionContext, rw io.ReadWriteCloser) error {
|
|
ctx, cancel := context.WithTimeout(ctx, sessionTimeout)
|
|
defer cancel()
|
|
|
|
username := sess.Username
|
|
if sess.CommonConfig.FakeUser != "" {
|
|
username = sess.CommonConfig.FakeUser
|
|
}
|
|
hostname := sess.CommonConfig.Hostname
|
|
|
|
fs := newFilesystem(hostname)
|
|
state := &shellState{
|
|
cwd: "/root",
|
|
username: username,
|
|
hostname: hostname,
|
|
fs: fs,
|
|
}
|
|
|
|
// Send banner.
|
|
if sess.CommonConfig.Banner != "" {
|
|
fmt.Fprint(rw, sess.CommonConfig.Banner)
|
|
}
|
|
fmt.Fprintf(rw, "Last login: %s from 10.0.0.1\r\n",
|
|
time.Now().Add(-2*time.Hour).Format("Mon Jan 2 15:04:05 2006"))
|
|
|
|
for {
|
|
prompt := formatPrompt(state)
|
|
if _, err := fmt.Fprint(rw, prompt); err != nil {
|
|
return nil
|
|
}
|
|
|
|
line, err := shell.ReadLine(ctx, rw)
|
|
if errors.Is(err, io.EOF) {
|
|
fmt.Fprint(rw, "logout\r\n")
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
|
|
result := dispatch(state, trimmed)
|
|
|
|
var output string
|
|
if result.output != "" {
|
|
output = result.output
|
|
// Convert newlines to \r\n for terminal display.
|
|
output = strings.ReplaceAll(output, "\r\n", "\n")
|
|
output = strings.ReplaceAll(output, "\n", "\r\n")
|
|
fmt.Fprintf(rw, "%s\r\n", output)
|
|
}
|
|
|
|
// Log command and output to store.
|
|
if sess.Store != nil {
|
|
if err := sess.Store.AppendSessionLog(ctx, sess.SessionID, trimmed, output); err != nil {
|
|
return fmt.Errorf("append session log: %w", err)
|
|
}
|
|
}
|
|
if sess.OnCommand != nil {
|
|
sess.OnCommand("bash")
|
|
}
|
|
|
|
if result.exit {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func formatPrompt(state *shellState) string {
|
|
cwd := state.cwd
|
|
if cwd == "/root" {
|
|
cwd = "~"
|
|
} else if strings.HasPrefix(cwd, "/root/") {
|
|
cwd = "~" + cwd[5:]
|
|
}
|
|
return fmt.Sprintf("%s@%s:%s# ", state.username, state.hostname, cwd)
|
|
}
|
|
|