monitoring: Release-/Advisory-Watch fuer Element-Upstreams (gitops#22)

Stdlib-Daemon im matrix-alerts-Muster: pollt die GitHub-Release-Atom-Feeds
von synapse/ess-helm/element-web/mas/element-call alle 6h und meldet neue
Eintraege als Notiz in den Alerts-Raum (Security-Verdacht mit 🚨 markiert).
Erstlauf setzt nur den State. UNGETESTET bis zum Deploy auf CFGMON.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
This commit is contained in:
Thore Cimbal
2026-08-01 04:51:53 +02:00
co-authored by Claude Fable 5
parent 6ffab68583
commit 8c06329e33
2 changed files with 110 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
# Release-/Advisory-Watch fuer den Element-Stack (gitops#22). Gleiche Machart wie
# matrix-alerts.py: purer Stdlib-Daemon, Bot-Token aus der Umgebung, Nachricht per
# Client-Server-API in den Alerts-Raum.
#
# Quelle sind die oeffentlichen GitHub-Release-Atom-Feeds (kein API-Token noetig).
# Element veroeffentlicht Security-Fixes als Releases - der Feed ist damit der
# praktikable Advisory-Kanal; echte GHSA-Advisories haben keinen oeffentlichen Feed.
# Neue Eintraege werden einmalig als 📦-Notiz gemeldet; Security-verdaechtige
# Titel/Inhalte (CVE/security/vulnerab...) bekommen 🚨 und stehen vorn.
import json
import os
import re
import time
import urllib.parse
import urllib.request
import uuid
import xml.etree.ElementTree as ET
HOMESERVER = os.environ["MATRIX_HOMESERVER"]
ROOM_ID = os.environ["MATRIX_ROOM_ID"]
TOKEN = os.environ["MATRIX_TOKEN"]
STATE_FILE = os.environ.get("RELEASE_WATCH_STATE_FILE", "/state/release-watch.json")
INTERVAL = int(os.environ.get("RELEASE_WATCH_INTERVAL", "21600")) # 6h
REPOS = [
"element-hq/synapse",
"element-hq/ess-helm",
"element-hq/element-web",
"element-hq/matrix-authentication-service",
"element-hq/element-call",
]
SECURITY_RE = re.compile(r"cve|security|vulnerab|advisory", re.IGNORECASE)
ATOM = "{http://www.w3.org/2005/Atom}"
try:
with open(STATE_FILE) as f:
seen = json.load(f) # repo -> [entry ids]
except Exception:
seen = {}
def send_notice(text):
url = (f"{HOMESERVER}/_matrix/client/v3/rooms/{urllib.parse.quote(ROOM_ID)}"
f"/send/m.room.message/{uuid.uuid4()}")
req = urllib.request.Request(url, data=json.dumps({"msgtype": "m.notice", "body": text}).encode(),
method="PUT", headers={"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json"})
urllib.request.urlopen(req, timeout=10).read()
def check(repo):
feed = urllib.request.urlopen(f"https://github.com/{repo}/releases.atom", timeout=20).read()
root = ET.fromstring(feed)
entries = root.findall(f"{ATOM}entry")
known = set(seen.get(repo, []))
first_run = repo not in seen
new = []
for e in entries:
eid = e.findtext(f"{ATOM}id", "")
title = e.findtext(f"{ATOM}title", "?").strip()
link = ""
le = e.find(f"{ATOM}link")
if le is not None:
link = le.get("href", "")
content = e.findtext(f"{ATOM}content", "") or ""
if eid and eid not in known:
new.append((eid, title, link, bool(SECURITY_RE.search(title + " " + content[:2000]))))
# Erstlauf: nur Stand merken, nicht den Raum mit Historie fluten
seen[repo] = [e.findtext(f"{ATOM}id", "") for e in entries][:30]
if first_run:
return
for eid, title, link, is_sec in new:
icon = "\U0001F6A8" if is_sec else "\U0001F4E6"
kind = "Security-verdaechtiges Release" if is_sec else "Neues Release"
send_notice(f"{icon} {kind}: {repo}{title}\n{link}")
while True:
for repo in REPOS:
try:
check(repo)
except Exception as exc:
print(f"{repo}: {exc}", flush=True)
try:
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
with open(STATE_FILE, "w") as f:
json.dump(seen, f)
except Exception as exc:
print(f"state: {exc}", flush=True)
time.sleep(INTERVAL)
+19
View File
@@ -58,6 +58,24 @@ services:
networks: networks:
- traefik - traefik
# Release-/Advisory-Watch (gitops#22): meldet neue Releases der Element-Stack-
# Upstreams in den Alerts-Raum (🚨 bei Security-Verdacht). Gleicher Bot/Raum
# wie matrix-alerts, eigener State (Erstlauf merkt nur, flutet nicht).
release-watch:
image: python:3.13-slim
container_name: release-watch
restart: unless-stopped
environment:
- MATRIX_HOMESERVER=${MATRIX_ALERT_HOMESERVER}
- MATRIX_ROOM_ID=${MATRIX_ALERT_ROOM_ID}
- MATRIX_TOKEN=${MATRIX_ALERT_TOKEN}
volumes:
- ./alertmanager/release-watch.py:/app/release-watch.py:ro
- release_watch_data:/state
command: python3 /app/release-watch.py
networks:
- traefik
loki: loki:
image: grafana/loki:3.7.1 image: grafana/loki:3.7.1
container_name: loki container_name: loki
@@ -145,6 +163,7 @@ volumes:
prometheus_data: prometheus_data:
alertmanager_data: alertmanager_data:
matrix_alerts_data: matrix_alerts_data:
release_watch_data:
grafana_data: grafana_data:
loki_data: loki_data:
alloy_data: alloy_data: