This repository has been archived on 2026-03-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
oubliette/internal/shell/shell.go
Torjus Håkestad df860b3061 feat: add new Prometheus metrics and bearer token auth for /metrics
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>
2026-02-15 15:54:29 +01:00

92 lines
2.0 KiB
Go

package shell
import (
"context"
"fmt"
"io"
"git.t-juice.club/torjus/oubliette/internal/storage"
)
// Shell is the interface that all honeypot shell implementations must satisfy.
type Shell interface {
Name() string
Description() string
Handle(ctx context.Context, sess *SessionContext, rw io.ReadWriteCloser) error
}
// SessionContext carries metadata about the current SSH session.
type SessionContext struct {
SessionID string
Username string
RemoteAddr string
ClientVersion string
Store storage.Store
ShellConfig map[string]any
CommonConfig ShellCommonConfig
OnCommand func(shell string) // called when a command is executed; may be nil
}
// ShellCommonConfig holds settings shared across all shell types.
type ShellCommonConfig struct {
Hostname string
Banner string
FakeUser string // override username in prompt; empty = use authenticated user
}
// ReadLine reads a line of input byte-by-byte, handling backspace, Ctrl+C, and Ctrl+D.
func ReadLine(ctx context.Context, rw io.ReadWriter) (string, error) {
var buf []byte
b := make([]byte, 1)
for {
select {
case <-ctx.Done():
return "", ctx.Err()
default:
}
n, err := rw.Read(b)
if err != nil {
return "", err
}
if n == 0 {
continue
}
ch := b[0]
switch {
case ch == '\r' || ch == '\n':
fmt.Fprint(rw, "\r\n")
return string(buf), nil
case ch == 4: // Ctrl+D
if len(buf) == 0 {
return "", io.EOF
}
case ch == 3: // Ctrl+C
fmt.Fprint(rw, "^C\r\n")
return "", nil
case ch == 127 || ch == 8: // DEL or Backspace
if len(buf) > 0 {
buf = buf[:len(buf)-1]
fmt.Fprint(rw, "\b \b")
}
case ch == 27: // ESC - start of escape sequence
// Read and discard the rest of the escape sequence.
// Most are 3 bytes: ESC [ X (arrow keys, etc.)
next := make([]byte, 1)
if n, _ := rw.Read(next); n > 0 && next[0] == '[' {
rw.Read(next) // read the final byte
}
case ch >= 32 && ch < 127: // printable ASCII
buf = append(buf, ch)
rw.Write([]byte{ch})
}
}
}