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:
co-authored by
Claude Fable 5
parent
6ffab68583
commit
8c06329e33
@@ -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)
|
||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user