#!/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:]))
