Bots often send commands via `ssh user@host <command>` (exec request) rather than requesting an interactive shell. These were previously rejected silently. Now exec commands are captured, stored on the session record, and displayed in the web UI session detail page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
166 lines
5.6 KiB
Go
166 lines
5.6 KiB
Go
package metrics
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"git.t-juice.club/torjus/oubliette/internal/storage"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/collectors"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
)
|
|
|
|
// Metrics holds all Prometheus collectors for the honeypot.
|
|
type Metrics struct {
|
|
registry *prometheus.Registry
|
|
|
|
SSHConnectionsTotal *prometheus.CounterVec
|
|
SSHConnectionsActive prometheus.Gauge
|
|
AuthAttemptsTotal *prometheus.CounterVec
|
|
AuthAttemptsByCountry *prometheus.CounterVec
|
|
CommandsExecuted *prometheus.CounterVec
|
|
HumanScore prometheus.Histogram
|
|
SessionsTotal *prometheus.CounterVec
|
|
SessionsActive prometheus.Gauge
|
|
SessionDuration prometheus.Histogram
|
|
ExecCommandsTotal prometheus.Counter
|
|
BuildInfo *prometheus.GaugeVec
|
|
}
|
|
|
|
// New creates a new Metrics instance with all collectors registered.
|
|
func New(version string) *Metrics {
|
|
reg := prometheus.NewRegistry()
|
|
|
|
m := &Metrics{
|
|
registry: reg,
|
|
SSHConnectionsTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "oubliette_ssh_connections_total",
|
|
Help: "Total SSH connections received.",
|
|
}, []string{"outcome"}),
|
|
SSHConnectionsActive: prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Name: "oubliette_ssh_connections_active",
|
|
Help: "Current active SSH connections.",
|
|
}),
|
|
AuthAttemptsTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "oubliette_auth_attempts_total",
|
|
Help: "Total authentication attempts.",
|
|
}, []string{"result", "reason"}),
|
|
AuthAttemptsByCountry: prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "oubliette_auth_attempts_by_country_total",
|
|
Help: "Total authentication attempts by country.",
|
|
}, []string{"country"}),
|
|
CommandsExecuted: prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "oubliette_commands_executed_total",
|
|
Help: "Total commands executed in shells.",
|
|
}, []string{"shell"}),
|
|
HumanScore: prometheus.NewHistogram(prometheus.HistogramOpts{
|
|
Name: "oubliette_human_score",
|
|
Help: "Distribution of final human detection scores.",
|
|
Buckets: prometheus.LinearBuckets(0, 0.1, 11), // 0.0, 0.1, ..., 1.0
|
|
}),
|
|
SessionsTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "oubliette_sessions_total",
|
|
Help: "Total sessions created.",
|
|
}, []string{"shell"}),
|
|
SessionsActive: prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Name: "oubliette_sessions_active",
|
|
Help: "Current active sessions.",
|
|
}),
|
|
SessionDuration: prometheus.NewHistogram(prometheus.HistogramOpts{
|
|
Name: "oubliette_session_duration_seconds",
|
|
Help: "Session duration in seconds.",
|
|
Buckets: []float64{1, 5, 10, 30, 60, 120, 300, 600, 1800, 3600},
|
|
}),
|
|
ExecCommandsTotal: prometheus.NewCounter(prometheus.CounterOpts{
|
|
Name: "oubliette_exec_commands_total",
|
|
Help: "Total SSH exec commands received.",
|
|
}),
|
|
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
Name: "oubliette_build_info",
|
|
Help: "Build information. Always 1.",
|
|
}, []string{"version"}),
|
|
}
|
|
|
|
reg.MustRegister(
|
|
collectors.NewGoCollector(),
|
|
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
|
|
m.SSHConnectionsTotal,
|
|
m.SSHConnectionsActive,
|
|
m.AuthAttemptsTotal,
|
|
m.AuthAttemptsByCountry,
|
|
m.CommandsExecuted,
|
|
m.HumanScore,
|
|
m.SessionsTotal,
|
|
m.SessionsActive,
|
|
m.SessionDuration,
|
|
m.ExecCommandsTotal,
|
|
m.BuildInfo,
|
|
)
|
|
|
|
m.BuildInfo.WithLabelValues(version).Set(1)
|
|
|
|
// Initialize label combinations so they appear in Gather/output.
|
|
for _, outcome := range []string{"accepted", "rejected_handshake", "rejected_max_connections"} {
|
|
m.SSHConnectionsTotal.WithLabelValues(outcome)
|
|
}
|
|
for _, reason := range []string{"static_credential", "remembered_credential", "threshold_reached", "rejected"} {
|
|
m.AuthAttemptsTotal.WithLabelValues("accepted", reason)
|
|
m.AuthAttemptsTotal.WithLabelValues("rejected", reason)
|
|
}
|
|
for _, sh := range []string{"bash", "fridge", "banking", "adventure", "cisco"} {
|
|
m.SessionsTotal.WithLabelValues(sh)
|
|
m.CommandsExecuted.WithLabelValues(sh)
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// RegisterStoreCollector registers a collector that queries storage stats on each scrape.
|
|
func (m *Metrics) RegisterStoreCollector(store storage.Store) {
|
|
m.registry.MustRegister(&storeCollector{store: store})
|
|
}
|
|
|
|
// Handler returns an http.Handler that serves Prometheus metrics.
|
|
func (m *Metrics) Handler() http.Handler {
|
|
return promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{})
|
|
}
|
|
|
|
// storeCollector implements prometheus.Collector, querying storage on each scrape.
|
|
type storeCollector struct {
|
|
store storage.Store
|
|
}
|
|
|
|
var (
|
|
storageLoginAttemptsDesc = prometheus.NewDesc(
|
|
"oubliette_storage_login_attempts_total",
|
|
"Total login attempts in storage.",
|
|
nil, nil,
|
|
)
|
|
storageUniqueIPsDesc = prometheus.NewDesc(
|
|
"oubliette_storage_unique_ips",
|
|
"Unique IPs in storage.",
|
|
nil, nil,
|
|
)
|
|
storageSessionsDesc = prometheus.NewDesc(
|
|
"oubliette_storage_sessions_total",
|
|
"Total sessions in storage.",
|
|
nil, nil,
|
|
)
|
|
)
|
|
|
|
func (c *storeCollector) Describe(ch chan<- *prometheus.Desc) {
|
|
ch <- storageLoginAttemptsDesc
|
|
ch <- storageUniqueIPsDesc
|
|
ch <- storageSessionsDesc
|
|
}
|
|
|
|
func (c *storeCollector) Collect(ch chan<- prometheus.Metric) {
|
|
stats, err := c.store.GetDashboardStats(context.Background())
|
|
if err != nil {
|
|
return
|
|
}
|
|
ch <- prometheus.MustNewConstMetric(storageLoginAttemptsDesc, prometheus.GaugeValue, float64(stats.TotalAttempts))
|
|
ch <- prometheus.MustNewConstMetric(storageUniqueIPsDesc, prometheus.GaugeValue, float64(stats.UniqueIPs))
|
|
ch <- prometheus.MustNewConstMetric(storageSessionsDesc, prometheus.GaugeValue, float64(stats.TotalSessions))
|
|
}
|