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
|
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]
|
||||||
|
|||||||
@ -50,3 +50,6 @@ redis==5.2.1
|
|||||||
|
|
||||||
# rabbitmq
|
# rabbitmq
|
||||||
pika==1.3.2
|
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