feat: tools/waechter.ps1 -- zaehlt offene Tueren und rechnet Dateien nach
Die Statusseite war waehrend des Einbruchs die ganze Zeit gruen. Sie waere auch
gruen geblieben, wenn der Miner gelaufen waere. Es fehlte kein Bildschirm, auf
dem man es haette sehen koennen -- es fehlte etwas, das VON SICH AUS Bescheid
sagt. Deshalb kein Frontend, sondern zwei Pruefungen mit Meldeweg.
1. Ports, die auf allen Adressen lauschen, gegen einen hinterlegten Stand.
Port 3000 stand elf Tage offen; das haette am ersten Tag gemeldet werden
koennen. Dazu eine feste Liste heikler Ports, die auch dann warnt, wenn
der Zustand "schon immer so" war -- 3000 war auch schon immer so.
2. Pruefsummen ueber sshd_config, administrators_authorized_keys, beide
Compose-Dateien und Giteas app.ini und .gitconfig. Der Angriff WAR eine
Datei, die sich um 21:13:57 aenderte.
Gemeldet wird nur bei Aenderung, mit Merker in den Einstellungen. Ein Waechter,
der taeglich meckert, wird stummgeschaltet und ist dann schlechter als keiner.
VOR DEM EINRICHTEN AN BEIDEN FAELLEN BEWIESEN statt behauptet:
Testport 45999 geoeffnet -> "NEU offen: 45999"
Datei geaendert -> Meldung raus, HTTP 204
zurueckgestellt -> "Wieder in Ordnung (Merker geleert)"
Und er prueft zuerst, ob er ueberhaupt etwas sehen kann: findet er keine
Objekte, meldet er "ungueltig" statt "sauber". Das ist die Lehre aus zwei
Suchen in dieser Nacht, die genau das verwechselt haben.
Die Statusseite zeigt den Stand mit; meldet der Waechter sich zwei Stunden
nicht, ist DAS der Befund -- er laeuft alle 15 Minuten.
Im Bericht ausserdem nachgetragen: die Inventur von aussen (NPM-Verwaltung und
MariaDB standen offen, beide jetzt gesperrt), die abgeschalteten Ueberbleibsel
von Caddy und cloudflared, und zwei Irrtuemer von mir -- "Docker geht an der
Firewall vorbei" und "der MariaDB-root hat kein Passwort". Beide waren aus
Messungen geschlossen, die die Frage nicht beantworten konnten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,3 +12,4 @@ server/web/
|
|||||||
|
|
||||||
# Die Ablage des Dienstes: Bilder und SQLite. Gehoert ins Volume, nie ins Repo.
|
# Die Ablage des Dienstes: Bilder und SQLite. Gehoert ins Volume, nie ins Repo.
|
||||||
data/
|
data/
|
||||||
|
.waechter-basis.json
|
||||||
|
|||||||
@@ -168,3 +168,94 @@ Seitdem laufen die Angriffe ins Leere:
|
|||||||
00:53:12 Connection closed by invalid user root 31.77.227.120 [preauth]
|
00:53:12 Connection closed by invalid user root 31.77.227.120 [preauth]
|
||||||
alle 12 erfolgreichen Anmeldungen: eine IP, die des Betreibers
|
alle 12 erfolgreichen Anmeldungen: eine IP, die des Betreibers
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nachtrag 13.08.2026, vormittags: die Inventur, und drei Irrtümer von mir
|
||||||
|
|
||||||
|
### Was von außen wirklich offen war
|
||||||
|
|
||||||
|
Erst eine Messung **vom Heimrechner** hat die Lage geklärt — vom Server aus ist
|
||||||
|
sie nicht zu beantworten:
|
||||||
|
|
||||||
|
```
|
||||||
|
vorher nachher
|
||||||
|
80 True (gewollt) 80 True
|
||||||
|
81 True NPM-Verwaltung 81 False
|
||||||
|
135 True Windows-RPC 135 False
|
||||||
|
3306 True MariaDB 3306 False
|
||||||
|
445/3389/5985/9000/9443/11000 durchweg False
|
||||||
|
```
|
||||||
|
|
||||||
|
**Die Verwaltungsoberfläche des Reverse-Proxys und eine Datenbank standen offen
|
||||||
|
im Internet.** Beides ist jetzt per Firewall gesperrt; die NPM-Oberfläche
|
||||||
|
erreicht man weiter über `https://proxy.d4rkst3r.de`, weil sie containerintern
|
||||||
|
auf `127.0.0.1:81` läuft.
|
||||||
|
|
||||||
|
### ⚠️ Irrtum 1: „Docker geht an der Windows-Firewall vorbei"
|
||||||
|
|
||||||
|
**Falsch.** Port 81 wird von Docker veröffentlicht, und die Sperrregel greift
|
||||||
|
dort sehr wohl — nachgemessen von außen. Ich hatte das Gegenteil behauptet,
|
||||||
|
nachdem ich es *vom Server aus* geprüft hatte, und deswegen sogar eine Regel
|
||||||
|
wieder gelöscht. Verkehr an die eigene öffentliche Adresse durchläuft die
|
||||||
|
Eingangsregeln nicht; diese Messung kann die Frage gar nicht beantworten.
|
||||||
|
|
||||||
|
### ⚠️ Irrtum 2: „Der MariaDB-root hat kein Passwort"
|
||||||
|
|
||||||
|
**Falsch.** Er hat eines — das FiveM benutzt. Dass ich ohne hineinkam, liegt an
|
||||||
|
der Standardeinrichtung von MariaDB unter Windows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"plugin": "mysql_native_password",
|
||||||
|
"auth_or": [ { }, { "plugin": "gssapi", "authentication_string": "SID:BA" } ]
|
||||||
|
```
|
||||||
|
|
||||||
|
`auth_or` heißt „oder": Passwort **oder** Windows-Authentifizierung, und
|
||||||
|
`SID:BA` ist die Gruppe der lokalen Administratoren. Ich war als Administrator
|
||||||
|
unterwegs. Ein Angreifer aus dem Internet hat keine Windows-Identität — MySQL
|
||||||
|
fällt als Einbruchsweg aus.
|
||||||
|
|
||||||
|
Meine Prüfung davor war kaputt (`JSON_LENGTH` auf eine Zeichenkette gibt immer
|
||||||
|
`NULL`), und statt das zu bemerken, habe ich das Ergebnis geglaubt.
|
||||||
|
|
||||||
|
### Vergessene Dienste — der Verdacht des Betreibers war richtig
|
||||||
|
|
||||||
|
```
|
||||||
|
Caddy-AutoStart [Ready] beim Systemstart nie gelaufen
|
||||||
|
CloudflaredTunnel [Ready] beim Systemstart nie gelaufen
|
||||||
|
C:\Users\Darkster\.cloudflared\ Tunnel-Zugangsdaten vom 23.03.2026
|
||||||
|
```
|
||||||
|
|
||||||
|
Beide waren **aktiv** und wären beim nächsten Neustart angesprungen. Ein Tunnel
|
||||||
|
ist ein Eingang, der an jeder Firewall vorbeigeht, weil er von innen aufgebaut
|
||||||
|
wird. Umfang war eng (`fivem-api.d4rkst3r.de` → `localhost:30121`, wo nichts
|
||||||
|
lauscht), der Caddyfile leer. Beide Aufgaben sind **abgeschaltet, nicht
|
||||||
|
gelöscht**.
|
||||||
|
|
||||||
|
**Offen für den Betreiber:** den Tunnel `3a4a599b-…` im Cloudflare-Konto
|
||||||
|
löschen. Solange er dort registriert ist, sind die Zugangsdaten auf der Platte
|
||||||
|
gültig.
|
||||||
|
|
||||||
|
Außerdem gefunden: **Azure Arc** (vier Dienste, seit 04.08., Status
|
||||||
|
`Disconnected`, ohne Abonnement) — vermutlich aus dem Anbieter-Abbild. Und
|
||||||
|
**IPBan** läuft, ein Brute-Force-Blocker.
|
||||||
|
|
||||||
|
### Der Wächter
|
||||||
|
|
||||||
|
`tools/waechter.ps1`, alle 15 Minuten. Zählt die Ports, die auf allen Adressen
|
||||||
|
lauschen, und rechnet Prüfsummen über `sshd_config`,
|
||||||
|
`administrators_authorized_keys`, beide Compose-Dateien sowie Giteas `app.ini`
|
||||||
|
und `.gitconfig`. Gemeldet wird **nur bei Änderung**, und die Statusseite zeigt
|
||||||
|
den Stand.
|
||||||
|
|
||||||
|
Vor dem Einrichten an beiden Fällen bewiesen, statt es zu behaupten:
|
||||||
|
|
||||||
|
```
|
||||||
|
Testport 45999 geoeffnet -> "NEU offen: 45999"
|
||||||
|
Datei harmlos geaendert -> Meldung raus, HTTP 204
|
||||||
|
Datei zurueckgestellt -> "Wieder in Ordnung (Merker geleert)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Er prüft zuerst, ob er überhaupt etwas sehen kann, und meldet sonst
|
||||||
|
„ungültig" statt „sauber" — die Lehre aus zwei Suchen, die genau das
|
||||||
|
verwechselt haben.
|
||||||
|
|||||||
@@ -221,6 +221,59 @@ async function pruefen(): Promise<Antwort> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- Der Waechter ---------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Geschrieben von tools/waechter.ps1 auf dem WIRT -- der Dienst im Container
|
||||||
|
// sieht weder die Ports der Maschine noch ihre Dateien. Hier steht nur, was
|
||||||
|
// der Waechter zuletzt gefunden hat.
|
||||||
|
//
|
||||||
|
// FEHLT DER EINTRAG GANZ, ist der Waechter nicht eingerichtet. Das ist kein
|
||||||
|
// Fehler des Dienstes, aber es gehoert gesagt: eine Statusseite, die
|
||||||
|
// schweigt, weil niemand hinsieht, ist genau das Problem, gegen das der
|
||||||
|
// Waechter gebaut wurde.
|
||||||
|
const wRoh = getSetting('waechter.letzte')
|
||||||
|
if (!wRoh) {
|
||||||
|
pruefungen.push({ was: 'waechter', ok: true, hinweis: 'nicht eingerichtet' })
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const w = JSON.parse(wRoh) as {
|
||||||
|
at: number
|
||||||
|
ok: boolean
|
||||||
|
ports?: number
|
||||||
|
heikel?: number[]
|
||||||
|
befund?: string[]
|
||||||
|
}
|
||||||
|
const stunden = (now() - w.at) / 3_600_000
|
||||||
|
if (stunden > 2) {
|
||||||
|
// Er laeuft alle 15 Minuten. Zwei Stunden Schweigen heisst: er
|
||||||
|
// laeuft nicht mehr -- und ein Waechter, von dem man glaubt,
|
||||||
|
// dass er wacht, ist schlimmer als keiner.
|
||||||
|
pruefungen.push({
|
||||||
|
was: 'waechter',
|
||||||
|
ok: false,
|
||||||
|
hinweis: `meldet sich seit ${Math.round(stunden)} Stunden nicht`,
|
||||||
|
})
|
||||||
|
} else if (!w.ok) {
|
||||||
|
pruefungen.push({
|
||||||
|
was: 'waechter',
|
||||||
|
ok: false,
|
||||||
|
hinweis: (w.befund ?? []).join('; ') || 'Befund ohne Text',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const heikel = w.heikel?.length
|
||||||
|
pruefungen.push({
|
||||||
|
was: 'waechter',
|
||||||
|
ok: true,
|
||||||
|
hinweis: heikel
|
||||||
|
? `${w.ports} Ports offen, davon ${heikel} heikel`
|
||||||
|
: `${w.ports} Ports offen`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
pruefungen.push({ was: 'waechter', ok: false, hinweis: 'Eintrag unlesbar' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -- Das Urteil -----------------------------------------------------------
|
// -- Das Urteil -----------------------------------------------------------
|
||||||
//
|
//
|
||||||
// ZWEI STUFEN, und der Unterschied ist der zwischen "tut es nicht" und
|
// ZWEI STUFEN, und der Unterschied ist der zwischen "tut es nicht" und
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
# Wacht über die Sicherheitslage des Wirts: offene Türen und wichtige Dateien.
|
||||||
|
#
|
||||||
|
# pwsh -File tools\waechter.ps1 <- prüfen und ggf. melden
|
||||||
|
# pwsh -File tools\waechter.ps1 -Uebernehmen <- den JETZIGEN Stand als richtig eintragen
|
||||||
|
# pwsh -File tools\waechter.ps1 -Still <- prüfen, nur anzeigen, nichts melden
|
||||||
|
#
|
||||||
|
# WOZU. In der Nacht zum 13.08.2026 hat jemand über eine offene Tür in den
|
||||||
|
# Gitea geschrieben (docs/vorfall-2026-08-12-gitea.md). Aufgefallen ist es,
|
||||||
|
# weil danach ein Push nicht mehr ging -- nicht weil etwas es bemerkt hätte.
|
||||||
|
#
|
||||||
|
# DIE STATUSSEITE WAR DIE GANZE ZEIT GRÜN. Sie wäre auch grün geblieben, wenn
|
||||||
|
# der Miner gelaufen wäre. Es fehlte kein Bildschirm, auf dem man es hätte
|
||||||
|
# sehen können; es fehlte etwas, das VON SICH AUS Bescheid sagt.
|
||||||
|
#
|
||||||
|
# Zwei Prüfungen, und beide hätten den Vorfall doppelt erwischt:
|
||||||
|
#
|
||||||
|
# 1. OFFENE TÜREN ZÄHLEN. Port 3000 stand eiskalt elf Tage offen. Diese
|
||||||
|
# Prüfung hätte am ersten Tag gesagt: "da ist ein Port dazugekommen, den
|
||||||
|
# niemand eingetragen hat" -- und sie sagt es auch beim nächsten, den
|
||||||
|
# jemand beim Einrichten aus Bequemlichkeit aufmacht.
|
||||||
|
#
|
||||||
|
# 2. WICHTIGE DATEIEN NACHRECHNEN. Der Angriff war eine Datei, die sich um
|
||||||
|
# 21:13:57 änderte. Eine Prüfsumme über .gitconfig, app.ini, sshd_config
|
||||||
|
# und die Schlüsseldatei hätte das im nächsten Durchlauf gemeldet.
|
||||||
|
#
|
||||||
|
# GEMELDET WIRD NUR BEI ÄNDERUNG. Ein Wächter, der täglich meckert, wird
|
||||||
|
# stummgeschaltet -- und ist dann schlechter als keiner. Dieselbe Lehre wie bei
|
||||||
|
# den 900 Fahrzeugbildern.
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$Container = 'd4rk-media',
|
||||||
|
[switch]$Uebernehmen,
|
||||||
|
[switch]$Still
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
function Schritt($t) { Write-Host "==> $t" -ForegroundColor Cyan }
|
||||||
|
function Gut($t) { Write-Host " $t" -ForegroundColor Green }
|
||||||
|
function Warn($t) { Write-Host " $t" -ForegroundColor Yellow }
|
||||||
|
function Schlecht($t) { Write-Host " $t" -ForegroundColor Red }
|
||||||
|
|
||||||
|
# Ports, die man auf einem Server aus dem Internet NIE erreichbar haben will --
|
||||||
|
# unabhängig davon, ob sie beim Übernehmen des Standes schon dastanden. Diese
|
||||||
|
# Liste warnt also auch dann, wenn der Zustand "schon immer so" war. Genau das
|
||||||
|
# ist der Punkt: Port 3000 war auch "schon immer so".
|
||||||
|
$HEIKEL = @{
|
||||||
|
135 = 'RPC'
|
||||||
|
139 = 'NetBIOS'
|
||||||
|
445 = 'SMB-Dateifreigabe'
|
||||||
|
1433 = 'MS SQL'
|
||||||
|
3306 = 'MySQL'
|
||||||
|
3389 = 'RDP'
|
||||||
|
5432 = 'PostgreSQL'
|
||||||
|
5985 = 'WinRM (HTTP)'
|
||||||
|
5986 = 'WinRM (HTTPS)'
|
||||||
|
6379 = 'Redis'
|
||||||
|
27017 = 'MongoDB'
|
||||||
|
47001 = 'WinRM-Listener'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Was nachgerechnet wird. Änderungen daran sind entweder deine -- dann
|
||||||
|
# übernimmst du sie mit -Uebernehmen -- oder sie sind ein Befund.
|
||||||
|
$DATEIEN = @(
|
||||||
|
'C:\ProgramData\ssh\sshd_config'
|
||||||
|
'C:\ProgramData\ssh\administrators_authorized_keys'
|
||||||
|
'C:\docker\gitea\docker-compose.yml'
|
||||||
|
"$PSScriptRoot\..\docker-compose.yml"
|
||||||
|
)
|
||||||
|
# Dateien IN Containern, als "container:pfad".
|
||||||
|
$IM_CONTAINER = @(
|
||||||
|
'gitea:/data/gitea/conf/app.ini'
|
||||||
|
'gitea:/data/gitea/home/.gitconfig'
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- Stand erheben --------------------------------------------------------------
|
||||||
|
|
||||||
|
Schritt 'Offene Tueren zaehlen'
|
||||||
|
$offen = @(Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.LocalAddress -in '0.0.0.0', '::' } |
|
||||||
|
Select-Object -ExpandProperty LocalPort -Unique | Sort-Object)
|
||||||
|
Gut "$($offen.Count) Ports lauschen auf allen Adressen"
|
||||||
|
|
||||||
|
Schritt 'Wichtige Dateien nachrechnen'
|
||||||
|
$hashes = @{}
|
||||||
|
foreach ($d in $DATEIEN) {
|
||||||
|
$p = try { (Resolve-Path $d -ErrorAction Stop).Path } catch { $null }
|
||||||
|
if ($p) { $hashes[$d] = (Get-FileHash $p -Algorithm SHA256).Hash }
|
||||||
|
else { $hashes[$d] = 'FEHLT' }
|
||||||
|
}
|
||||||
|
foreach ($eintrag in $IM_CONTAINER) {
|
||||||
|
$c, $pfad = $eintrag -split ':', 2
|
||||||
|
# sha256sum IM Container: die Datei liegt in einem Volume, das der Wirt
|
||||||
|
# nicht ohne Weiteres lesen kann -- und ein Umweg ueber docker cp wuerde
|
||||||
|
# bei jedem Lauf eine Kopie anlegen.
|
||||||
|
$h = (docker exec $c sh -c "sha256sum '$pfad' 2>/dev/null | cut -d' ' -f1") -join ''
|
||||||
|
$hashes[$eintrag] = if ($h.Trim()) { $h.Trim().ToUpper() } else { 'FEHLT' }
|
||||||
|
}
|
||||||
|
Gut "$($hashes.Count) Dateien"
|
||||||
|
|
||||||
|
# -- Mit dem hinterlegten Stand vergleichen -------------------------------------
|
||||||
|
|
||||||
|
$basisDatei = Join-Path $PSScriptRoot '..\.waechter-basis.json'
|
||||||
|
$basis = if (Test-Path $basisDatei) { Get-Content $basisDatei -Raw | ConvertFrom-Json } else { $null }
|
||||||
|
|
||||||
|
$jetzt = [ordered]@{
|
||||||
|
ports = $offen
|
||||||
|
dateien = $hashes
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Uebernehmen -or -not $basis) {
|
||||||
|
$jetzt | ConvertTo-Json -Depth 5 | Set-Content $basisDatei -Encoding UTF8
|
||||||
|
if (-not $basis) {
|
||||||
|
Schritt 'Kein hinterlegter Stand -- den jetzigen als richtig eingetragen'
|
||||||
|
Warn 'Beim ERSTEN Lauf gibt es nichts zu vergleichen. Ab jetzt wird gemeldet,'
|
||||||
|
Warn 'was sich daran aendert. Sieh die Liste unten einmal durch.'
|
||||||
|
} else {
|
||||||
|
Gut 'Stand uebernommen.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$basis = Get-Content $basisDatei -Raw | ConvertFrom-Json
|
||||||
|
$neu = @($offen | Where-Object { $_ -notin $basis.ports })
|
||||||
|
$weg = @($basis.ports | Where-Object { $_ -notin $offen })
|
||||||
|
$geaendert = @()
|
||||||
|
foreach ($k in $hashes.Keys) {
|
||||||
|
$alt = $basis.dateien.$k
|
||||||
|
if ($alt -and $alt -ne $hashes[$k]) { $geaendert += $k }
|
||||||
|
}
|
||||||
|
$heikelOffen = @($offen | Where-Object { $HEIKEL.ContainsKey([int]$_) })
|
||||||
|
|
||||||
|
# -- Anzeigen -------------------------------------------------------------------
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
if ($neu.Count) { Schlecht "NEU offen: $($neu -join ', ')" }
|
||||||
|
if ($weg.Count) { Gut "nicht mehr offen: $($weg -join ', ')" }
|
||||||
|
if ($geaendert.Count) { foreach ($g in $geaendert) { Schlecht "GEAENDERT: $g" } }
|
||||||
|
if (-not $neu.Count -and -not $geaendert.Count) { Gut 'Keine Aenderung gegenueber dem hinterlegten Stand.' }
|
||||||
|
|
||||||
|
if ($heikelOffen.Count) {
|
||||||
|
Write-Host ''
|
||||||
|
Warn 'Diese lauschen auf allen Adressen und gehoeren nicht ins Internet:'
|
||||||
|
foreach ($p in $heikelOffen) { Warn (" {0,6} {1}" -f $p, $HEIKEL[[int]$p]) }
|
||||||
|
Warn 'OB sie von aussen erreichbar sind, sagt nur eine Messung VON AUSSEN --'
|
||||||
|
Warn 'vom Wirt aus antwortet die eigene Adresse auch dann, wenn die Firewall sperrt.'
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Melden ---------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Nur bei AENDERUNG, und nur einmal je Zustand. Dafuer wandert eine Kennung des
|
||||||
|
# gemeldeten Zustands in die Einstellungen: bleibt sie gleich, bleibt es still.
|
||||||
|
|
||||||
|
if ($Still) { Write-Host ''; Gut 'Nur angezeigt (-Still), nichts gemeldet.'; exit 0 }
|
||||||
|
|
||||||
|
$befund = @()
|
||||||
|
if ($neu.Count) { $befund += "NEU offen: " + ($neu -join ', ') }
|
||||||
|
if ($geaendert.Count) { $befund += "geaendert: " + (($geaendert | ForEach-Object { Split-Path $_ -Leaf }) -join ', ') }
|
||||||
|
|
||||||
|
$kennung = if ($befund.Count) { ($befund -join ' | ') } else { '' }
|
||||||
|
|
||||||
|
function LiesEinstellung($schluessel) {
|
||||||
|
$s = @"
|
||||||
|
const db = require('better-sqlite3')('/data/media.db', { readonly: true });
|
||||||
|
const r = db.prepare('SELECT value FROM settings WHERE key = ?').get('$schluessel');
|
||||||
|
console.log(r ? r.value : '');
|
||||||
|
"@ -replace "`r", ''
|
||||||
|
(docker exec $Container node -e $s 2>$null) -join ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function SchreibEinstellung($schluessel, $wert) {
|
||||||
|
$s = @"
|
||||||
|
let roh = '';
|
||||||
|
process.stdin.on('data', (d) => (roh += d)).on('end', () => {
|
||||||
|
const db = require('better-sqlite3')('/data/media.db');
|
||||||
|
db.prepare('INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?) ' +
|
||||||
|
'ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at')
|
||||||
|
.run('$schluessel', roh.trim(), Date.now());
|
||||||
|
});
|
||||||
|
"@ -replace "`r", ''
|
||||||
|
$wert | docker exec -i $Container node -e $s 2>&1 | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bericht fuer die Statusseite -- immer, auch wenn nichts zu melden ist.
|
||||||
|
$bericht = [ordered]@{
|
||||||
|
at = [long]([DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds())
|
||||||
|
ok = ($befund.Count -eq 0)
|
||||||
|
ports = $offen.Count
|
||||||
|
heikel = @($heikelOffen)
|
||||||
|
befund = @($befund)
|
||||||
|
}
|
||||||
|
SchreibEinstellung 'waechter.letzte' ($bericht | ConvertTo-Json -Compress)
|
||||||
|
|
||||||
|
$zuletzt = LiesEinstellung 'waechter.merker'
|
||||||
|
if ($kennung -eq $zuletzt) {
|
||||||
|
Write-Host ''
|
||||||
|
Gut 'Nichts Neues -- dieser Zustand wurde schon gemeldet.'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
SchreibEinstellung 'waechter.merker' $kennung
|
||||||
|
if (-not $kennung) { Write-Host ''; Gut 'Wieder in Ordnung (Merker geleert).'; exit 0 }
|
||||||
|
|
||||||
|
$webhook = LiesEinstellung 'discord.webhook'
|
||||||
|
if (-not $webhook) { Write-Host ''; Warn 'Kein Discord-Webhook hinterlegt -- nur angezeigt.'; exit 0 }
|
||||||
|
|
||||||
|
$felder = @()
|
||||||
|
if ($neu.Count) { $felder += @{ name = 'Neu offen'; value = ($neu -join ', '); inline = $true } }
|
||||||
|
if ($geaendert.Count) { $felder += @{ name = 'Geaenderte Dateien'; value = (($geaendert | ForEach-Object { '`' + (Split-Path $_ -Leaf) + '`' }) -join "`n"); inline = $false } }
|
||||||
|
|
||||||
|
$rumpf = @{
|
||||||
|
username = 'd4rk_media'
|
||||||
|
embeds = @(@{
|
||||||
|
title = 'Die Lage am Server hat sich geaendert'
|
||||||
|
description = "Etwas ist offen oder anders, was vorher nicht so war.`n" +
|
||||||
|
"Wenn du das selbst warst: ``pwsh -File tools\waechter.ps1 -Uebernehmen``"
|
||||||
|
color = 15158332
|
||||||
|
fields = $felder
|
||||||
|
footer = @{ text = 'D4RKST3R // WAECHTER' }
|
||||||
|
timestamp = (Get-Date).ToUniversalTime().ToString('o')
|
||||||
|
})
|
||||||
|
} | ConvertTo-Json -Depth 6
|
||||||
|
|
||||||
|
$antwort = $rumpf | curl.exe -s -o NUL -w '%{http_code}' -X POST `
|
||||||
|
-H 'content-type: application/json' --data-binary '@-' $webhook
|
||||||
|
Write-Host ''
|
||||||
|
if ($antwort -in '200', '204') { Gut "Gemeldet (HTTP $antwort)." } else { Schlecht "Melden fehlgeschlagen: HTTP $antwort" }
|
||||||
Reference in New Issue
Block a user