#!/usr/bin/env python3 """Receptury: PAM hardening (5), sudo secure_path (6), bezpieczniejsze hooki (8). Idempotentny: jesli nowa tresc juz jest, pomija zmiane. Tolerancyjny na zawijanie dlugich linii YAML (backslash + newline). """ import re, sys R = "/var/lib/pagan-sync/recipes" def variants(logical): """Reprezentacje tekstu w pliku: literalna i escaped (scalar w cudzyslowie).""" out = [logical] esc = logical.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"') if esc != logical: out.append(esc) return out def tolerant_pattern(logical): """Regex dopasowujacy tekst takze gdy YAML zawinal linie (backslash+newline).""" parts = [] for ch in logical: if ch == " ": parts.append(r"(?:[ \t]|\\\n)+") elif ch == "\n": parts.append(r"\\n") elif ch in '"\\': parts.append(r"\\?" + re.escape(ch)) else: parts.append(re.escape(ch)) return re.compile("".join(parts)) def applied(text, new): return any(v in text for v in variants(new)) def edit(path, old, new, label): text = open(path, encoding="utf-8").read() if applied(text, new): print(f"-- {label} (juz zastosowane)") return done = False for v in variants(old): if text.count(v) == 1: text = text.replace(v, new if v == old else variants(new)[-1], 1) done = True break if not done: m = tolerant_pattern(old) hits = list(m.finditer(text)) if len(hits) != 1: sys.exit(f"[{label}] {path}: dopasowan {len(hits)} (oczekiwano 1)") h = hits[0] text = text[:h.start()] + new + text[h.end():] open(path, "w", encoding="utf-8").write(text) print(f"OK {label}") def bump_pkgrel(path, label): 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") nr = int(m.group(1)) + 1 text = text[:m.start()] + f"pkgrel: '{nr}'" + text[m.end():] open(path, "w", encoding="utf-8").write(text) print(f"OK {label}: pkgrel -> {nr}") # ══════════════ 5. PAM: faillock + pwquality ══════════════ SH = f"{R}/core/shadow/PAGBUILD.yaml" edit(SH, "depends: []", "depends:\n- pam\n- libxcrypt\n- libpwquality", "shadow: depends") edit(SH, "s@#ENCRYPT_METHOD DES@ENCRYPT_METHOD SHA512@", "s@^#ENCRYPT_METHOD .*@ENCRYPT_METHOD SHA512@", "shadow: ENCRYPT_METHOD SHA512") edit(SH, "# Begin /etc/pam.d/system-account\n\naccount required pam_unix.so\n", "# Begin /etc/pam.d/system-account\n\n" "account required pam_unix.so\n" "account required pam_faillock.so\n", "shadow: system-account + faillock") edit(SH, "# Begin /etc/pam.d/system-auth\n\nauth required pam_unix.so\n", "# Begin /etc/pam.d/system-auth\n\n" "# Blokada konta po nieudanych probach logowania. Parametry (deny,\n" "# unlock_time) ustawia /etc/security/faillock.conf; root nie jest objety.\n" "auth required pam_faillock.so preauth\n" "auth [success=1 default=ignore] pam_unix.so\n" "auth [default=die] pam_faillock.so authfail\n" "auth sufficient pam_faillock.so authsucc\n", "shadow: system-auth + faillock") edit(SH, "password required pam_pwhistory.so retry=3\n", "# Jakosc hasel - parametry w /etc/security/pwquality.conf.\n" "# Pamiec: ostatnie 24 hasla.\n" "password required pam_pwquality.so retry=3\n" "password required pam_pwhistory.so remember=24 retry=3\n", "shadow: system-password + pwquality") edit(SH, "rm -f ${PKGDIR}/etc/pam.d/system-auth ${PKGDIR}/etc/pam.d/system-account " "${PKGDIR}/etc/pam.d/system-login ${PKGDIR}/etc/pam.d/system-password " "${PKGDIR}/etc/pam.d/system-session", "# Pliki system-* ZOSTAJA w pakiecie - wczesniej byly usuwane i pozostawaly\n" "# niezarzadzane, przez co zmiany w PAM nie docieraly do systemu.", "shadow: system-* zarzadzane") bump_pkgrel(SH, "shadow") # ══════════════ 6. sudo: secure_path ══════════════ SU = f"{R}/core/sudo/PAGBUILD.yaml" edit(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_pkgrel(SU, "sudo") # ══════════════ 8. hooki ══════════════ BA = f"{R}/core/bash/PAGBUILD.yaml" edit(BA, "rm -f bin/sh\nrm -f bin/bash\n", "# Usuniete kasowanie /bin/sh i /bin/bash: pakiet dostarcza oba pliki, a gdy\n" "# transakcja padala po rm, system zostawal bez powloki.\n", "bash: bez rm /bin/sh") bump_pkgrel(BA, "bash") GL = f"{R}/core/glibc/PAGBUILD.yaml" edit(GL, "rm -f /etc/nsswitch.conf\n", "# Usuniete kasowanie /etc/nsswitch.conf: plik jest w pakiecie, a pag chroni\n" "# zmiany uzytkownika (zapisuje .pacnew) - rm byl zbedny i ryzykowny.\n", "glibc: bez rm nsswitch.conf") bump_pkgrel(GL, "glibc") NG = f"{R}/utils/nginx/PAGBUILD.yaml" edit(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_pkgrel(NG, "nginx") PA = f"{R}/gui/pulseaudio/PAGBUILD.yaml" edit(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_pkgrel(PA, "pulseaudio") TI = f"{R}/gui/telepathy-idle/PAGBUILD.yaml" edit(TI, "killall -HUP dbus-daemon 2>&1", "systemctl reload dbus 2>/dev/null || true", "telepathy-idle: reload dbus") bump_pkgrel(TI, "telepathy-idle") print("gotowe")