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>
160 lines
5.4 KiB
Go
160 lines
5.4 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
|
|
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},
|
|
}),
|
|
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.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))
|
|
}
|