#!/bin/bash
#==============================================================================
# audyt-midero.sh — konto audytowe TYLKO-DO-ODCZYTU dla floty serwerów Midero
#==============================================================================
#
# JEDEN plik, pięć trybów:
#
#   audyt-midero.sh                      -> drukuje raport o stanie serwera (bez argumentów)
#   audyt-midero.sh --instaluj "ssh-..." -> jako root: zakłada konto 'audyt' z tym kluczem
#   audyt-midero.sh --zbierz lista.txt   -> z laptopa: pobiera raporty ze wszystkich serwerów
#   audyt-midero.sh --porownaj A B       -> z laptopa: różnice między dwoma przebiegami
#   audyt-midero.sh --dostepnosc lista   -> czy usługi żyją i jak szybko odpowiadają
#
#------------------------------------------------------------------------------
# CO TEN SKRYPT ROBI I CZEGO NIE ROBI  (sekcja dla audytu bezpieczeństwa)
#------------------------------------------------------------------------------
#
# Tryb raportu (jedyny, jaki konto 'audyt' może uruchomić przez sudo):
#   * Wykonuje WYŁĄCZNIE polecenia odczytujące stan systemu:
#     hostname, uname, uptime, nproc, free, df, ps, ss, systemctl, docker ps,
#     ls, du, sshd -T, fail2ban-client status, firewall-cmd/ufw --list,
#     apt-get -s (symulacja, nie instaluje), openssl x509 (odczyt dat),
#     journalctl (odczyt), crontab -l (odczyt).
#   * NIE nawiązuje ŻADNYCH połączeń sieciowych. Brak curl/wget/nc.
#     (Sieci używa wyłącznie tryb --dostepnosc, który NIE jest dostępny przez sudo
#      i uruchamiasz go sam na serwerze monitorującym.)
#   * NIE zapisuje ani nie kasuje żadnego pliku. Pisze tylko na stdout.
#   * NIE czyta zawartości plików z danymi: żadnych haseł, kluczy, wp-config,
#     baz danych, maili, /etc/shadow. Czyta wyłącznie metadane i stan usług.
#   * NIE pokazuje kodu aplikacji — żadnych plików źródłowych, konfiguracji
#     aplikacji ani zawartości katalogów WWW. Tylko nazwy i rozmiary.
#   * REDAKCJA POŚWIADCZEŃ (dwie warstwy, od wersji 1.2):
#     (a) strukturalna — z crona wypisujemy wyłącznie harmonogram i ścieżkę
#         programu; argumenty i zmienne środowiskowe są odrzucane, bo właśnie
#         tam trafiają hasła (realny przypadek na naszej flocie: DB_PASS w cronie);
#     (b) filtr redakcja() — cały raport przechodzi przez sed zamazujący hasła
#         w URL-ach, przypisania *PASS/TOKEN/KEY/SECRET, "-p<haslo>", nagłówki
#         Bearer, tokeny sk-/glpat-/ghp_/AKIA/xox, JWT, klucze publiczne
#         i wartości o wysokiej entropii. Zostaje nazwa, znika wartość.
#   * Ustawia własny PATH na starcie -> nie da się go przejąć przez podmianę
#     zmiennej środowiskowej.
#   * Odrzuca jakiekolwiek argumenty w trybie raportu.
#
# Model uprawnień:
#   * Konto 'audyt' ma ZABLOKOWANE hasło (passwd -l). Logowanie tylko kluczem.
#   * Plik w /etc/sudoers.d/audyt dopuszcza dokładnie jedną ścieżkę,
#     BEZ ARGUMENTÓW:   audyt ALL=(root) NOPASSWD: /usr/local/sbin/audyt-midero.sh ""
#     Pusty "" na końcu to składnia sudo oznaczająca "tylko bez argumentów".
#     Dlatego konto 'audyt' NIE może uruchomić trybu --instaluj przez sudo.
#   * Skrypt w /usr/local/sbin należy do root:root z prawami 755 — konto 'audyt'
#     nie może go podmienić ani edytować.
#   * Brak ścieżek eskalacji: nie ma tu 'sudo cat', 'sudo find -exec',
#     'sudo vi', ani żadnej komendy przyjmującej ścieżkę od użytkownika.
#
# Odinstalowanie (cofa wszystko):
#   sudo userdel -r audyt
#   sudo rm -f /etc/sudoers.d/audyt /usr/local/sbin/audyt-midero.sh
#
#==============================================================================

set -uo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export LC_ALL=C

WERSJA="1.3"
CEL=/usr/local/sbin/audyt-midero.sh
UZYTKOWNIK=audyt

# Zamienia date z certyfikatu ("Sep  8 07:32:00 2026 GMT") na sekundy epoch.
# Linux uzywa 'date -d', macOS/BSD 'date -j -f' — obsluzone oba, zeby ten sam
# skrypt dzialal na serwerze monitorujacym i na laptopie. Zwraca pusty ciag,
# gdy nie da sie sparsowac (wywolujacy ma wtedy pokazac "-", a nie liczbe).
na_epoch() {
  local d="${1:-}"; [ -z "$d" ] && return 1
  date -d "$d" +%s 2>/dev/null && return 0
  date -j -f "%b %e %H:%M:%S %Y %Z" "$d" +%s 2>/dev/null && return 0
  date -j -f "%b %d %H:%M:%S %Y %Z" "$d" +%s 2>/dev/null && return 0
  return 1
}

# Ile pelnych dni zostalo do podanej daty; pusto, gdy nie udalo sie sparsowac.
dni_do() {
  local e; e=$(na_epoch "${1:-}") || return 1
  [ -z "$e" ] && return 1
  echo $(( ( e - $(date +%s) ) / 86400 ))
}

#------------------------------------------------------------------------------
# REDAKCJA — filtr, przez ktory przechodzi CALY raport przed wypisaniem.
#------------------------------------------------------------------------------
# Powod: raport pokazuje m.in. crona roota i linie polecen procesow, a tam
# realnie trafiaja sie hasla (np. "-e DB_PASS=... ", "ftp://user:haslo@host").
# Bez tego filtra narzedzie do audytu samo wynosiloby poswiadczenia.
# Filtr jest CELOWO nadgorliwy: lepiej zamazac o jedno pole za duzo niz
# wypuscic haslo. Zamazuje wartosc, zostawia nazwe — widac WTEDY, ze sekret
# tam jest (to sama w sobie wazna informacja), ale nie widac jaki.
# Uwaga metodyczna: pierwsza wersja tego filtra dopasowywala sekrety po NAZWIE
# zmiennej (PASS, TOKEN...). To nie dziala — realny skrypt backupu na naszym
# serwerze trzymal haslo w zmiennej o nazwie "BP", ktorej zadna lista nie zlapie.
# Dlatego glowna obrona jest STRUKTURALNA (patrz sekcja CRON: wypisujemy tylko
# harmonogram i sciezke, nigdy argumentow), a ten filtr jest druga warstwa
# i dopasowuje takze po KSZTALCIE wartosci, nie tylko po nazwie.
redakcja() {
  sed -E \
    -e 's#([a-zA-Z][a-zA-Z0-9+.-]*://[^:/@[:space:]]+):[^@[:space:]]+@#\1:[UKRYTE]@#g' \
    -e 's/([Pp][Aa][Ss][Ss][A-Za-z0-9_]*|[Hh][Aa][Ss][Ll][Oo][A-Za-z0-9_]*|[Ss][Ee][Cc][Rr][Ee][Tt][A-Za-z0-9_]*|[Tt][Oo][Kk][Ee][Nn][A-Za-z0-9_]*|[A-Za-z0-9_]*_?[Kk][Ee][Yy]|[Pp][Ww][Dd])([[:space:]]*[=:][[:space:]]*)("[^"]*"|'"'"'[^'"'"']*'"'"'|[^[:space:];,)]+)/\1\2[UKRYTE]/g' \
    -e 's/([[:space:]]-p)[^[:space:]=-][^[:space:]]*/\1[UKRYTE]/g' \
    -e 's/(--password[=[:space:]])[^[:space:]]+/\1[UKRYTE]/g' \
    -e 's/([Bb]earer[[:space:]]+)[A-Za-z0-9._-]+/\1[UKRYTE]/g' \
    -e 's/(sk-[A-Za-z0-9_-]{6}|glpat-[A-Za-z0-9_-]{4}|gh[pousr]_[A-Za-z0-9]{4}|AKIA[0-9A-Z]{4}|xox[baprs]-[A-Za-z0-9-]{4})[A-Za-z0-9._-]*/\1[UKRYTE]/g' \
    -e 's/(eyJ[A-Za-z0-9_-]{6})[A-Za-z0-9._-]+/\1[UKRYTE-JWT]/g' \
    -e 's/(ssh-(rsa|ed25519|dss)[[:space:]]+)[A-Za-z0-9+\/=]{20,}/\1[UKRYTE-KLUCZ-PUBLICZNY]/g' \
    -e 's/-----BEGIN[^-]*PRIVATE KEY-----/[UKRYTE-KLUCZ-PRYWATNY]/g' \
    -e "s/=[[:space:]]*'[A-Za-z0-9+\/_-]{12,}'/='[UKRYTE-WYSOKA-ENTROPIA]'/g" \
    -e 's/=[[:space:]]*"[A-Za-z0-9+\/_-]{12,}"/="[UKRYTE-WYSOKA-ENTROPIA]"/g'
}

#------------------------------------------------------------------------------
# TRYB 1: raport o stanie serwera  (domyślny, bez argumentów)
#------------------------------------------------------------------------------
# raport() = raport_surowy() przepuszczony przez redakcje(). Nigdzie w kodzie
# nie wolno wywolywac raport_surowy() bezposrednio.
raport() { raport_surowy | redakcja; }

raport_surowy() {
  sek() { printf '\n===== %s =====\n' "$1"; }

  sek "RAPORT"
  echo "wersja skryptu: $WERSJA"
  echo "wygenerowano:   $(date -Is)"

  sek "HOST"
  hostname -f 2>/dev/null || hostname
  [ -r /etc/os-release ] && . /etc/os-release && echo "${PRETTY_NAME:-nieznany}"
  uname -r

  sek "UPTIME I OBCIAZENIE"
  uptime

  sek "CPU"
  echo "rdzenie: $(nproc 2>/dev/null || echo '?')"
  grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2-

  sek "PAMIEC (MB)"
  free -m 2>/dev/null | head -3

  sek "DYSK"
  df -h -x tmpfs -x devtmpfs -x overlay 2>/dev/null | grep -vE 'loop|shm'

  sek "I-WEZLY"
  df -i -x tmpfs -x devtmpfs -x overlay 2>/dev/null | grep -vE 'loop|shm'

  sek "NAJCIEZSZE PROCESY - CPU"
  ps -eo pcpu,pmem,rss,comm --sort=-pcpu 2>/dev/null | head -12

  sek "NAJCIEZSZE PROCESY - RAM"
  ps -eo pmem,pcpu,rss,comm --sort=-pmem 2>/dev/null | head -12

  sek "PORTY NASLUCHUJACE"
  ss -tlnpH 2>/dev/null | awk '{
    n=split($4,a,":"); port=a[n];
    adr=($4 ~ /^(0\.0\.0\.0|\*|\[::\])/) ? "PUBLICZNY" : "lokalny";
    proc=""; if (match($0,/users:\(\("[^"]+"/)) { proc=substr($0,RSTART+9,RLENGTH-10) }
    print port"\t"adr"\t"proc
  }' | sort -n -u

  sek "URUCHOMIONE USLUGI"
  systemctl list-units --type=service --state=running --no-pager --no-legend 2>/dev/null | awk '{print $1}'

  sek "KONTENERY DOCKER"
  docker ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null || echo "(brak dockera)"

  sek "VHOSTY WWW"
  ls /etc/apache2/sites-enabled/ /etc/nginx/sites-enabled/ 2>/dev/null

  sek "DOMENY VIRTUALMIN"
  virtualmin list-domains --name-only 2>/dev/null || echo "(brak virtualmina)"

  sek "NAJWIEKSZE KATALOGI"
  # 'du' na serwerze z setkami GB potrafi liczyc minutami — ograniczamy czas,
  # zeby raport nigdy nie wisial. Gdy nie zdazy, mowimy o tym wprost.
  if command -v timeout >/dev/null 2>&1; then
    timeout 90 du -xh -d1 /home /var/www /opt /var/lib 2>/dev/null | sort -rh | head -25 \
      || echo "  (przerwano po 90 s — katalogi zbyt duze, uzyj 'du' recznie)"
  else
    du -xh -d1 /home /var/www /opt /var/lib 2>/dev/null | sort -rh | head -25
  fi

  sek "KONFIGURACJA SSH"
  sshd -T 2>/dev/null | grep -iE '^(passwordauthentication|permitrootlogin|port|permitemptypasswords)' \
    || echo "(nie odczytano)"

  sek "FAIL2BAN"
  fail2ban-client status 2>/dev/null || echo "(nieaktywny lub brak)"

  sek "FIREWALL"
  { firewall-cmd --list-all 2>/dev/null; ufw status verbose 2>/dev/null; } | head -30

  sek "WERSJE OPROGRAMOWANIA"
  # wersja() drukuje "—" gdy programu nie ma, zamiast smieci z komunikatu bledu
  wersja() {
    local etykieta="$1" polecenie="$2"; shift 2
    if command -v "$polecenie" >/dev/null 2>&1; then
      printf '%-12s %s\n' "$etykieta" "$("$@" 2>&1 | head -1)"
    else
      printf '%-12s —\n' "$etykieta"
    fi
  }
  printf '%-12s %s\n' "jadro:"   "$(uname -r)"
  wersja "openssl:"  openssl  sh -c 'openssl version | awk "{print \$2}"'
  wersja "apache:"   apache2  sh -c 'apache2 -v | awk -F/ "/version/{print \$2}" | awk "{print \$1}"'
  wersja "nginx:"    nginx    sh -c 'nginx -v 2>&1 | awk -F/ "{print \$2}"'
  wersja "php:"      php      sh -c 'php -v | awk "NR==1{print \$2}"'
  printf '%-12s %s\n' "php (pule):" "$(ls -1 /etc/php/ 2>/dev/null | tr '\n' ' ')"
  wersja "mysql:"    mysqld   sh -c 'mysqld --version | grep -oE "[0-9]+\.[0-9]+\.[0-9]+" | head -1'
  wersja "postgres:" psql     sh -c 'psql --version | awk "{print \$3}"'
  wersja "docker:"   docker   sh -c 'docker --version | grep -oE "[0-9]+\.[0-9]+\.[0-9]+" | head -1'
  wersja "ssh:"      sshd     sh -c 'sshd -V 2>&1 | head -1'

  sek "ZALEGLE AKTUALIZACJE"
  echo "pakietow do aktualizacji: $(apt-get -s upgrade 2>/dev/null | grep -c '^Inst')"
  echo "w tym bezpieczenstwa:     $(apt-get -s upgrade 2>/dev/null | grep '^Inst' | grep -ci security)"
  [ -e /var/run/reboot-required ] && echo "WYMAGANY RESTART SYSTEMU"
  # data konca wsparcia dystrybucji - wykrywa systemy po EOL
  if [ -r /etc/os-release ]; then
    ( . /etc/os-release; echo "dystrybucja: ${ID:-?} ${VERSION_ID:-?} (${UBUNTU_CODENAME:-${VERSION_CODENAME:-?}})" )
  fi

  sek "KONTA I DOSTEP"
  # Na serwerach Virtualmin kont bywa 100+ (po jednym na vhost) — pelna lista
  # zalewalaby raport, wiec listujemy tylko konta ISTOTNE DLA BEZPIECZENSTWA.
  echo "kont z powloka logowania: $(awk -F: '$7 !~ /(nologin|false|sync)$/' /etc/passwd 2>/dev/null | wc -l)"
  echo "konta z uid 0 (rownowazne rootowi) — powinien byc tylko 'root':"
  awk -F: '$3==0 {print "  "$1}' /etc/passwd 2>/dev/null
  echo "konta w grupach sudo/admin/wheel:"
  for g in sudo admin wheel; do
    czlonkowie=$(getent group "$g" 2>/dev/null | cut -d: -f4)
    [ -n "${czlonkowie:-}" ] && printf '  %-8s %s\n' "$g" "$czlonkowie"
  done
  echo "wpisy sudoers (pliki w /etc/sudoers.d):"
  ls -1 /etc/sudoers.d/ 2>/dev/null | sed 's/^/  /'
  echo "konta z kluczami SSH (liczba kluczy):"
  for ak in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do
    [ -r "$ak" ] || continue
    n=$(grep -cE '^(ssh|ecdsa)' "$ak" 2>/dev/null)
    [ "${n:-0}" -gt 0 ] && printf '  %-46s %s\n' "$ak" "$n"
  done
  echo "konta utworzone w ciagu ostatnich 30 dni:"
  find /home -maxdepth 1 -mindepth 1 -type d -mtime -30 2>/dev/null | sed 's/^/  /' | head -10

  sek "LOGI - PODSUMOWANIE 24H (tylko liczniki, bez tresci)"
  # WYDAJNOSC: na serwerze z dziesiatkami kontenerow journalctl za 24h potrafi
  # zwrocic setki MB. Dlatego czytamy dziennik JEDEN raz do zmiennej i liczymy
  # z niej, zamiast wolac journalctl osobno dla kazdego licznika.
  # Zmienna, nie plik — raport nadal niczego nie zapisuje na dysk.
  local dziennik ssh_log
  if command -v timeout >/dev/null 2>&1; then
    dziennik=$(timeout 60 journalctl --since '24 hours ago' --no-pager -q 2>/dev/null)
    ssh_log=$(timeout 30 journalctl -u ssh -u sshd --since '24 hours ago' --no-pager -q 2>/dev/null)
  else
    dziennik=$(journalctl --since '24 hours ago' --no-pager -q 2>/dev/null)
    ssh_log=$(journalctl -u ssh -u sshd --since '24 hours ago' --no-pager -q 2>/dev/null)
  fi
  # grep -c sam drukuje 0 przy braku trafien, ale konczy sie kodem 1 —
  # dlatego "|| true", a NIE "|| echo 0" (to dawaloby dwie zera).
  licz() { printf '%s\n' "$2" | grep -ciE "$1" 2>/dev/null || true; }
  echo "nieudane logowania SSH:      $(licz 'Failed password'      "$ssh_log")"
  echo "udane logowania SSH:         $(licz 'Accepted'             "$ssh_log")"
  echo "uzycia sudo:                 $(licz 'sudo:.*COMMAND='      "$dziennik")"
  echo "zabicia OOM (brak pamieci):  $(licz 'Out of memory'        "$dziennik")"
  echo "bledy I/O dysku:             $(licz 'I/O error|EXT4-fs error' "$dziennik")"
  echo "restarty uslug:              $(licz 'Scheduled restart job' "$dziennik")"
  unset dziennik ssh_log
  echo "adresy zbanowane przez fail2ban (laczne, per jail):"
  for j in $(fail2ban-client status 2>/dev/null | sed -n 's/.*Jail list:[[:space:]]*//p' | tr ',' ' '); do
    printf '  %-20s %s\n' "$j" "$(fail2ban-client status "$j" 2>/dev/null | sed -n 's/.*Total banned:[[:space:]]*//p')"
  done

  sek "BACKUPY - SWIEZOSC KOPII"
  command -v timeout >/dev/null 2>&1 && LIMIT="timeout 45" || LIMIT=""
  # Szuka typowych katalogów z kopiami i podaje wiek NAJNOWSZEGO pliku w każdym.
  # Kopia starsza niż 2 dni to sygnał, że harmonogram przestał działać.
  local znaleziono=0 przeterminowane=0
  for kat in /home/*/backups /home/backup /var/backups /backup /opt/backup /home/*/kopie; do
    [ -d "$kat" ] || continue
    local najnowszy wiek
    najnowszy=$(${LIMIT:-} find "$kat" -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1)
    [ -z "$najnowszy" ] && { printf '  %-42s PUSTY KATALOG\n' "$kat"; znaleziono=1; continue; }
    wiek=$(( ( $(date +%s) - ${najnowszy%%.*} ) / 3600 ))
    local flaga=""
    if [ "$wiek" -gt 48 ]; then flaga="  <-- STARSZE NIZ 48H"; przeterminowane=$((przeterminowane+1)); fi
    printf '  %-42s %4s h temu  %s%s\n' "$kat" "$wiek" \
      "$(du -sh "$kat" 2>/dev/null | cut -f1)" "$flaga"
    znaleziono=1
  done
  [ "$znaleziono" -eq 0 ] && echo "  (nie znaleziono zadnego typowego katalogu z kopiami)"

  echo "zadania cron zawierajace 'backup' lub 'dump':"
  { crontab -l 2>/dev/null; cat /etc/cron.d/* 2>/dev/null; } \
    | grep -viE '^\s*#' | grep -iE 'backup|dump|rsync' | sed 's/^/  /' | head -10
  # Werdykt: interesuje nas NAJNOWSZY log backupu, nie caly archiwalny stos.
  # Serwer moze miec 30+ logow historycznych — liczy sie ten ostatni.
  echo "ostatnie logi backupu (od najnowszego):"
  najsw=""
  for log in /var/log/*backup*.log /var/log/backup*/*.log; do
    [ -r "$log" ] || continue
    printf '%s %s\n' "$(stat -c %Y "$log" 2>/dev/null || echo 0)" "$log"
  done | sort -rn | head -3 | while read -r ts sciezka; do
    printf '  %-46s %4s h temu\n' "$sciezka" "$(( ( $(date +%s) - ts ) / 3600 ))"
  done
  najsw=$(for log in /var/log/*backup*.log /var/log/backup*/*.log; do
            [ -r "$log" ] && stat -c %Y "$log" 2>/dev/null
          done | sort -rn | head -1)
  if [ -n "${najsw:-}" ]; then
    wiek_h=$(( ( $(date +%s) - najsw ) / 3600 ))
    # UWAGA: sam wiek najnowszego logu nie wystarcza. Jeden dzialajacy proces
    # przykrylby fakt, ze inny katalog kopii stoi od tygodni. Dlatego werdykt
    # laczy oba sygnaly i nie mowi "OK", dopoki cokolwiek jest przeterminowane.
    if [ "$wiek_h" -le 26 ] && [ "$przeterminowane" -eq 0 ]; then
      echo "  WERDYKT: backup dzialal $wiek_h h temu, zaden katalog nie jest przeterminowany — OK"
    elif [ "$wiek_h" -le 26 ]; then
      echo "  WERDYKT: najnowszy backup $wiek_h h temu, ALE $przeterminowane katalog(ow) kopii nie bylo aktualizowanych ponad 48 h — CZESC HARMONOGRAMOW STANELA"
    else
      echo "  WERDYKT: ostatni backup $wiek_h h temu, przeterminowanych katalogow: $przeterminowane — SPRAWDZIC"
    fi
  else
    echo "  WERDYKT: brak logow backupu na tym serwerze"
  fi

  sek "CRON ROOTA"
  # OBRONA STRUKTURALNA: wypisujemy TYLKO harmonogram i sciezke uruchamianego
  # programu. Argumenty i zmienne srodowiskowe sa ODRZUCANE, bo wlasnie tam
  # trafiaja hasla (realny przyklad z naszej floty: "-e DB_PASS=... " w cronie).
  # Do analizy "co i kiedy sie uruchamia" sciezka w zupelnosci wystarcza.
  { crontab -l 2>/dev/null; cat /etc/cron.d/* 2>/dev/null; } \
  | grep -v '^[[:space:]]*#' | grep -vE '^[[:space:]]*$' \
  | awk '{
      # przepusc definicje zmiennych srodowiskowych tylko jako sama nazwe
      if ($0 ~ /^[A-Za-z_][A-Za-z0-9_]*=/) { split($0,z,"="); print "  [zmienna] " z[1] "=..."; next }
      harm=""; i=1
      # harmonogram: 5 pol, albo @reboot/@daily itp.
      if ($1 ~ /^@/) { harm=$1; i=2 }
      else { harm=$1" "$2" "$3" "$4" "$5; i=6 }
      # pliki /etc/cron.d maja dodatkowe pole z uzytkownikiem
      if (zrodlo=="crond" ) { }
      cmd=""
      # Pliki /etc/cron.d maja PO harmonogramie dodatkowe pole z uzytkownikiem
      # (np. "15 3 * * * root /usr/local/sbin/backup.sh"). Dlatego najpierw
      # szukamy tokenu ze sciezka, a dopiero w ostatecznosci golego slowa.
      for (j=i; j<=NF; j++) { if ($j ~ /^\//) { cmd=$j; break } }
      if (cmd=="") { for (j=i; j<=NF; j++) { if ($j ~ /^[a-z][a-z0-9_.-]*$/ && $j != "root") { cmd=$j; break } } }
      if (cmd=="") cmd="(nierozpoznane)"
      printf "  %-18s %s\n", harm, cmd
    }' | head -20
  echo "  (argumenty i zmienne celowo pominiete — moga zawierac hasla)"

  sek "KONIEC RAPORTU"
}

#------------------------------------------------------------------------------
# TRYB 2: instalacja konta audytowego  (uruchamiane raz, jako root)
#------------------------------------------------------------------------------
instaluj() {
  local klucz="${1:-}"

  [ "$(id -u)" -eq 0 ] || { echo "BLAD: instalacja wymaga roota. Uzyj: sudo $0 --instaluj \"<klucz>\"" >&2; exit 1; }
  case "$klucz" in
    ssh-ed25519\ *|ssh-rsa\ *|ecdsa-sha2-*\ *) : ;;
    *) echo "BLAD: podaj klucz PUBLICZNY w cudzyslowie, np.:" >&2
       echo "      sudo $0 --instaluj \"\$(cat ~/.ssh/audyt_ed25519.pub)\"" >&2; exit 1 ;;
  esac

  id "$UZYTKOWNIK" >/dev/null 2>&1 || useradd -m -s /bin/bash -c "Konto audytowe read-only" "$UZYTKOWNIK"
  passwd -l "$UZYTKOWNIK" >/dev/null          # brak logowania haslem - tylko klucz

  local dom; dom=$(getent passwd "$UZYTKOWNIK" | cut -d: -f6)
  install -d -m 700 -o "$UZYTKOWNIK" -g "$UZYTKOWNIK" "$dom/.ssh"
  printf '%s\n' "$klucz" > "$dom/.ssh/authorized_keys"
  chmod 600 "$dom/.ssh/authorized_keys"
  chown "$UZYTKOWNIK:$UZYTKOWNIK" "$dom/.ssh/authorized_keys"

  # skrypt kopiuje sam siebie; wlasciciel root, konto 'audyt' nie moze go podmienic
  install -m 755 -o root -g root "$0" "$CEL"

  # sudo WYLACZNIE na te jedna sciezke i WYLACZNIE bez argumentow
  local tmp; tmp=$(mktemp)
  {
    echo "# Zainstalowane przez audyt-midero.sh v$WERSJA — $(date -Is)"
    echo "# Konto moze uruchomic wylacznie ten skrypt, bez argumentow (tryb raportu)."
    echo "$UZYTKOWNIK ALL=(root) NOPASSWD: $CEL \"\""
    echo "Defaults:$UZYTKOWNIK !requiretty"
    echo "Defaults:$UZYTKOWNIK env_reset"
  } > "$tmp"

  if visudo -cf "$tmp" >/dev/null 2>&1; then
    install -m 440 -o root -g root "$tmp" /etc/sudoers.d/audyt
    rm -f "$tmp"
  else
    rm -f "$tmp"
    echo "BLAD: wygenerowany wpis sudoers jest niepoprawny — nic nie zmieniono." >&2
    exit 1
  fi

  echo "OK — konto '$UZYTKOWNIK' gotowe na $(hostname -f 2>/dev/null || hostname)."
  echo "Test z laptopa:  ssh $UZYTKOWNIK@<adres-ip> sudo $CEL"
  echo "Cofniecie:       sudo userdel -r $UZYTKOWNIK && sudo rm -f /etc/sudoers.d/audyt $CEL"
}

#------------------------------------------------------------------------------
# TRYB 3: zbieranie raportów z całej floty  (uruchamiane na laptopie)
#------------------------------------------------------------------------------
zbierz() {
  local lista="${1:-serwery.txt}"
  local klucz="${KLUCZ:-$HOME/.ssh/audyt_ed25519}"

  [ -r "$lista" ] || { echo "BLAD: brak pliku '$lista'. Format: <nazwa> <adres-ip> w kazdej linii." >&2; exit 1; }

  local katalog="raporty/$(date +%Y-%m-%d)"
  mkdir -p "$katalog"
  local ok=0 bledy=0

  while read -r nazwa adres _; do
    [ -z "${nazwa:-}" ] && continue
    case "$nazwa" in \#*) continue ;; esac
    printf '%-28s ' "$nazwa"
    if ssh -i "$klucz" -o BatchMode=yes -o ConnectTimeout=10 -o IdentitiesOnly=yes \
         -o StrictHostKeyChecking=accept-new \
         "$UZYTKOWNIK@$adres" "sudo $CEL" \
         > "$katalog/$nazwa.txt" 2> "$katalog/$nazwa.blad"; then
      echo "OK ($(wc -l < "$katalog/$nazwa.txt" | tr -d ' ') linii)"
      rm -f "$katalog/$nazwa.blad"
      ok=$((ok+1))
    else
      echo "BLAD: $(tail -1 "$katalog/$nazwa.blad" 2>/dev/null)"
      bledy=$((bledy+1))
    fi
  done < "$lista"

  echo
  echo "Zebrano: $ok  |  Bledy: $bledy  |  Katalog: $katalog"
}

#------------------------------------------------------------------------------
# TRYB 4: porównanie dwóch przebiegów — to zamienia zdjęcia stanu w monitoring
#------------------------------------------------------------------------------
# Wyciąga z raportu jedną sekcję (bez nagłówka), żeby porównywać sekcja po sekcji.
sekcja() { awk -v s="===== $2 =====" '$0==s{f=1;next} /^===== /{f=0} f' "$1" 2>/dev/null; }

porownaj() {
  local stary="${1:-}" nowy="${2:-}"
  [ -d "$stary" ] && [ -d "$nowy" ] || {
    echo "Uzycie: $0 --porownaj raporty/2026-09-01 raporty/2026-09-08" >&2; exit 1; }

  # sekcje, w których zmiana ma znaczenie dla bezpieczeństwa
  local istotne=("PORTY NASLUCHUJACE" "WERSJE OPROGRAMOWANIA" "KONTA I DOSTEP"
                 "KONTENERY DOCKER" "KONFIGURACJA SSH" "FIREWALL" "URUCHOMIONE USLUGI")

  echo "PORÓWNANIE: $stary  ->  $nowy"
  echo "Pokazuje tylko RÓŻNICE. Brak wyniku dla serwera = nic się nie zmieniło."

  local zmienione=0
  for plik_nowy in "$nowy"/*.txt; do
    [ -e "$plik_nowy" ] || continue
    local nazwa; nazwa=$(basename "$plik_nowy" .txt)
    local plik_stary="$stary/$nazwa.txt"

    if [ ! -e "$plik_stary" ]; then
      printf '\n### %s — NOWY SERWER w zestawieniu\n' "$nazwa"
      zmienione=$((zmienione+1)); continue
    fi

    local naglowek_wypisany=0
    for s in "${istotne[@]}"; do
      local roznica
      roznica=$(diff <(sekcja "$plik_stary" "$s") <(sekcja "$plik_nowy" "$s") 2>/dev/null \
                | grep -E '^[<>]' | grep -vE '^[<>][[:space:]]*$')
      [ -z "$roznica" ] && continue
      if [ "$naglowek_wypisany" -eq 0 ]; then
        printf '\n### %s\n' "$nazwa"; naglowek_wypisany=1; zmienione=$((zmienione+1))
      fi
      printf '  [%s]\n' "$s"
      # '<' = bylo wczesniej (zniknelo), '>' = jest teraz (pojawilo sie)
      echo "$roznica" | sed 's/^</    USUNIETO:/;s/^>/    NOWE:    /'
    done
  done

  # serwery, które wypadły z zestawienia — mogą oznaczać padniętą maszynę
  for plik_stary in "$stary"/*.txt; do
    [ -e "$plik_stary" ] || continue
    local nazwa; nazwa=$(basename "$plik_stary" .txt)
    [ -e "$nowy/$nazwa.txt" ] || { printf '\n### %s — BRAK w nowym przebiegu (serwer nie odpowiedzial?)\n' "$nazwa"; zmienione=$((zmienione+1)); }
  done

  echo
  [ "$zmienione" -eq 0 ] && echo "Brak zmian w monitorowanych obszarach." \
                         || echo "Serwerow ze zmianami: $zmienione"
}

#------------------------------------------------------------------------------
# TRYB 5: dostępność i czas odpowiedzi — do cyklicznego uruchamiania na serwerze
#         monitorującym. Odpowiada na "czy strona żyje i czy szybko odpowiada".
#------------------------------------------------------------------------------
# Plik wejściowy: jedna linia = <nazwa> <url> [maks_ms]   (domyślny próg 2000 ms)
# Kończy się kodem != 0, jeśli cokolwiek jest niesprawne -> cron sam wyśle maila.
dostepnosc() {
  local lista="${1:-uslugi.txt}"
  [ -r "$lista" ] || { echo "BLAD: brak pliku '$lista'." >&2
    echo "Format: <nazwa> <url> [prog_ms], np.:  sklep https://specjal.com/ 3000" >&2; exit 1; }

  printf '%-24s %-8s %-9s %-9s %s\n' "USLUGA" "KOD" "CZAS" "TLS(dni)" "STAN"
  printf '%s\n' "--------------------------------------------------------------------------"

  local problemy=0
  while read -r nazwa url prog _; do
    [ -z "${nazwa:-}" ] && continue
    case "$nazwa" in \#*) continue ;; esac
    prog=${prog:-2000}

    # jedno zapytanie: kod HTTP + całkowity czas. -k celowo NIE jest uzywane,
    # zeby wygasly certyfikat byl wykryty jako awaria, a nie przemilczany.
    local wynik kod ms
    wynik=$(curl -sS -m 15 -o /dev/null -w '%{http_code} %{time_total}' -L "$url" 2>/dev/null)
    kod=${wynik%% *}; local sek_total=${wynik##* }
    ms=$(awk -v t="${sek_total:-0}" 'BEGIN{printf "%d", t*1000}')

    # ile dni do wygasniecia certyfikatu
    local host dni="-"
    host=$(printf '%s' "$url" | sed -E 's#^[a-z]+://##; s#[:/].*$##')
    local koniec
    koniec=$(echo | openssl s_client -connect "$host:443" -servername "$host" 2>/dev/null \
             | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
    if [ -n "$koniec" ]; then dni=$(dni_do "$koniec") || dni="-"; fi

    local stan="OK"
    if [ -z "$kod" ] || [ "$kod" = "000" ]; then stan="NIE ODPOWIADA"; problemy=$((problemy+1))
    elif [ "$kod" -ge 500 ] 2>/dev/null;     then stan="BLAD SERWERA";  problemy=$((problemy+1))
    elif [ "$kod" -ge 400 ] 2>/dev/null;     then stan="BLAD $kod";     problemy=$((problemy+1))
    elif [ "$ms" -gt "$prog" ];              then stan="WOLNO (>${prog}ms)"; problemy=$((problemy+1))
    fi
    if [ "$dni" != "-" ] && [ "$dni" -lt 14 ] 2>/dev/null; then
      stan="$stan | CERT ZA $dni DNI"; problemy=$((problemy+1))
    fi

    printf '%-24s %-8s %-9s %-9s %s\n' "$nazwa" "${kod:-—}" "${ms}ms" "$dni" "$stan"
  done < "$lista"

  echo
  if [ "$problemy" -eq 0 ]; then echo "Wszystko sprawne."; return 0
  else echo "Wykryto problemow: $problemy"; return 1; fi
}

pomoc() {
  sed -n '2,60p' "$0" | sed 's/^#//;s/^ //'
}

#------------------------------------------------------------------------------
case "${1:-}" in
  "")           raport ;;
  --instaluj)   instaluj "${2:-}" ;;
  --zbierz)     zbierz "${2:-serwery.txt}" ;;
  --porownaj)   porownaj "${2:-}" "${3:-}" ;;
  --dostepnosc) dostepnosc "${2:-uslugi.txt}" ;;
  --wersja)     echo "audyt-midero.sh $WERSJA" ;;
  --pomoc|-h|--help) pomoc ;;
  *) echo "Nieznany argument: $1" >&2
     echo "Uzyj: $0 [--instaluj \"<klucz>\" | --zbierz <lista> | --porownaj <A> <B> | --dostepnosc <lista> | --pomoc]" >&2
     exit 1 ;;
esac
