introduces prometheus output plugin (#2555)

This commit is contained in:
Erik Belak
2025-04-20 17:03:56 +02:00
committed by GitHub
parent e61416b989
commit 9ef04de471
5 changed files with 379 additions and 0 deletions

148
docs/prometheus/README.rst Normal file
View File

@ -0,0 +1,148 @@
How to Send Cowrie output to a Prometheus
=============================================
This guide will show you how to stand up a complete monitoring stack in Docker:
1. Prometheus
2. Cowrie
3. node-exporter (host-level metrics)
4. cAdvisor (container-level metrics)
All containers will join a user-defined Docker network so they can find one another by name.
1. Create the Docker network
=============================
.. code-block:: bash
docker network create cowrie-net
2. Run Prometheus
==================
Create a volume for Prometheuss TSDB
.. code-block:: bash
docker volume create prometheus-data
For configuration file you can
Copy the example config into `/etc/prometheus` on your host
.. code-block:: bash
sudo mkdir -p /etc/prometheus
sudo cp ./docs/prometheus/prometheus.yaml /etc/prometheus/prometheus.yaml
Or from ~/cowrie call docker run with updated path
.. code-block:: bash
-v ./docs/prometheus/prometheus.yaml:/etc/prometheus/prometheus.yaml:ro \
3. Launch Prometheus on `cowrie-net`
======================================
.. code-block:: bash
docker run -d \
--name prometheus \
--network cowrie-net \
-p 9090:9090 \
-v /etc/prometheus/prometheus.yaml:/etc/prometheus/prometheus.yaml:ro \
-v prometheus-data:/prometheus \
prom/prometheus \
--config.file=/etc/prometheus/prometheus.yaml
Verify its running at http://localhost:9090/targets
3. Run Cowrie with Prometheus metrics
======================================
.. code-block:: bash
docker run
--name cowrie \
--network cowrie-net \
-p 2222:2222 \
-p 9000:9000 \
-e COWRIE_OUTPUT_PROMETHEUS_ENABLED=yes \
cowrie/cowrie:latest
---
4. Run node-exporter (host metrics)
======================================
.. code-block:: bash
docker run -d \
--name node-exporter \
--network cowrie-net \
--pid host \
-v /:/host:ro \
-p 9100:9100 \
quay.io/prometheus/node-exporter:latest \
--path.rootfs /host
---
5. Run cAdvisor (container metrics)
======================================
.. code-block:: bash
docker run -d \
--name cadvisor \
--network cowrie-net \
--privileged \
-v /:/rootfs:ro \
-v /var/run:/var/run:rw \
-v /sys:/sys:ro \
-v /sys/fs/cgroup:/sys/fs/cgroup:ro \
-v /var/lib/docker/:/var/lib/docker:ro \
-p 8080:8080 \
gcr.io/cadvisor/cadvisor:latest
Run cowrie with prometheus locally
===================================
Add the following entries in ``etc/cowrie.cfg`` under the Output Plugins section::
[output_prometheus]
enabled = true
port = 9000
debug = false
Ensure your `prometheus.yaml` has:
.. code-block:: yaml
global:
scrape_interval: 5s
scrape_configs:
- job_name: 'cowrie'
static_configs:
- targets: [
'localhost:9000',
]
- job_name: 'scrapers'
static_configs:
- targets: [
'node-exporter:9100',
'cadvisor:8080'
]
metric_relabel_configs:
- source_labels: [ cowrie ]
regex: '^cowrie$'
action: keep
- action: drop
regex: '.*'
Reload Prometheus if needed, then visit **Status → Targets** to confirm all three are UP.

View File

@ -0,0 +1,21 @@
---
global:
scrape_interval: 5s
scrape_configs:
- job_name: 'cowrie'
static_configs:
- targets: [
'cowrie:9000',
]
- job_name: 'scrapers'
static_configs:
- targets: [
'node-exporter:9100',
'cadvisor:8080'
]
metric_relabel_configs:
- source_labels: [ cowrie ]
regex: '^cowrie$'
action: keep
- action: drop
regex: '.*'

View File

@ -893,6 +893,16 @@ sourcetype = cowrie
source = cowrie source = cowrie
# Prometheus logging module
#
# Prometheus requires an extra Python module: pip install prometheus_client
#
[output_prometheus]
enabled = false
port = 9000
debug = false
# HPFeeds3 # HPFeeds3
# Python3 implementation of HPFeeds # Python3 implementation of HPFeeds
[output_hpfeeds3] [output_hpfeeds3]

View File

@ -50,3 +50,6 @@ redis==5.2.1
# rabbitmq # rabbitmq
pika==1.3.2 pika==1.3.2
# prometheus
prometheus_client==0.21.1

View File

@ -0,0 +1,197 @@
"""
Cowrie Prometheus output.
[output_prometheus]
enabled = true
port = 9000
"""
from __future__ import annotations
import socket
import time
from prometheus_client import start_http_server, Counter, Gauge, Histogram
from twisted.python import log
import cowrie.core.output
from cowrie.core.config import CowrieConfig
# ────────────────────────────────────────────
# Metric objects
# ────────────────────────────────────────────
HOST_LABEL = CowrieConfig.get("honeypot", "hostname", fallback=socket.gethostname())
BUCKETS_LEN = (0, 4, 8, 12, 16, 20, 40)
BUCKETS_DUR = (1, 5, 15, 30, 60, 120, 300, 900, 1800, 3600)
sessions_total = Counter(
"cowrie_sessions_total", "Total SSH/Telnet sessions", ["transport", "sensor"]
)
sessions_active = Gauge(
"cowrie_sessions_active", "Active sessions", ["transport", "sensor"]
)
session_duration = Histogram(
"cowrie_session_duration_seconds",
"Session duration seconds",
["transport"],
buckets=BUCKETS_DUR,
)
source_ip_total = Counter(
"cowrie_source_ip_total", "Sessions per source IP", ["ip", "asn_country"]
)
source_ip_card = Gauge(
"cowrie_source_ip_cardinality", "Unique source IPs seen", ["interval"]
)
login_attempts = Counter(
"cowrie_login_attempts_total", "Login attempts", ["result", "username"]
)
password_length = Histogram(
"cowrie_password_length", "Password length histogram", buckets=BUCKETS_LEN
)
commands_total = Counter(
"cowrie_command_total", "Commands executed", ["command", "sensor"]
)
dl_bytes_total = Counter(
"cowrie_file_download_bytes_total", "Downloaded bytes", ["protocol", "sensor"]
)
dl_time_hist = Histogram(
"cowrie_file_download_time_seconds",
"File download duration",
["protocol"],
buckets=BUCKETS_DUR,
)
outbound_total = Counter(
"cowrie_connection_outbound_total",
"Outbound directtcpip connections",
["dst_ip", "dst_port"],
)
loop_lag_hist = Histogram("cowrie_event_loop_lag_seconds", "Twisted reactor lag (s)")
py_exceptions = Counter(
"cowrie_python_exceptions_total", "Uncaught Python exceptions", ["exception"]
)
class Output(cowrie.core.output.Output):
def start(self) -> None:
port = CowrieConfig.getint("output_prometheus", "port", fallback=9000)
self.debug = CowrieConfig.getboolean(
"output_prometheus", "debug", fallback=False
)
start_http_server(port)
if self.debug:
log.msg(f"[Prometheus] Exporter started on port: {port}")
log.msg(f"[Prometheus] Host label: {HOST_LABEL}")
# Helper structures
self._start_times: dict[str, float] = {}
self._srcip_seen_5m: set[str] = set()
self._srcip_seen_60m: set[str] = set()
# Periodic callbacks for event-loop lag & unique-IP gauges
# task.LoopingCall(self._report_loop_lag).start(5, now=False)
# task.LoopingCall(self._flush_unique_ip_gauges, 300, "5m").start(300, now=False)
# task.LoopingCall(self._flush_unique_ip_gauges, 3600, "1h").start(
# 3600, now=False
# )
def write(self, event: dict) -> None:
try:
eid = event["eventid"]
if self.debug:
log.msg(f"[Prometheus] Event: {eid}")
if eid == "cowrie.session.connect":
self._on_session_connect(event)
elif eid == "cowrie.session.closed":
self._on_session_closed(event)
elif eid in ("cowrie.login.success", "cowrie.login.failed"):
self._on_login(event, success=eid.endswith("success"))
elif eid == "cowrie.command.input":
self._on_command(event)
elif eid == "cowrie.session.file_download":
self._on_download(event)
elif eid == "cowrie.direct-tcpip.request":
self._on_outbound(event)
if eid == "cowrie.session.connect":
ip = event.get("src_ip")
if ip:
self._srcip_seen_5m.add(ip)
self._srcip_seen_60m.add(ip)
except Exception as e:
if self.debug:
log.msg(f"[Prometheus] Exception: {e!s}")
py_exceptions.labels(exception=e.__class__.__name__).inc()
def _on_session_connect(self, ev: dict) -> None:
transport = ev.get("protocol", "ssh")
sensor = HOST_LABEL
sid = ev["session"]
sessions_total.labels(transport, sensor).inc()
sessions_active.labels(transport, sensor).inc()
self._start_times[sid] = ev["time"] if "time" in ev else time.time()
ip = ev.get("src_ip", "unknown")
asn = ev.get("src_persist_as", "UNK")
source_ip_total.labels(ip, asn).inc()
def _on_session_closed(self, ev: dict) -> None:
sid = ev["session"]
transport = ev.get("protocol", "ssh")
sensor = HOST_LABEL
sessions_active.labels(transport, sensor).dec()
start_ts = self._start_times.pop(sid, None)
if start_ts:
session_duration.labels(transport).observe(time.time() - start_ts)
def _on_login(self, ev: dict, *, success: bool) -> None:
res = "success" if success else "fail"
user = ev.get("username", "unknown")
passwd = ev.get("password", "")
login_attempts.labels(res, user).inc()
if passwd:
password_length.observe(len(str(passwd)))
def _on_command(self, ev: dict) -> None:
cmd = ev.get("input", "").strip().split(" ")[0][:30] # first token
commands_total.labels(cmd, HOST_LABEL).inc()
def _on_download(self, ev: dict) -> None:
proto = ev.get("shasum", "").split(":")[0] or "unknown"
size = int(ev.get("len", 0))
dtime = float(ev.get("duration", 0))
dl_bytes_total.labels(proto, HOST_LABEL).inc(size)
dl_time_hist.labels(proto).observe(dtime)
def _on_outbound(self, ev: dict) -> None:
dst_ip = ev.get("dst_ip", "unknown")
dst_port = str(ev.get("dst_port", "0"))
outbound_total.labels(dst_ip, dst_port).inc()
# def _report_loop_lag(self) -> None:
# before = time.time()
# reactor.callLater(0, lambda: loop_lag_hist.observe(time.time() - before))
#
# def _flush_unique_ip_gauges(self, interval_sec: int, label: str) -> None:
# s = self._srcip_seen_5m if interval_sec == 300 else self._srcip_seen_60m
# source_ip_card.labels(label).set(len(s))
# s.clear()
def stop(self):
pass