first commit
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
"""Gemeinsame Logik fuer Manager und Collector: hosts.yml laden, pruefen,
|
||||
Defaults anwenden und die SSH-Client-Konfiguration erzeugen.
|
||||
|
||||
Wird sowohl vom Wazuh-Python (3.10, im Manager-Image) als auch vom
|
||||
Collector (Python 3.12) importiert -> nur stdlib + PyYAML.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import re
|
||||
|
||||
import yaml
|
||||
|
||||
NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$")
|
||||
# Wazuh-Logformate, die fuer per SSH geholte Dateien sinnvoll sind
|
||||
FILE_FORMATS = {
|
||||
"syslog", "json", "apache", "nginx", "snort-full", "squid", "iis",
|
||||
"audit", "mysql_log", "postgresql_log", "djb-multilog", "multi-line",
|
||||
}
|
||||
|
||||
BUILTIN_DEFAULTS = {
|
||||
"user": "wazuh-pull",
|
||||
"port": 22,
|
||||
"identity_file": "id_ed25519",
|
||||
"sudo": False,
|
||||
"agent_tunnel": False,
|
||||
"tunnel_ports": [1514, 1515],
|
||||
"logs": [{"type": "journal"}],
|
||||
"integrity": {
|
||||
"enabled": True,
|
||||
"interval": 3600,
|
||||
"paths": ["/etc", "/usr/bin", "/usr/sbin", "/bin", "/sbin", "/boot"],
|
||||
},
|
||||
"commands": {
|
||||
"enabled": True,
|
||||
"interval": 3600,
|
||||
"run": [
|
||||
"ss -tulpn 2>/dev/null || netstat -tulpn",
|
||||
"cat /etc/passwd",
|
||||
"cat /etc/group",
|
||||
"find /etc/sudoers /etc/sudoers.d -type f -exec md5sum {} + 2>/dev/null",
|
||||
"crontab -l -u root 2>/dev/null; ls -la /etc/cron.d",
|
||||
"systemctl list-unit-files --state=enabled --no-pager 2>/dev/null",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
SSH_SETTINGS = {
|
||||
"host_key_checking": "yes", # yes | accept-new
|
||||
"connect_timeout": 15,
|
||||
"server_alive_interval": 30,
|
||||
}
|
||||
|
||||
|
||||
class ConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _merge(base, override):
|
||||
"""Rekursiver Merge fuer dicts, Listen werden komplett ersetzt."""
|
||||
out = copy.deepcopy(base)
|
||||
for key, val in (override or {}).items():
|
||||
if isinstance(val, dict) and isinstance(out.get(key), dict):
|
||||
out[key] = _merge(out[key], val)
|
||||
else:
|
||||
out[key] = copy.deepcopy(val)
|
||||
return out
|
||||
|
||||
|
||||
def _check_safe(value, what):
|
||||
# Pfade/Units landen in Remote-Shellskripten (gequotet) und ossec.conf.
|
||||
if not isinstance(value, str) or not value or any(c in value for c in "\n\r\0<>&\"'"):
|
||||
raise ConfigError(f"Ungueltiger Wert fuer {what}: {value!r}")
|
||||
return value
|
||||
|
||||
|
||||
def _normalize_logs(host, logs):
|
||||
result = []
|
||||
names = set()
|
||||
for i, src in enumerate(logs or []):
|
||||
if not isinstance(src, dict) or src.get("type") not in ("journal", "file"):
|
||||
raise ConfigError(f"{host}: logs[{i}] braucht type: journal oder type: file")
|
||||
src = dict(src)
|
||||
if src["type"] == "journal":
|
||||
src.setdefault("name", "journal")
|
||||
src["units"] = [_check_safe(u, f"{host}.units") for u in src.get("units", [])]
|
||||
if "priority" in src:
|
||||
src["priority"] = _check_safe(str(src["priority"]), f"{host}.priority")
|
||||
else:
|
||||
path = _check_safe(src.get("path"), f"{host}.logs[{i}].path")
|
||||
if not path.startswith("/"):
|
||||
raise ConfigError(f"{host}: logs[{i}].path muss absolut sein")
|
||||
default_name = re.sub(r"[^A-Za-z0-9._-]", "_", path.strip("/"))
|
||||
src.setdefault("name", default_name)
|
||||
src.setdefault("format", "syslog")
|
||||
if src["format"] not in FILE_FORMATS:
|
||||
raise ConfigError(f"{host}: unbekanntes format {src['format']!r}")
|
||||
if not NAME_RE.match(src["name"]):
|
||||
raise ConfigError(f"{host}: ungueltiger Log-Name {src['name']!r}")
|
||||
if src["name"] in names:
|
||||
raise ConfigError(f"{host}: Log-Name {src['name']!r} doppelt")
|
||||
names.add(src["name"])
|
||||
result.append(src)
|
||||
return result
|
||||
|
||||
|
||||
def load(path):
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
raw = yaml.safe_load(fh) or {}
|
||||
|
||||
defaults = _merge(BUILTIN_DEFAULTS, raw.get("defaults"))
|
||||
ssh = _merge(SSH_SETTINGS, raw.get("ssh"))
|
||||
if ssh["host_key_checking"] not in ("yes", "accept-new"):
|
||||
raise ConfigError("ssh.host_key_checking muss 'yes' oder 'accept-new' sein")
|
||||
|
||||
hosts = []
|
||||
seen = set()
|
||||
for i, entry in enumerate(raw.get("hosts") or []):
|
||||
if not isinstance(entry, dict):
|
||||
raise ConfigError(f"hosts[{i}] ist kein Objekt")
|
||||
h = _merge(defaults, entry)
|
||||
name = h.get("name")
|
||||
if not isinstance(name, str) or not NAME_RE.match(name):
|
||||
raise ConfigError(f"hosts[{i}]: ungueltiger name {name!r}")
|
||||
if name in seen:
|
||||
raise ConfigError(f"Host {name!r} doppelt")
|
||||
seen.add(name)
|
||||
h.setdefault("address", name)
|
||||
_check_safe(str(h["address"]), f"{name}.address")
|
||||
_check_safe(str(h["user"]), f"{name}.user")
|
||||
_check_safe(str(h["identity_file"]), f"{name}.identity_file")
|
||||
if "/" in h["identity_file"]:
|
||||
raise ConfigError(f"{name}: identity_file ist ein Dateiname im ssh/-Ordner")
|
||||
h["port"] = int(h["port"])
|
||||
h["logs"] = _normalize_logs(name, h.get("logs"))
|
||||
h["integrity"]["paths"] = [
|
||||
_check_safe(p, f"{name}.integrity.paths") for p in h["integrity"].get("paths", [])
|
||||
]
|
||||
if any(" " in p for p in h["integrity"]["paths"]):
|
||||
raise ConfigError(f"{name}: integrity.paths duerfen keine Leerzeichen enthalten")
|
||||
h["tunnel_ports"] = [int(p) for p in h["tunnel_ports"]]
|
||||
hosts.append(h)
|
||||
return {"ssh": ssh, "hosts": hosts}
|
||||
|
||||
|
||||
def render_ssh_config(cfg, key_dir, known_hosts):
|
||||
"""OpenSSH-Client-Config: Host-Alias = name aus hosts.yml."""
|
||||
ssh = cfg["ssh"]
|
||||
lines = ["# Automatisch erzeugt aus hosts.yml - nicht von Hand bearbeiten", ""]
|
||||
for h in cfg["hosts"]:
|
||||
lines += [
|
||||
f"Host {h['name']}",
|
||||
f" HostName {h['address']}",
|
||||
f" Port {h['port']}",
|
||||
f" User {h['user']}",
|
||||
f" IdentityFile {key_dir}/{h['identity_file']}",
|
||||
"",
|
||||
]
|
||||
lines += [
|
||||
"Host *",
|
||||
" IdentitiesOnly yes",
|
||||
" BatchMode yes",
|
||||
" PasswordAuthentication no",
|
||||
" KbdInteractiveAuthentication no",
|
||||
f" StrictHostKeyChecking {ssh['host_key_checking']}",
|
||||
f" UserKnownHostsFile {known_hosts}",
|
||||
f" ConnectTimeout {int(ssh['connect_timeout'])}",
|
||||
f" ServerAliveInterval {int(ssh['server_alive_interval'])}",
|
||||
" ServerAliveCountMax 3",
|
||||
" LogLevel ERROR",
|
||||
"",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
Reference in New Issue
Block a user