syncova-backup/scripts/diagnose.sh
Jerrit Fritzsche b6668c600d
Some checks failed
CI / Backend (Go) (push) Failing after 29s
CI / Frontend (React/TypeScript) (push) Successful in 33s
CI / Sicherheitsprüfungen (push) Successful in 24s
Weboberflaeche richtet sich mit ein (rc5)
Bisher endete setup.sh mit einer laufenden API auf 127.0.0.1:8080 und der
Aufgabe, einen Webserver von Hand davorzusetzen. Das war der haeufigste Punkt,
an dem eine Einrichtung liegen blieb.

setup.sh richtet jetzt nginx ein und stellt ein selbst signiertes Zertifikat
aus. Es gilt fuer Rechnernamen, vollstaendigen Namen und jede globale
IPv4-Adresse (subjectAltName — moderne Browser lesen den CN nicht mehr), 3650
Tage; der SHA-256-Fingerabdruck wird genannt.

Drei Entscheidungen:

- Die API bleibt an 127.0.0.1:8080 gebunden. Sie auf alle Schnittstellen zu
  legen waere der kuerzere Weg und der falsche: Die Verschluesselung liesse
  sich dann umgehen, indem man Port 8080 direkt anspricht.
- Die Firewall wird gemeldet, nicht geaendert. Eine Einrichtung, die
  selbsttaetig einen Port ins Netz oeffnet, hebelt genau die Entscheidung aus,
  fuer die jemand die Firewall aufgesetzt hat.
- Scheitert die Oberflaeche, scheitert nicht die Einrichtung. Geprueft wird mit
  nginx -t, bevor die Konfiguration uebernommen wird; haelt sie nicht, wird sie
  entfernt und der Nachholweg gezeigt.

Zwei Funde beim Erproben:

- http2 on; gibt es erst ab nginx 1.25.1. Debian 12 liefert 1.22, wo HTTP/2 ein
  Parameter von listen ist — die neue Schreibweise ergibt dort "unknown
  directive http2", und nginx startet nicht. Die Fassung wird jetzt gelesen.
- setup.sh kopierte nur diagnose.sh neben die Programme. Der eigene Hinweis
  "Spaeter nachholen: /opt/syncova/setup.sh --weboberflaeche" verwies damit auf
  eine Datei, die es nicht gab; schwerer wiegt uninstall.sh — wer das
  ausgepackte Paket aufraeumte, haette die Anlage nie wieder entfernen koennen.
  Jetzt kommen alle vier Skripte mit.

Nachgewiesen im Container gegen Debian 12 mit nginx 1.22: Neuinstallation von
Grund auf, Oberflaeche und /api/ von aussen ueber HTTPS erreichbar (200),
SPA-Fallback traegt, Anmeldung und Repository-Anlage durch nginx hindurch,
Durchsetzungsstufe gemessen, HTTP leitet mit 301 auf HTTPS, Fingerabdruck
stimmt mit dem genannten ueberein, Neuausstellung des Zertifikats geprueft.

Regressionstests fuer beide Funde, beide durch Mutation als fangend bestaetigt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 08:53:15 +02:00

537 lines
21 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Sammelt den Zustand einer Syncova-Installation fuer eine Fehlermeldung.
#
# Der Sinn: **eine** Ausgabe statt zwoelf Rueckfragen. Was hier steht, ist genau
# das, wonach man sonst einzeln fragen muesste — und was ohne Nachfrage meist
# fehlt: Fassung, Schemastand, Dateisystem des Repositorys, gemessene
# Durchsetzungsstufe, die letzten Fehlerzeilen.
#
# sudo ./diagnose.sh Ausgabe auf den Bildschirm
# sudo ./diagnose.sh > bericht.md In eine Datei
#
# Das Skript **veraendert nichts**. Es liest, mehr nicht.
#
# Geheimnisse werden nicht ausgegeben. Der Weg dahin ist bewusst umgekehrt: Es
# gibt eine Liste der Werte, die gezeigt werden **duerfen**; alles andere bleibt
# draussen. Eine Sperrliste vergaesse den naechsten neuen Wert.
set -uo pipefail
# ---------------------------------------------------------------------------
# Feste Orte — gleichlautend in setup.sh, update.sh und uninstall.sh
# ---------------------------------------------------------------------------
readonly installationRoot="/opt/syncova"
readonly configurationDirectory="/etc/syncova"
readonly configurationFile="${configurationDirectory}/syncova.env"
readonly serviceName="syncova-api"
readonly backupDirectory="/var/backups/syncova"
# publicSettings sind die Werte, die in einer Fehlermeldung stehen duerfen.
#
# Eine Erlaubnisliste und keine Sperrliste: Kommt morgen eine neue Variable mit
# einem Geheimnis hinzu, faellt sie hier automatisch heraus. Bei einer Sperrliste
# stuende sie im naechsten Bericht.
readonly publicSettings=(
SYNCOVA_ENV
SYNCOVA_DB_HOST SYNCOVA_DB_PORT SYNCOVA_DB_NAME SYNCOVA_DB_USER SYNCOVA_DB_SSLMODE
SYNCOVA_DB_MAX_OPEN_CONNECTIONS
SYNCOVA_ENCRYPTION_CURRENT_KEY
SYNCOVA_HTTP_LISTEN_ADDRESS SYNCOVA_HTTP_REQUESTS_PER_MINUTE
SYNCOVA_HTTP_TLS_CERT_FILE SYNCOVA_HTTP_TLS_KEY_FILE
SYNCOVA_LOG_LEVEL SYNCOVA_LOG_FORMAT
SYNCOVA_AUTH_REQUIRE_MFA_FOR_PRIVILEGED_USERS
SYNCOVA_AUTH_MAX_FAILED_LOGIN_ATTEMPTS SYNCOVA_AUTH_LOCKOUT_DURATION
SYNCOVA_RESTORE_ALLOWED_ROOTS
SYNCOVA_ALLOW_INTERNAL_NOTIFICATION_TARGETS
)
# secretValues sind die tatsaechlichen Geheimnisse dieser Anlage.
#
# Sie werden gelesen, um sie aus **jeder** Ausgabe zu entfernen — auch aus
# Protokollzeilen, in die sie auf einem Weg geraten sind, den niemand vorhergesehen
# hat. Die Erlaubnisliste oben schuetzt die Konfiguration; das hier schuetzt alles
# uebrige.
secretValues=()
# serviceIsReachable prueft, ob eine Adresse mit HTTP 200 antwortet.
#
# Drei Wege, weil keiner ueberall vorhanden ist. Genau daran ist die Einrichtung
# in Issue #2 gescheitert: Der Dienst lief einwandfrei — Protokoll und
# Zeitstempel belegen es —, aber auf dem System war kein curl installiert. Die
# Bereitschaftspruefung hielt das fuer einen gescheiterten Start und baute eine
# funktionierende Anlage wieder zurueck.
#
# Ein Einrichtungsskript darf nicht voraussetzen, was es nicht selbst mitbringt.
serviceIsReachable() {
local requestURL="$1"
if command -v curl >/dev/null 2>&1; then
curl -fsS --max-time 5 "${requestURL}" >/dev/null 2>&1
return $?
fi
# wget endet bei 4xx und 5xx mit einem Fehler — dasselbe Verhalten wie
# "curl -f".
if command -v wget >/dev/null 2>&1; then
wget -q -O /dev/null --timeout=5 "${requestURL}" >/dev/null 2>&1
return $?
fi
bashHTTPStatusIsOK "${requestURL}"
}
# bashHTTPStatusIsOK spricht HTTP ohne jedes Zusatzwerkzeug.
#
# Der letzte Ausweg: /dev/tcp gehoert zur Bash selbst. Damit laesst sich die
# Bereitschaft auch auf einem System pruefen, auf dem weder curl noch wget
# liegen — und das ist bei einem schlanken Serverabbild der Normalfall, nicht
# die Ausnahme.
bashHTTPStatusIsOK() {
local strippedURL="${1#http://}"
local hostAndPort="${strippedURL%%/*}"
local requestPath="/${strippedURL#*/}"
# Ohne Pfadanteil liefert die Ersetzung oben die Adresse zurueck.
[[ "${requestPath}" == "/${hostAndPort}" ]] && requestPath="/"
local targetHost="${hostAndPort%%:*}"
local targetPort="${hostAndPort##*:}"
[[ "${targetPort}" == "${targetHost}" ]] && targetPort=80
exec 3<>"/dev/tcp/${targetHost}/${targetPort}" 2>/dev/null || return 1
printf 'GET %s HTTP/1.0\r\nHost: %s\r\nConnection: close\r\n\r\n' \
"${requestPath}" "${hostAndPort}" >&3
local statusLine=""
read -r -t 5 statusLine <&3
exec 3<&- 3>&- 2>/dev/null
[[ "${statusLine}" == *" 200 "* ]]
}
# writeHeading schreibt eine Ueberschrift.
writeHeading() { printf '\n## %s\n\n' "$1"; }
# writeBlock rahmt einen Ausgabeblock.
writeBlock() { printf '```text\n%s\n```\n' "$1"; }
# redactSecrets entfernt bekannte Geheimnisse aus einem Text.
redactSecrets() {
local text="$1" secretValue
for secretValue in "${secretValues[@]}"; do
[[ ${#secretValue} -lt 8 ]] && continue
text="${text//${secretValue}/***}"
done
printf '%s' "${text}"
}
# httpToolInUse benennt das verwendete Werkzeug.
#
# Es gehoert in den Bericht: Steht dort der Notweg ueber /dev/tcp, weiss der
# Leser, dass auf diesem System weder curl noch wget liegt — und genau daran ist
# in Issue #2 eine Einrichtung gescheitert.
if command -v curl >/dev/null 2>&1; then
httpToolInUse="curl"
elif command -v wget >/dev/null 2>&1; then
httpToolInUse="wget (curl ist nicht vorhanden)"
else
httpToolInUse="Bash /dev/tcp (weder curl noch wget vorhanden)"
fi
# fetchHTTPBody holt eine Antwort mit dem vorhandenen Werkzeug.
fetchHTTPBody() {
local requestURL="$1"
if command -v curl >/dev/null 2>&1; then
curl -s --max-time 10 -w $'\n(HTTP %{http_code})' "${requestURL}" 2>&1 || echo '(nicht erreichbar)'
return
fi
if command -v wget >/dev/null 2>&1; then
wget -q -O - --timeout=10 "${requestURL}" 2>&1 || echo '(nicht erreichbar)'
return
fi
if bashHTTPStatusIsOK "${requestURL}"; then
echo "(HTTP 200 — Inhalt ohne curl oder wget nicht lesbar)"
else
echo "(nicht erreichbar oder kein HTTP 200)"
fi
}
# runAndCapture fuehrt ein Kommando aus und gibt seine Ausgabe redigiert zurueck.
runAndCapture() {
local commandOutput
commandOutput="$("$@" 2>&1)" || true
[[ -z "${commandOutput}" ]] && commandOutput="(keine Ausgabe)"
redactSecrets "${commandOutput}"
}
# ---------------------------------------------------------------------------
# Konfiguration einlesen
# ---------------------------------------------------------------------------
configurationIsReadable="nein"
if [[ -r "${configurationFile}" ]]; then
configurationIsReadable="ja"
set -a
# shellcheck disable=SC1090
. "${configurationFile}"
set +a
[[ -n "${SYNCOVA_DB_PASSWORD:-}" ]] && secretValues+=("${SYNCOVA_DB_PASSWORD}")
[[ -n "${SYNCOVA_ENCRYPTION_KEYS:-}" ]] && secretValues+=("${SYNCOVA_ENCRYPTION_KEYS}")
# Auch der reine Schluesselwert ohne Versionspraefix, falls er einzeln
# irgendwo auftaucht.
if [[ "${SYNCOVA_ENCRYPTION_KEYS:-}" == *:* ]]; then
secretValues+=("${SYNCOVA_ENCRYPTION_KEYS#*:}")
fi
fi
# ---------------------------------------------------------------------------
# Kopf
# ---------------------------------------------------------------------------
printf '# Syncova — Diagnosebericht\n\n'
printf 'Erstellt: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
if [[ "$(id -u)" -ne 0 ]]; then
printf '\n> **Hinweis:** ohne root-Rechte erstellt. Konfiguration, Dienstprotokoll\n'
printf '> und Datenbankangaben fehlen deshalb. Vollstaendig: `sudo ./diagnose.sh`\n'
fi
# ---------------------------------------------------------------------------
# 1. Fassungen
# ---------------------------------------------------------------------------
writeHeading "Fassungen"
versionReport=""
if [[ -d "${installationRoot}/bin" ]]; then
for programPath in "${installationRoot}"/bin/*; do
[[ -x "${programPath}" ]] || continue
versionReport+="$("${programPath}" --version 2>/dev/null || echo "$(basename "${programPath}") (keine Versionsangabe)")"$'\n'
done
else
versionReport="Unter ${installationRoot}/bin liegt kein Programm."$'\n'
fi
[[ -f "${installationRoot}/VERSION" ]] && \
versionReport+="VERSION-Datei: $(cat "${installationRoot}/VERSION")"$'\n'
[[ -d "${installationRoot}.vorherige" ]] && \
versionReport+="Vorige Fassung liegt noch: ${installationRoot}.vorherige"$'\n'
writeBlock "${versionReport%$'\n'}"
# ---------------------------------------------------------------------------
# 2. System
# ---------------------------------------------------------------------------
writeHeading "System"
systemReport="Betriebssystem: $(. /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-unbekannt}" || echo unbekannt)
Kern: $(uname -sr)
Architektur: $(uname -m)"
# Container oder nicht: Er entscheidet ueber den Loeschschutz. In einem Container
# auf overlayfs gibt es kein Unveraenderlich-Kennzeichen, und ein "advisory" ist
# dann die richtige Auskunft und kein Fehler.
if [[ -f /.dockerenv ]] || grep -qa 'container=' /proc/1/environ 2>/dev/null; then
systemReport+=$'\n'"Umgebung: Container"
else
systemReport+=$'\n'"Umgebung: kein Container erkannt"
fi
systemReport+=$'\n'"systemd: $(command -v systemctl >/dev/null 2>&1 && echo vorhanden || echo 'nicht vorhanden')"
writeBlock "${systemReport}"
# ---------------------------------------------------------------------------
# 3. Dienst
# ---------------------------------------------------------------------------
writeHeading "Dienst"
serviceReport=""
if command -v systemctl >/dev/null 2>&1; then
serviceState="$(systemctl is-active "${serviceName}" 2>/dev/null)"
serviceReport+="Zustand: ${serviceState:-nicht aktiv}"$'\n'
serviceEnabled="$(systemctl is-enabled "${serviceName}" 2>/dev/null)"
serviceReport+="Autostart: ${serviceEnabled:-nicht eingerichtet}"$'\n'
serviceReport+=$'\n'"$(runAndCapture systemctl status "${serviceName}" --no-pager -n 0)"
else
serviceReport="systemd ist nicht vorhanden."
fi
writeBlock "$(redactSecrets "${serviceReport}")"
# Die Gesundheitsantwort traegt auch bei 503 den vollstaendigen Bericht — sie
# sagt, welche Komponente fehlt, nicht nur dass etwas fehlt.
writeHeading "Gesundheit"
healthAddress="${SYNCOVA_HTTP_LISTEN_ADDRESS:-127.0.0.1:8080}"
healthReport=""
for endpointPath in /health/live /health/ready /api/v1/health; do
responseBody="$(fetchHTTPBody "http://${healthAddress}${endpointPath}")"
healthReport+="${endpointPath}: ${responseBody}"$'\n\n'
done
healthReport+="Abgefragt mit: ${httpToolInUse}"
writeBlock "$(redactSecrets "${healthReport%$'\n\n'}")"
# ---------------------------------------------------------------------------
# 3b. Weboberflaeche
# ---------------------------------------------------------------------------
#
# Seit rc5 richtet setup.sh nginx mit ein. Damit kann eine Stoerung auch dort
# liegen — und "die Oberflaeche laedt nicht" ist ohne diese Angaben nicht von
# "der Dienst laeuft nicht" zu unterscheiden.
writeHeading "Weboberflaeche"
webReport=""
if [[ -f "${configurationDirectory}/tls/server.crt" ]]; then
# Gueltigkeit und Geltungsbereich. Ein abgelaufenes Zertifikat oder eine
# fehlende IP im subjectAltName sind zwei der haeufigsten Ursachen dafuer,
# dass ein Browser den Zugang verweigert — und beide sind von aussen nicht
# zu erkennen.
webReport+="Zertifikat: ${configurationDirectory}/tls/server.crt"$'\n'
webReport+="$(openssl x509 -in "${configurationDirectory}/tls/server.crt" \
-noout -enddate -ext subjectAltName 2>/dev/null || echo "nicht lesbar")"$'\n\n'
else
webReport+="Kein Zertifikat unter ${configurationDirectory}/tls/"$'\n\n'
fi
if command -v nginx >/dev/null 2>&1; then
webReport+="$(nginx -v 2>&1)"$'\n'
# Die Fassung entscheidet ueber die HTTP/2-Schreibweise; steht sie hier,
# laesst sich eine abgelehnte Konfiguration sofort zuordnen.
if nginx -t >/dev/null 2>&1; then
webReport+="Konfiguration: angenommen"$'\n'
else
webReport+="Konfiguration: ABGELEHNT"$'\n'
webReport+="$(nginx -t 2>&1)"$'\n'
fi
# Nicht ueber "|| echo unbekannt": systemctl kann mit Status 0 und leerer
# Ausgabe antworten, und ein leeres Feld im Fehlerbericht ist wertlos —
# es liest sich wie eine Angabe, ist aber keine.
nginxServiceState="$(systemctl is-active nginx 2>/dev/null)"
webReport+="Dienst: ${nginxServiceState:-unbekannt}"$'\n'
webReport+="Site vorhanden: $([[ -f /etc/nginx/sites-enabled/syncova ]] && echo ja || echo nein)"
else
webReport+="nginx ist nicht vorhanden — die Oberflaeche wurde nicht eingerichtet."$'\n'
webReport+="Nachholen: ${installationRoot}/setup.sh --weboberflaeche"
fi
writeBlock "$(redactSecrets "${webReport}")"
# ---------------------------------------------------------------------------
# 4. Datenbank
# ---------------------------------------------------------------------------
writeHeading "Datenbank und Schema"
databaseReport=""
if [[ "${configurationIsReadable}" == "ja" ]] && [[ -x "${installationRoot}/bin/syncova-migrate" ]]; then
databaseReport+="$(runAndCapture "${installationRoot}/bin/syncova-migrate" status)"$'\n'
else
databaseReport+="Der Schemastand ist ohne lesbare Konfiguration nicht zu ermitteln."$'\n'
fi
if [[ "${configurationIsReadable}" == "ja" ]] && command -v psql >/dev/null 2>&1; then
databaseReport+=$'\n'"PostgreSQL: $(PGPASSWORD="${SYNCOVA_DB_PASSWORD:-}" psql -h "${SYNCOVA_DB_HOST:-127.0.0.1}" \
-p "${SYNCOVA_DB_PORT:-5432}" -U "${SYNCOVA_DB_USER:-syncova}" -d "${SYNCOVA_DB_NAME:-syncova}" \
-tAc 'SHOW server_version' 2>&1 | head -1)"
fi
writeBlock "$(redactSecrets "${databaseReport%$'\n'}")"
# Der Bestand sagt mehr als jede Beschreibung: Eine Anlage ohne einen einzigen
# erfolgreichen Lauf hat ein anderes Problem als eine mit tausend.
if [[ "${configurationIsReadable}" == "ja" ]] && command -v psql >/dev/null 2>&1; then
writeHeading "Bestand"
inventoryQuery="
SELECT 'Repositories: ' || count(*) FROM repositories
UNION ALL SELECT 'Auftraege: ' || count(*) FROM backup_jobs
UNION ALL SELECT 'Laeufe gesamt: ' || count(*) FROM backup_job_runs
UNION ALL SELECT 'davon erfolgreich: ' || count(*) FROM backup_job_runs WHERE status = 'succeeded'
UNION ALL SELECT 'davon Teilfehler: ' || count(*) FROM backup_job_runs WHERE status = 'partial_failure'
UNION ALL SELECT 'davon gescheitert: ' || count(*) FROM backup_job_runs WHERE status = 'failed'
UNION ALL SELECT 'Wiederherstellungsp.: ' || count(*) FROM backups WHERE deleted_at IS NULL
UNION ALL SELECT 'Agenten: ' || count(*) FROM agents
UNION ALL SELECT 'Konten (aktiv): ' || count(*) FROM users WHERE status = 'active';"
writeBlock "$(redactSecrets "$(PGPASSWORD="${SYNCOVA_DB_PASSWORD:-}" psql \
-h "${SYNCOVA_DB_HOST:-127.0.0.1}" -p "${SYNCOVA_DB_PORT:-5432}" \
-U "${SYNCOVA_DB_USER:-syncova}" -d "${SYNCOVA_DB_NAME:-syncova}" \
-tA -c "${inventoryQuery}" 2>&1)")"
# Die letzten gescheiterten Laeufe samt Fehlercode und Klasse. Der Code ist
# der Vertrag; der Meldungstext ist fuer Menschen und darf sich aendern.
writeHeading "Letzte nicht erfolgreiche Laeufe"
failureQuery="
SELECT to_char(coalesce(completed_at, started_at), 'YYYY-MM-DD HH24:MI') || ' ' ||
rpad(status, 16) || ' ' ||
rpad(coalesce(error_code, '-'), 26) || ' ' ||
coalesce(failure_class, '-')
FROM backup_job_runs
WHERE status <> 'succeeded'
ORDER BY coalesce(completed_at, started_at) DESC NULLS LAST
LIMIT 10;"
failureOutput="$(PGPASSWORD="${SYNCOVA_DB_PASSWORD:-}" psql \
-h "${SYNCOVA_DB_HOST:-127.0.0.1}" -p "${SYNCOVA_DB_PORT:-5432}" \
-U "${SYNCOVA_DB_USER:-syncova}" -d "${SYNCOVA_DB_NAME:-syncova}" \
-tA -c "${failureQuery}" 2>&1)"
[[ -z "${failureOutput}" ]] && failureOutput="(keine — alle Laeufe waren erfolgreich)"
writeBlock "$(redactSecrets "${failureOutput}")"
fi
# ---------------------------------------------------------------------------
# 5. Repositories
# ---------------------------------------------------------------------------
writeHeading "Repositories"
repositoryReport=""
if [[ "${configurationIsReadable}" == "ja" ]] && command -v psql >/dev/null 2>&1; then
while IFS='|' read -r repositoryLocation repositoryHardened repositoryLevel; do
[[ -n "${repositoryLocation}" ]] || continue
repositoryReport+="Ort: ${repositoryLocation}"$'\n'
# PostgreSQL liefert "t"/"f" — das liest sich in einem Bericht wie ein
# Tippfehler.
[[ "${repositoryHardened}" == "t" ]] && repositoryHardened="ja" || repositoryHardened="nein"
repositoryReport+="Gehaertet: ${repositoryHardened}"$'\n'
repositoryReport+="Durchsetzung: ${repositoryLevel:-nie gemessen}"$'\n'
if [[ -d "${repositoryLocation}" ]]; then
# Das Dateisystem entscheidet ueber den Loeschschutz. Auf overlayfs,
# NFS oder ohne CAP_LINUX_IMMUTABLE gibt es kein
# Unveraenderlich-Kennzeichen — "advisory" ist dann richtig.
repositoryReport+="Dateisystem: $(stat -f -c '%T' "${repositoryLocation}" 2>/dev/null || echo unbekannt)"$'\n'
repositoryReport+="Belegt: $(du -sh "${repositoryLocation}" 2>/dev/null | cut -f1)"$'\n'
repositoryReport+="Frei: $(df -h "${repositoryLocation}" 2>/dev/null | awk 'NR==2 {print $4" von "$2}')"$'\n'
if [[ -x "${installationRoot}/bin/syncova-repo" ]]; then
repositoryReport+=$'\n'"$(runAndCapture "${installationRoot}/bin/syncova-repo" health --path "${repositoryLocation}")"$'\n'
fi
else
repositoryReport+="Zustand: NICHT ERREICHBAR"$'\n'
fi
repositoryReport+=$'\n'
done < <(PGPASSWORD="${SYNCOVA_DB_PASSWORD:-}" psql -h "${SYNCOVA_DB_HOST:-127.0.0.1}" \
-p "${SYNCOVA_DB_PORT:-5432}" -U "${SYNCOVA_DB_USER:-syncova}" -d "${SYNCOVA_DB_NAME:-syncova}" \
-tA -F'|' -c "SELECT location, hardened, coalesce(enforcement_level,'') FROM repositories ORDER BY name" 2>/dev/null)
fi
# Ein Repository auf der Platte, das nicht in der Control Plane steht, ist eine
# haeufige und leicht zu uebersehende Lage: Die Sicherung laeuft nie, weil das
# Ziel dem Server unbekannt ist. Sie gehoert in den Bericht.
for candidatePath in /srv/syncova-repository "${SYNCOVA_SETUP_REPOSITORY_PATH:-}"; do
[[ -n "${candidatePath}" ]] || continue
[[ -f "${candidatePath}/format/repository.json" ]] || continue
[[ "${repositoryReport}" == *"${candidatePath}"* ]] && continue
repositoryReport+="Ort: ${candidatePath}"$'\n'
repositoryReport+="Eingetragen: NEIN — die Control Plane kennt dieses Repository nicht."$'\n'
repositoryReport+=" Ohne Eintrag laeuft keine Sicherung hinein."$'\n'
repositoryReport+="Dateisystem: $(stat -f -c '%T' "${candidatePath}" 2>/dev/null || echo unbekannt)"$'\n\n'
done
[[ -z "${repositoryReport}" ]] && repositoryReport="(keines gefunden oder nicht ermittelbar)"
writeBlock "$(redactSecrets "${repositoryReport%$'\n'}")"
# ---------------------------------------------------------------------------
# 6. Konfiguration
# ---------------------------------------------------------------------------
writeHeading "Konfiguration"
configurationReport=""
if [[ "${configurationIsReadable}" == "ja" ]]; then
for settingName in "${publicSettings[@]}"; do
settingValue="${!settingName:-}"
[[ -n "${settingValue}" ]] && configurationReport+="${settingName}=${settingValue}"$'\n'
done
# Nur ob gesetzt, niemals der Wert.
configurationReport+=$'\n'"SYNCOVA_DB_PASSWORD: $( [[ -n "${SYNCOVA_DB_PASSWORD:-}" ]] && echo gesetzt || echo 'NICHT GESETZT' )"$'\n'
configurationReport+="SYNCOVA_ENCRYPTION_KEYS: $( [[ -n "${SYNCOVA_ENCRYPTION_KEYS:-}" ]] && echo gesetzt || echo 'NICHT GESETZT' )"$'\n'
configurationReport+=$'\n'"Rechte: $(stat -c '%a %U:%G' "${configurationFile}" 2>/dev/null || echo unbekannt) (erwartet: 600 syncova:syncova)"
else
configurationReport="${configurationFile} ist nicht lesbar (root-Rechte noetig?)."
fi
writeBlock "${configurationReport}"
# ---------------------------------------------------------------------------
# 7. Protokoll
# ---------------------------------------------------------------------------
writeHeading "Letzte Fehler- und Warnzeilen"
logReport=""
if command -v journalctl >/dev/null 2>&1; then
logReport="$(journalctl -u "${serviceName}" --no-pager -n 400 2>/dev/null \
| grep -iE '"level":"(ERROR|WARN)"|error|fehler|panic' | tail -30)"
fi
[[ -z "${logReport}" ]] && logReport="(keine Fehler- oder Warnzeilen in den letzten 400 Eintraegen)"
writeBlock "$(redactSecrets "${logReport}")"
writeHeading "Die letzten Protokollzeilen"
recentLog=""
command -v journalctl >/dev/null 2>&1 && \
recentLog="$(journalctl -u "${serviceName}" --no-pager -n 25 2>/dev/null)"
[[ -z "${recentLog}" ]] && recentLog="(kein Journal verfuegbar)"
writeBlock "$(redactSecrets "${recentLog}")"
# ---------------------------------------------------------------------------
# 8. Sicherungen der Aktualisierungen
# ---------------------------------------------------------------------------
if [[ -d "${backupDirectory}" ]]; then
writeHeading "Sicherungen frueherer Aktualisierungen"
writeBlock "$(ls -lh "${backupDirectory}" 2>/dev/null | tail -8)"
fi
printf '\n---\n\n'
printf 'Erstellt mit `diagnose.sh`. Geheimnisse sind entfernt; sehen Sie den\n'
printf 'Bericht trotzdem durch, bevor Sie ihn veroeffentlichen.\n'