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:
|
||||
- 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:
|
||||
image: grafana/loki:3.7.1
|
||||
container_name: loki
|
||||
@@ -145,6 +163,7 @@ volumes:
|
||||
prometheus_data:
|
||||
alertmanager_data:
|
||||
matrix_alerts_data:
|
||||
release_watch_data:
|
||||
grafana_data:
|
||||
loki_data:
|
||||
alloy_data:
|
||||
|
||||
Reference in New Issue
Block a user