first commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
ARG WAZUH_VERSION=4.14.8
|
||||
FROM wazuh/wazuh-manager:${WAZUH_VERSION}
|
||||
|
||||
# openssh-clients: Agentless per SSH; diffutils: wazuh-agentlessd vergleicht Befehlsausgaben mit diff
|
||||
RUN dnf install -y openssh-clients diffutils && dnf clean all && rm -rf /var/cache/dnf
|
||||
|
||||
COPY common/sshpull_hosts.py manager/generate.py /opt/sshpull/
|
||||
COPY manager/agentless/ /opt/sshpull/agentless/
|
||||
COPY manager/rules/ /opt/sshpull/rules/
|
||||
COPY manager/entrypoint-scripts/ /entrypoint-scripts/
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Agentless-Befehls-Diff per SSH (Schluessel, ohne expect).
|
||||
# Aufruf durch wazuh-agentlessd: [use_sudo] <user@host> <hostname aus hosts.yml>
|
||||
# Fuehrt /var/ossec/etc/sshpull/commands/<hostname>.sh auf dem Host aus;
|
||||
# wazuh-agentlessd vergleicht die Ausgabe mit dem letzten Lauf und alarmiert bei Aenderungen.
|
||||
|
||||
[ "$1" = "test" ] && [ "$2" = "test" ] && exit 0
|
||||
|
||||
SUDO=""
|
||||
if [ "$1" = "use_sudo" ]; then SUDO="sudo -n"; shift; fi
|
||||
TARGET="$1"
|
||||
CMDS="/var/ossec/etc/sshpull/commands/$2.sh"
|
||||
case "$2" in */*|"") echo "ERROR: ungueltiger Hostname '$2'"; exit 1;; esac
|
||||
[ -r "$CMDS" ] || { echo "ERROR: $CMDS fehlt"; exit 1; }
|
||||
|
||||
ERR=$(mktemp)
|
||||
OUT=$(mktemp)
|
||||
trap 'rm -f "$ERR" "$OUT"' EXIT
|
||||
|
||||
echo "INFO: Starting."
|
||||
# LC_ALL=C: stabile Ausgabe, sonst gibt es Diffs nur wegen Locale-Wechseln
|
||||
{ echo "export LC_ALL=C; unset HISTFILE"; cat "$CMDS"; } \
|
||||
| ssh -F /var/ossec/etc/sshpull/ssh_config "$TARGET" $SUDO sh -s >"$OUT" 2>"$ERR"
|
||||
RC=$?
|
||||
# 255 = ssh selbst ist gescheitert; andere Codes kommen vom letzten Befehl
|
||||
if [ "$RC" -eq 255 ] || { [ "$RC" -ne 0 ] && [ ! -s "$OUT" ]; }; then
|
||||
MSG=$(tr '\n"\\' ' ' < "$ERR" | cut -c1-400)
|
||||
echo "LOG: {\"sshpull_status\":{\"host\":\"${TARGET#*@}\",\"check\":\"commands\",\"status\":\"down\",\"message\":\"rc=$RC $MSG\"}}"
|
||||
echo "ERROR: ssh zu $TARGET fehlgeschlagen (rc=$RC): $MSG"
|
||||
exit 1
|
||||
fi
|
||||
echo "STORE: now"
|
||||
# Zeilen, die wie Steuerzeilen von agentlessd aussehen, entschaerfen
|
||||
sed -e 's/^\(ERROR\|INFO\|FWD\|LOG\|STORE\): /\1 : /' "$OUT"
|
||||
exit 0
|
||||
Executable
+159
@@ -0,0 +1,159 @@
|
||||
#!/var/ossec/framework/python/bin/python3
|
||||
"""Agentless-Datei-Integritaetspruefung per SSH (Schluessel, ohne expect).
|
||||
|
||||
Aufruf durch wazuh-agentlessd: [use_sudo] <user@host> <pfad> [<pfad> ...]
|
||||
|
||||
Warum nicht das FWD:-Protokoll von Wazuh? Damit landen alle Hosts in der
|
||||
FIM-Datenbank des Managers (Agent 000), nur nach Pfad geordnet -> /etc/passwd
|
||||
von Host A, Host B und vom Manager selbst ueberschreiben sich gegenseitig, und
|
||||
der erste Lauf jedes Hosts erzeugt pro Datei einen "File added"-Alarm.
|
||||
|
||||
Stattdessen: eigene Baseline pro Host (agentless/.sshpull/<host>.json), Vergleich
|
||||
hier, und nur echte Aenderungen als JSON ueber "LOG: " an analysisd
|
||||
(Regeln: etc/rules/sshpull_rules.xml).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
SSH_CONFIG = "/var/ossec/etc/sshpull/ssh_config"
|
||||
STATE_DIR = "/var/ossec/agentless/.sshpull"
|
||||
MAX_EVENTS = 200 # mehr Aenderungen pro Lauf -> nur Zusammenfassung
|
||||
MAX_LINE = 2000 # agentlessd liest Zeilen mit max. 2048 Bytes
|
||||
TIMEOUT = 3000
|
||||
|
||||
REMOTE = r"""
|
||||
export LC_ALL=C
|
||||
unset HISTFILE
|
||||
if find / -maxdepth 0 -printf '' >/dev/null 2>&1; then
|
||||
find {paths} -xdev -type f -printf 'S\t%s\t%m\t%U\t%G\t%p\n' 2>/dev/null
|
||||
else
|
||||
find {paths} -xdev -type f 2>/dev/null | while IFS= read -r f; do
|
||||
printf 'S\t%s\t%s\t%s\t%s\t%s\n' $(stat -c '%s %a %u %g' "$f" 2>/dev/null) "$f"
|
||||
done
|
||||
fi
|
||||
find {paths} -xdev -type f -print0 2>/dev/null | xargs -0 -r sha256sum 2>/dev/null | sed 's/^/H\t/'
|
||||
printf 'E\tEND\n'
|
||||
"""
|
||||
|
||||
|
||||
def out(prefix, text):
|
||||
print(f"{prefix}: {text}", flush=True)
|
||||
|
||||
|
||||
def log_event(data):
|
||||
line = json.dumps({"sshpull_fim": data}, separators=(",", ":"))
|
||||
if len(line) > MAX_LINE:
|
||||
for k in ("sha256_before", "sha256_after"):
|
||||
data.pop(k, None)
|
||||
line = json.dumps({"sshpull_fim": data}, separators=(",", ":"))
|
||||
if len(line) <= MAX_LINE:
|
||||
out("LOG", line)
|
||||
|
||||
|
||||
def scan(target, sudo, paths):
|
||||
quoted = " ".join("'" + p + "'" for p in paths)
|
||||
cmd = ["ssh", "-F", SSH_CONFIG, target] + (["sudo", "-n"] if sudo else []) + ["sh", "-s"]
|
||||
proc = subprocess.run(cmd, input=REMOTE.replace("{paths}", quoted), capture_output=True,
|
||||
text=True, errors="replace", timeout=TIMEOUT)
|
||||
lines = proc.stdout.split("\n")
|
||||
if proc.returncode == 255 or "E\tEND" not in lines:
|
||||
raise RuntimeError(f"rc={proc.returncode} {proc.stderr.strip()[:300]}")
|
||||
|
||||
files = {}
|
||||
hashes = {}
|
||||
for ln in lines:
|
||||
if ln.startswith("S\t"):
|
||||
parts = ln.split("\t", 5) # S, size, perm, uid, gid, path
|
||||
if len(parts) == 6 and parts[1].isdigit():
|
||||
files[parts[5]] = [int(parts[1]), parts[2], parts[3], parts[4], ""]
|
||||
elif ln.startswith("H\t") and len(ln) > 68 and not ln.startswith("H\t\\"):
|
||||
hashes[ln[68:]] = ln[2:66]
|
||||
for path, h in hashes.items():
|
||||
if path in files:
|
||||
files[path][4] = h
|
||||
return files
|
||||
|
||||
|
||||
def compare(host, old, new):
|
||||
events = []
|
||||
for path in sorted(new.keys() - old.keys()):
|
||||
s, p, u, g, h = new[path]
|
||||
events.append({"event": "added", "host": host, "path": path, "size_after": s,
|
||||
"perm_after": p, "uid_after": u, "gid_after": g, "sha256_after": h})
|
||||
for path in sorted(old.keys() - new.keys()):
|
||||
s, p, u, g, h = old[path]
|
||||
events.append({"event": "deleted", "host": host, "path": path, "size_before": s,
|
||||
"perm_before": p, "uid_before": u, "gid_before": g, "sha256_before": h})
|
||||
for path in sorted(old.keys() & new.keys()):
|
||||
o, n = old[path], new[path]
|
||||
changed = [name for i, name in enumerate(("size", "perm", "uid", "gid"))
|
||||
if o[i] != n[i]]
|
||||
if o[4] and n[4] and o[4] != n[4]: # leerer Hash = Datei war nicht lesbar
|
||||
changed.append("sha256")
|
||||
if changed:
|
||||
ev = {"event": "modified", "host": host, "path": path, "changed": ",".join(changed)}
|
||||
for i, name in enumerate(("size", "perm", "uid", "gid", "sha256")):
|
||||
ev[f"{name}_before"], ev[f"{name}_after"] = o[i], n[i]
|
||||
events.append(ev)
|
||||
return events
|
||||
|
||||
|
||||
def main(argv):
|
||||
if argv[:2] == ["test", "test"]:
|
||||
return 0
|
||||
sudo = False
|
||||
if argv and argv[0] == "use_sudo":
|
||||
sudo, argv = True, argv[1:]
|
||||
if len(argv) < 2:
|
||||
out("ERROR", "Aufruf: [use_sudo] <user@host> <pfade...>")
|
||||
return 1
|
||||
target, paths = argv[0], argv[1:]
|
||||
host = target.split("@", 1)[-1]
|
||||
state_file = os.path.join(STATE_DIR, f"{host}.json")
|
||||
|
||||
out("INFO", "Starting.")
|
||||
try:
|
||||
new = scan(target, sudo, paths)
|
||||
except (RuntimeError, subprocess.TimeoutExpired) as exc:
|
||||
print("LOG: " + json.dumps({"sshpull_status": {"host": host, "check": "integrity",
|
||||
"status": "down", "message": str(exc)[:500]}}))
|
||||
out("ERROR", f"ssh zu {target} fehlgeschlagen: {exc}")
|
||||
return 1
|
||||
|
||||
try:
|
||||
with open(state_file, encoding="utf-8") as fh:
|
||||
state = json.load(fh)
|
||||
except (OSError, ValueError):
|
||||
state = None
|
||||
|
||||
if state is None or state.get("paths") != paths or not state.get("files"):
|
||||
log_event({"event": "baseline", "host": host, "files": len(new)})
|
||||
out("INFO", f"Baseline angelegt: {len(new)} Dateien")
|
||||
elif not new and state["files"]:
|
||||
# Kein einziges Ergebnis, vorher aber Dateien -> eher Rechte-/Mountproblem als Loeschung
|
||||
out("ERROR", f"Scan von {target} lieferte 0 Dateien (vorher {len(state['files'])}) - Baseline bleibt")
|
||||
return 1
|
||||
else:
|
||||
events = compare(host, state["files"], new)
|
||||
for ev in events[:MAX_EVENTS]:
|
||||
log_event(ev)
|
||||
if len(events) > MAX_EVENTS:
|
||||
counts = {k: sum(1 for e in events if e["event"] == k) for k in ("added", "modified", "deleted")}
|
||||
log_event({"event": "summary", "host": host, "total": len(events),
|
||||
"reported": MAX_EVENTS, **counts})
|
||||
out("INFO", f"{len(new)} Dateien, {len(events)} Aenderung(en)")
|
||||
|
||||
os.makedirs(STATE_DIR, mode=0o750, exist_ok=True)
|
||||
tmp = state_file + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as fh:
|
||||
json.dump({"paths": paths, "files": new}, fh, separators=(",", ":"))
|
||||
os.replace(tmp, state_file)
|
||||
out("INFO", "Finished.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
# Wird von /etc/cont-init.d/2-manager vor "wazuh-control start" ausgefuehrt.
|
||||
set -e
|
||||
|
||||
# Eigene Agentless-Skripte und Regeln (bei jedem Start aktualisieren)
|
||||
install -o root -g wazuh -m 750 /opt/sshpull/agentless/* /var/ossec/agentless/
|
||||
install -o wazuh -g wazuh -m 660 /opt/sshpull/rules/sshpull_rules.xml /var/ossec/etc/rules/
|
||||
|
||||
/var/ossec/framework/python/bin/python3 /opt/sshpull/generate.py
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Erzeugt beim Start des Managers aus /sshpull/hosts.yml:
|
||||
|
||||
* /var/ossec/etc/sshpull/ssh_config SSH-Client-Config (Alias = Hostname)
|
||||
* /var/ossec/etc/sshpull/keys/* Kopie der Schluessel (Owner wazuh)
|
||||
* /var/ossec/etc/sshpull/commands/*.sh Befehle fuer den Diff-Check pro Host
|
||||
* einen <ossec_config>-Block in ossec.conf mit <agentless> und <localfile>
|
||||
|
||||
Laeuft mit dem Wazuh-eigenen Python, bevor wazuh-control startet.
|
||||
"""
|
||||
|
||||
import grp
|
||||
import os
|
||||
import pwd
|
||||
import shutil
|
||||
import sys
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
sys.path.insert(0, "/opt/sshpull")
|
||||
import sshpull_hosts # noqa: E402
|
||||
|
||||
HOSTS_FILE = "/sshpull/hosts.yml"
|
||||
SSH_SRC = "/sshpull/ssh"
|
||||
LOG_DIR = "/remote-logs"
|
||||
BASE = "/var/ossec/etc/sshpull"
|
||||
OSSEC_CONF = "/var/ossec/etc/ossec.conf"
|
||||
BEGIN = "<!-- SSHPULL BEGIN (automatisch erzeugt) -->"
|
||||
END = "<!-- SSHPULL END -->"
|
||||
|
||||
UID = pwd.getpwnam("wazuh").pw_uid
|
||||
GID = grp.getgrnam("wazuh").gr_gid
|
||||
|
||||
|
||||
def write(path, content, mode):
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(content)
|
||||
os.chown(path, UID, GID)
|
||||
os.chmod(path, mode)
|
||||
|
||||
|
||||
def mkdir(path, mode=0o750):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
os.chown(path, UID, GID)
|
||||
os.chmod(path, mode)
|
||||
|
||||
|
||||
def agentless_block(h):
|
||||
host = f"{'use_sudo ' if h['sudo'] else ''}{h['user']}@{h['name']}"
|
||||
out = []
|
||||
integ = h["integrity"]
|
||||
if integ.get("enabled") and integ.get("paths"):
|
||||
out.append(f""" <agentless>
|
||||
<type>sshpull_integrity_linux</type>
|
||||
<frequency>{int(integ['interval'])}</frequency>
|
||||
<host>{escape(host)}</host>
|
||||
<state>periodic</state>
|
||||
<arguments>{escape(' '.join(integ['paths']))}</arguments>
|
||||
</agentless>""")
|
||||
cmds = h["commands"]
|
||||
if cmds.get("enabled") and cmds.get("run"):
|
||||
out.append(f""" <agentless>
|
||||
<type>sshpull_generic_diff</type>
|
||||
<frequency>{int(cmds['interval'])}</frequency>
|
||||
<host>{escape(host)}</host>
|
||||
<state>periodic_diff</state>
|
||||
<arguments>{escape(h['name'])}</arguments>
|
||||
</agentless>""")
|
||||
return out
|
||||
|
||||
|
||||
def localfile_block(h):
|
||||
out = []
|
||||
for src in h["logs"]:
|
||||
fmt = "syslog" if src["type"] == "journal" else src["format"]
|
||||
out.append(f""" <localfile>
|
||||
<log_format>{fmt}</log_format>
|
||||
<location>{LOG_DIR}/{h['name']}/{src['name']}.log</location>
|
||||
</localfile>""")
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.exists(HOSTS_FILE):
|
||||
print("sshpull: keine hosts.yml gefunden - ueberspringe")
|
||||
return 0
|
||||
try:
|
||||
cfg = sshpull_hosts.load(HOSTS_FILE)
|
||||
except (sshpull_hosts.ConfigError, OSError, ValueError) as exc:
|
||||
print(f"sshpull: FEHLER in hosts.yml: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
mkdir(BASE)
|
||||
keys = os.path.join(BASE, "keys")
|
||||
cmd_dir = os.path.join(BASE, "commands")
|
||||
for d in (keys, cmd_dir):
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
mkdir(d, 0o700 if d == keys else 0o750)
|
||||
|
||||
for key in {h["identity_file"] for h in cfg["hosts"]}:
|
||||
src = os.path.join(SSH_SRC, key)
|
||||
if not os.path.isfile(src):
|
||||
print(f"sshpull: WARNUNG: Schluessel {src} fehlt", file=sys.stderr)
|
||||
continue
|
||||
dst = os.path.join(keys, key)
|
||||
shutil.copyfile(src, dst)
|
||||
os.chown(dst, UID, GID)
|
||||
os.chmod(dst, 0o600)
|
||||
|
||||
write(os.path.join(BASE, "ssh_config"),
|
||||
sshpull_hosts.render_ssh_config(cfg, keys, f"{SSH_SRC}/known_hosts"), 0o640)
|
||||
|
||||
blocks = []
|
||||
for h in cfg["hosts"]:
|
||||
write(os.path.join(cmd_dir, f"{h['name']}.sh"),
|
||||
"\n".join(h["commands"].get("run") or []) + "\n", 0o640)
|
||||
blocks += agentless_block(h) + localfile_block(h)
|
||||
|
||||
# Verbindungsstatus des Collectors (Zustandswechsel up/down)
|
||||
blocks.append(f""" <localfile>
|
||||
<log_format>json</log_format>
|
||||
<location>{LOG_DIR}/_status/status.log</location>
|
||||
</localfile>""")
|
||||
|
||||
with open(OSSEC_CONF, encoding="utf-8") as fh:
|
||||
conf = fh.read()
|
||||
if BEGIN in conf: # alten Block entfernen (falls ossec.conf nicht neu gemountet wurde)
|
||||
conf = conf[:conf.index(BEGIN)] + conf[conf.index(END) + len(END):]
|
||||
conf = conf.rstrip() + "\n\n" + BEGIN + "\n<ossec_config>\n" + \
|
||||
"\n\n".join(blocks) + "\n</ossec_config>\n" + END + "\n"
|
||||
with open(OSSEC_CONF, "w", encoding="utf-8") as fh:
|
||||
fh.write(conf)
|
||||
|
||||
print(f"sshpull: {len(cfg['hosts'])} Host(s) in ossec.conf eingetragen")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,85 @@
|
||||
<!--
|
||||
Regeln fuer den SSH-Pull-Betrieb (wird bei jedem Manager-Start nach etc/rules kopiert).
|
||||
sshpull_fim: Datei-Integritaet aus manager/agentless/sshpull_integrity_linux
|
||||
sshpull_status: Verbindungsstatus (Collector und Agentless-Checks)
|
||||
-->
|
||||
<group name="sshpull,">
|
||||
|
||||
<!-- ===== Datei-Integritaet ===== -->
|
||||
<rule id="100500" level="0">
|
||||
<decoded_as>json</decoded_as>
|
||||
<field name="sshpull_fim.event">\.+</field>
|
||||
<description>sshpull FIM-Ereignis</description>
|
||||
</rule>
|
||||
|
||||
<rule id="100501" level="3">
|
||||
<if_sid>100500</if_sid>
|
||||
<field name="sshpull_fim.event">^baseline$</field>
|
||||
<description>$(sshpull_fim.host): FIM-Baseline angelegt ($(sshpull_fim.files) Dateien)</description>
|
||||
<group>sshpull_fim,</group>
|
||||
</rule>
|
||||
|
||||
<rule id="100502" level="5">
|
||||
<if_sid>100500</if_sid>
|
||||
<field name="sshpull_fim.event">^added$</field>
|
||||
<description>$(sshpull_fim.host): Datei hinzugefuegt: $(sshpull_fim.path)</description>
|
||||
<group>sshpull_fim,syscheck_entry_added,pci_dss_11.5,gdpr_II_5.1.f,</group>
|
||||
</rule>
|
||||
|
||||
<rule id="100503" level="7">
|
||||
<if_sid>100500</if_sid>
|
||||
<field name="sshpull_fim.event">^modified$</field>
|
||||
<description>$(sshpull_fim.host): Datei geaendert ($(sshpull_fim.changed)): $(sshpull_fim.path)</description>
|
||||
<group>sshpull_fim,syscheck_entry_modified,pci_dss_11.5,gdpr_II_5.1.f,</group>
|
||||
</rule>
|
||||
|
||||
<rule id="100504" level="7">
|
||||
<if_sid>100500</if_sid>
|
||||
<field name="sshpull_fim.event">^deleted$</field>
|
||||
<description>$(sshpull_fim.host): Datei geloescht: $(sshpull_fim.path)</description>
|
||||
<group>sshpull_fim,syscheck_entry_deleted,pci_dss_11.5,gdpr_II_5.1.f,</group>
|
||||
</rule>
|
||||
|
||||
<rule id="100505" level="10">
|
||||
<if_sid>100500</if_sid>
|
||||
<field name="sshpull_fim.event">^summary$</field>
|
||||
<description>$(sshpull_fim.host): $(sshpull_fim.total) Dateiaenderungen auf einmal (neu $(sshpull_fim.added), geaendert $(sshpull_fim.modified), geloescht $(sshpull_fim.deleted)) - Update oder Kompromittierung?</description>
|
||||
<group>sshpull_fim,</group>
|
||||
</rule>
|
||||
|
||||
<rule id="100506" level="10">
|
||||
<if_sid>100502,100503,100504</if_sid>
|
||||
<field name="sshpull_fim.path" type="pcre2">^/etc/(passwd|shadow|group|gshadow|sudoers|ssh/sshd_config|pam\.d/)|^/etc/sudoers\.d/|/\.ssh/authorized_keys|^/etc/(crontab|cron\.d/)|^/etc/ld\.so\.preload$</field>
|
||||
<description>$(sshpull_fim.host): Sicherheitskritische Datei $(sshpull_fim.event): $(sshpull_fim.path)</description>
|
||||
<group>sshpull_fim,</group>
|
||||
</rule>
|
||||
|
||||
<rule id="100507" level="10">
|
||||
<if_sid>100502,100503</if_sid>
|
||||
<field name="sshpull_fim.perm_after" type="pcre2">^[2-7][0-7]{3}$</field>
|
||||
<description>$(sshpull_fim.host): Datei mit SUID/SGID-Bit ($(sshpull_fim.perm_after)): $(sshpull_fim.path)</description>
|
||||
<group>sshpull_fim,</group>
|
||||
</rule>
|
||||
|
||||
<!-- ===== Verbindungsstatus ===== -->
|
||||
<rule id="100510" level="0">
|
||||
<decoded_as>json</decoded_as>
|
||||
<field name="sshpull_status.status">\.+</field>
|
||||
<description>sshpull Statusmeldung</description>
|
||||
</rule>
|
||||
|
||||
<rule id="100511" level="7">
|
||||
<if_sid>100510</if_sid>
|
||||
<field name="sshpull_status.status">^down$</field>
|
||||
<description>$(sshpull_status.host): SSH-Abholung ($(sshpull_status.check)) gestoert: $(sshpull_status.message)</description>
|
||||
<group>sshpull_status,</group>
|
||||
</rule>
|
||||
|
||||
<rule id="100512" level="3">
|
||||
<if_sid>100510</if_sid>
|
||||
<field name="sshpull_status.status">^up$</field>
|
||||
<description>$(sshpull_status.host): SSH-Abholung ($(sshpull_status.check)) wieder verbunden</description>
|
||||
<group>sshpull_status,</group>
|
||||
</rule>
|
||||
|
||||
</group>
|
||||
Reference in New Issue
Block a user