mirror of
https://github.com/cowrie/cowrie.git
synced 2025-07-01 18:07:27 -04:00
introduces prometheus output plugin (#2555)
This commit is contained in:
148
docs/prometheus/README.rst
Normal file
148
docs/prometheus/README.rst
Normal 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 Prometheus’s 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 it’s 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.
|
||||
21
docs/prometheus/prometheus.yaml
Normal file
21
docs/prometheus/prometheus.yaml
Normal 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: '.*'
|
||||
@ -893,6 +893,16 @@ sourcetype = 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
|
||||
# Python3 implementation of HPFeeds
|
||||
[output_hpfeeds3]
|
||||
|
||||
@ -50,3 +50,6 @@ redis==5.2.1
|
||||
|
||||
# rabbitmq
|
||||
pika==1.3.2
|
||||
|
||||
# prometheus
|
||||
prometheus_client==0.21.1
|
||||
197
src/cowrie/output/prometheus.py
Normal file
197
src/cowrie/output/prometheus.py
Normal 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 direct‑tcpip 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
|
||||
Reference in New Issue
Block a user