#!/usr/bin/env python3 """Reszta hardeningu: faillock.conf (shadow), secure_path (sudo), hooki (8).""" import re, sys, yaml R = "/var/lib/pagan-sync/recipes" # ── helper 1: podmiana wartosci klucza-scalara (shadow.build) ── def scalar_range(text, key): m = re.search(r"(?m)^" + re.escape(key) + r":", text) if not m: sys.exit(f"brak klucza {key}") i = m.end() while i < len(text) and text[i] in " \t": i += 1 if i < len(text) and text[i] == '"': j = i + 1 while j < len(text): if text[j] == "\\": j += 2 continue if text[j] == '"': return i, j + 1 j += 1 if i < len(text) and text[i] in "|>": line_start = text.rfind("\n", 0, m.start()) + 1 ind = m.start() - line_start j = end = text.find("\n", i) + 1 while j < len(text): nl = text.find("\n", j) if nl == -1: nl = len(text) line = text[j:nl] if line.strip() == "": j = end = nl + 1 continue if (len(line) - len(line.lstrip(" "))) > ind: j = end = nl + 1 else: break return text.find("\n", i) + 1, end sys.exit("nieobslugiwany styl") def esc(v): return '"' + v.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + '"' def edit_scalar(path, key, old, new, label): text = open(path, encoding="utf-8").read() cur = (yaml.safe_load(text) or {}).get(key) or "" if new in cur: print(f"-- {label} (juz)") return if cur.count(old) != 1: sys.exit(f"[{label}] {cur.count(old)} wystapien w {key}") nv = cur.replace(old, new, 1) s, e = scalar_range(text, key) open(path, "w", encoding="utf-8").write(text[:s] + esc(nv) + text[e:]) print(f"OK {label}") # ── helper 2: edycja tekstowa tolerujaca styl (literal / escaped) ── def edit_text(path, old, new, label): text = open(path, encoding="utf-8").read() for v in (old, old.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"')): if v in text: if text.count(v) != 1: sys.exit(f"[{label}] {text.count(v)} wystapien") repl = new if v == old else new.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"') open(path, "w", encoding="utf-8").write(text.replace(v, repl, 1)) print(f"OK {label}") return sys.exit(f"[{label}] nie znaleziono fragmentu") def bump(path, label, expect=None): text = open(path, encoding="utf-8").read() m = re.search(r"(?m)^pkgrel:\s*'?(\d+)'?\s*$", text) if not m: sys.exit(f"[{label}] brak pkgrel") cur = int(m.group(1)) if expect is not None and cur != expect: print(f"-- {label}: pkgrel juz {cur}") return open(path, "w", encoding="utf-8").write(text[:m.start()] + f"pkgrel: '{cur + 1}'" + text[m.end():]) print(f"OK {label}: pkgrel -> {cur + 1}") # ── 1. shadow: /etc/security/faillock.conf ── SH = f"{R}/core/shadow/PAGBUILD.yaml" edit_scalar(SH, "build", "# Pliki system-* ZOSTAJA w pakiecie", "install -v -m755 -d ${PKGDIR}/etc/security\n" "cat > ${PKGDIR}/etc/security/faillock.conf << \"EOF\"\n" "# Blokada konta po nieudanych probach logowania (pam_faillock,\n" "# uzywany przez /etc/pam.d/system-auth i system-account).\n" "deny = 5\n" "fail_interval = 900\n" "unlock_time = 900\n" "# even_deny_root nie jest ustawione - root nie zostaje zablokowany.\n" "EOF\n" "\n" "# Pliki system-* ZOSTAJA w pakiecie", "shadow: /etc/security/faillock.conf") # ── 2. sudo: secure_path ── SU = f"{R}/core/sudo/PAGBUILD.yaml" edit_text(SU, 'echo "%wheel ALL=(ALL) ALL" >> ${PKGDIR}/etc/sudoers', 'echo "Defaults secure_path=\\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\\"" >> ${PKGDIR}/etc/sudoers\n' 'echo "%wheel ALL=(ALL) ALL" >> ${PKGDIR}/etc/sudoers', "sudo: secure_path") bump(SU, "sudo", expect=2) # ── 3. hooki (punkt 8) ── BA = f"{R}/core/bash/PAGBUILD.yaml" edit_text(BA, "rm -f bin/sh\nrm -f bin/bash\n", "# Usuniete kasowanie /bin/sh i /bin/bash: pakiet dostarcza oba pliki,\n" "# a gdy transakcja padala po rm, system zostawal bez powloki.\n", "bash: bez rm /bin/sh") bump(BA, "bash") GL = f"{R}/core/glibc/PAGBUILD.yaml" edit_text(GL, "rm -f /etc/nsswitch.conf\n", "# Usuniete kasowanie /etc/nsswitch.conf: plik jest w pakiecie, a pag\n" "# chroni zmiany uzytkownika (zapisuje .pacnew) - rm byl zbedny.\n", "glibc: bez rm nsswitch.conf") bump(GL, "glibc") NG = f"{R}/utils/nginx/PAGBUILD.yaml" edit_text(NG, "(getent passwd $UN > /dev/null) && userdel $UN\n" "(getent group $UN > /dev/null) && groupdel $UN\n\n" "UN=apache\n\n" "(getent passwd $UN > /dev/null) && userdel $UN\n" "(getent group $UN > /dev/null) && groupdel $UN\n", "# Usuniete kasowanie uzytkownikow www/apache: userdel zostawial pliki z\n" "# osieroconym uid. Grupa i uzytkownik http sa tworzone ponizej.\n", "nginx: bez userdel") bump(NG, "nginx") PA = f"{R}/gui/pulseaudio/PAGBUILD.yaml" edit_text(PA, "grep -qe 'autospawn = no' etc/pulse/client.conf||sudo sed '/autospawn/iautospawn = no' -i etc/pulse/client.conf", "grep -qe 'autospawn = no' etc/pulse/client.conf || sed -i '/autospawn/iautospawn = no' etc/pulse/client.conf", "pulseaudio: bez sudo") bump(PA, "pulseaudio") TI = f"{R}/gui/telepathy-idle/PAGBUILD.yaml" edit_text(TI, "killall -HUP dbus-daemon 2>&1", "systemctl reload dbus 2>/dev/null || true", "telepathy-idle: reload dbus") bump(TI, "telepathy-idle") print("gotowe")