#!/usr/bin/env sh

VERSION="0.2.4"
OUT="${HOME:-}/langenoten-dokter-report.md"
RUN_DOCKER="1"
RUN_JOURNAL="1"
RUN_LOG_SCAN="1"
DOMAIN=""
KEEP_PUBLIC_IPS="0"

[ -n "${HOME:-}" ] || OUT="./langenoten-dokter-report.md"

usage() {
  cat <<USAGE
LANgenoten Dokter v$VERSION

Gebruik:
  sh langenoten-dokter.sh --forum
  sh langenoten-dokter.sh --domain voorbeeld.nl
  sh langenoten-dokter.sh --output rapport.md

Opties:
  --forum             Maak forumvriendelijke Markdown-output
  --domain domein.nl  Check DNS en TLS-certificaat
  --output bestand    Kies outputbestand
  --no-docker         Sla Docker-checks over
  --no-journal        Sla journalctl-logcheck over
  --no-log-scan       Sla automatische logscan over
  --keep-public-ips   Redacteer publieke IPv4-adressen niet
  -h, --help          Toon deze hulp
USAGE
}

die() { echo "Fout: $*" >&2; exit 1; }
have() { command -v "$1" >/dev/null 2>&1; }

while [ "$#" -gt 0 ]; do
  case "$1" in
    --forum) ;;
    --domain)
      shift
      [ "$#" -gt 0 ] || die "--domain mist een domeinnaam"
      DOMAIN="$1"
      ;;
    --output|-o)
      shift
      [ "$#" -gt 0 ] || die "--output mist een bestandsnaam"
      OUT="$1"
      ;;
    --no-docker) RUN_DOCKER="0" ;;
    --no-journal) RUN_JOURNAL="0" ;;
    --no-log-scan) RUN_LOG_SCAN="0" ;;
    --keep-public-ips) KEEP_PUBLIC_IPS="1" ;;
    -h|--help) usage; exit 0 ;;
    *) die "Onbekende optie: $1" ;;
  esac
  shift
done

if [ -n "$DOMAIN" ]; then
  case "$DOMAIN" in
    *[!A-Za-z0-9._-]*) die "Ongeldig domein: $DOMAIN" ;;
  esac
fi

if ! ( : > "$OUT" ) 2>/dev/null; then
  OUT="/tmp/langenoten-dokter-report.md"
  if ! ( : > "$OUT" ) 2>/dev/null; then
    echo "Fout: kan geen rapport schrijven naar home directory of /tmp." >&2
    exit 1
  fi
fi

WARN="$(mktemp "${TMPDIR:-/tmp}/langenoten-dokter-warnings.XXXXXX")" || exit 1
trap 'rm -f "$WARN"' EXIT HUP INT TERM

warn() {
  printf '%s\n' "- $1" >> "$WARN"
}

redact() {
  sed -E \
    -e 's/((password|passwd|pwd|secret|token|api[_-]?key|apikey|private[_-]?key|auth[_-]?token|access[_-]?token|refresh[_-]?token|client[_-]?secret|secret_key_base|mysql_password|postgres_password|db_password|redis_password|smtp_password|cloudflare_api_token|cf_api_key)[[:space:]]*[:=][[:space:]]*)[^[:space:]"]+/\1[REDACTED]/Ig' \
    -e 's/(Authorization:[[:space:]]*Bearer[[:space:]]+)[A-Za-z0-9._~+\/=-]+/\1[REDACTED]/Ig' \
    -e 's/-----BEGIN [A-Z ]*PRIVATE KEY-----/[REDACTED PRIVATE KEY]/g' \
    -e 's/-----END [A-Z ]*PRIVATE KEY-----/[REDACTED PRIVATE KEY]/g' |
  if [ "$KEEP_PUBLIC_IPS" = "1" ]; then
    cat
  else
    awk '
      function is_private(ip, a) {
        split(ip, a, ".")
        if (a[1] == 10) return 1
        if (a[1] == 192 && a[2] == 168) return 1
        if (a[1] == 172 && a[2] >= 16 && a[2] <= 31) return 1
        if (a[1] == 127) return 1
        if (a[1] == 169 && a[2] == 254) return 1
        if (ip == "0.0.0.0") return 1
        return 0
      }
      {
        line = $0
        out = ""
        while (match(line, /[0-9][0-9]?[0-9]?[.][0-9][0-9]?[0-9]?[.][0-9][0-9]?[0-9]?[.][0-9][0-9]?[0-9]?/)) {
          ip = substr(line, RSTART, RLENGTH)
          repl = is_private(ip) ? ip : "[PUBLIC-IP]"
          out = out substr(line, 1, RSTART - 1) repl
          line = substr(line, RSTART + RLENGTH)
        }
        print out line
      }'
  fi
}

section() {
  printf '\n## %s\n\n' "$1" >> "$OUT"
}

run_block() {
  title="$1"
  command="$2"

  printf '\n### %s\n\n' "$title" >> "$OUT"
  printf '%s\n' '```text' >> "$OUT"
  sh -c "$command" 2>&1 | redact >> "$OUT" || true
  printf '%s\n\n' '```' >> "$OUT"
}

collect_warnings() {
  if have df; then
    df -P 2>/dev/null | awk '
      NR > 1 {
        used = $5
        gsub("%", "", used)
        if (used + 0 >= 80) {
          printf "- Mount `%s` is %s%% vol.\n", $6, used
        }
      }
    ' >> "$WARN"
  fi

  if have systemctl; then
    failed_count="$(systemctl --failed --no-legend --no-pager 2>/dev/null | awk 'NF {c++} END {print c+0}')"
    if [ "${failed_count:-0}" -gt 0 ] 2>/dev/null; then
      warn "$failed_count failed systemd unit(s) gevonden."
    fi
  fi

  if [ "$RUN_DOCKER" = "1" ] && have docker; then
    if docker ps >/dev/null 2>&1; then
      docker ps -a --format '{{.Names}}|{{.Image}}|{{.Status}}' 2>/dev/null |
      while IFS='|' read -r name image status; do
        case "$status" in
          *Exited*|*Restarting*|*Dead*)
            case "$name:$status" in
              nextcloud-aio-borgbackup:Exited\ \(0\)*|nextcloud-aio-watchtower:Exited\ \(0\)*)
                ;;
              *)
                printf '%s\n' "- Container \`$name\` heeft status: $status." >> "$WARN"
                ;;
            esac
            ;;
        esac

        case "$image" in
          ghcr.io/nextcloud-releases/*:latest|gcr.io/cadvisor/cadvisor:latest)
            ;;
          *:latest)
            printf '%s\n' "- Container \`$name\` gebruikt de latest-tag: $image." >> "$WARN"
            ;;
        esac
      done

      ids="$(docker ps -aq 2>/dev/null || true)"

      for id in $ids; do
        docker inspect --format '{{.Name}}|{{.HostConfig.RestartPolicy.Name}}|{{.HostConfig.Privileged}}' "$id" 2>/dev/null || true
      done |
      while IFS='|' read -r rawname policy privileged; do
        name="$(printf '%s' "$rawname" | sed 's#^/##')"

        if [ "$policy" = "no" ] || [ -z "$policy" ]; then
          case "$name" in
            nextcloud-aio-borgbackup|nextcloud-aio-watchtower)
              ;;
            *)
              printf '%s\n' "- Container \`$name\` heeft geen restart policy." >> "$WARN"
              ;;
          esac
        fi

        if [ "$privileged" = "true" ]; then
          printf '%s\n' "- Container \`$name\` draait privileged." >> "$WARN"
        fi
      done

      for id in $ids; do
        docker inspect --format '{{.Name}}|{{range .Mounts}}{{.Source}}:{{.Destination}} {{end}}' "$id" 2>/dev/null || true
      done |
      while IFS='|' read -r rawname mounts; do
        name="$(printf '%s' "$rawname" | sed 's#^/##')"
        case "$mounts" in
          */var/run/docker.sock*)
            case "$name" in
              nextcloud-aio-mastercontainer|nextcloud-aio-watchtower)
                ;;
              *)
                printf '%s\n' "- Container \`$name\` heeft de Docker socket gemount." >> "$WARN"
                ;;
            esac
            ;;
        esac
      done
    else
      warn "Docker is aanwezig, maar deze gebruiker mag Docker niet uitlezen."
    fi
  fi
}

collect_warnings

: > "$OUT" || die "Kan outputbestand niet schrijven: $OUT"

{
  printf '%s\n' '# LANgenoten Dokter Rapport'
  printf '\n'
  printf '%s\n' "- Versie: $VERSION"
  printf '%s\n' "- Gemaakt op: $(date -Iseconds 2>/dev/null || date)"
  printf '%s\n' "- Upload: geen, dit rapport is lokaal gemaakt"
  printf '%s\n' "- Let op: controleer dit rapport altijd zelf voordat je het deelt"
} >> "$OUT"

section "Belangrijke waarschuwingen"

if [ -s "$WARN" ]; then
  redact < "$WARN" >> "$OUT"
else
  printf '%s\n' "Geen directe waarschuwingen gevonden." >> "$OUT"
fi

section "Systeem"

run_block "Host" '
hostname 2>/dev/null || true
uname -a 2>/dev/null || true
[ -r /etc/os-release ] && cat /etc/os-release || true
'

run_block "Uptime en geheugen" '
uptime 2>/dev/null || true
free -h 2>/dev/null || true
'

run_block "Opslag" '
df -h -x tmpfs -x devtmpfs -x overlay -x squashfs -x nsfs 2>/dev/null || df -h 2>/dev/null | grep -Ev "/var/lib/docker/overlay2|overlay" || true
'

run_block "Relevante mounts" '
if command -v findmnt >/dev/null 2>&1; then
  findmnt -rno TARGET,SOURCE,FSTYPE,OPTIONS 2>/dev/null |
  awk '\''
    $1 == "/" ||
    $1 ~ "^/backup$" ||
    $1 ~ "^/mnt/" ||
    $3 ~ "^(nfs|nfs4|cifs|smb3|ext2|ext3|ext4|xfs|btrfs|zfs)$" {
      if ($1 ~ "^/var/lib/docker/overlay2") next
      if ($1 ~ "^/run/docker") next
      if ($1 ~ "^/proc") next
      if ($1 ~ "^/sys") next
      if ($1 ~ "^/dev") next
      print
    }
  '\'' | head -n 80
else
  mount 2>/dev/null | grep -Ev "overlay|lxcfs|proc|sysfs|tmpfs|devpts|nsfs|cgroup|mqueue" | head -n 80 || true
fi
'

section "Netwerk"

run_block "IPv4-adressen" '
ip -br -4 addr 2>/dev/null || ifconfig 2>/dev/null || true
'

run_block "Routes" '
ip route 2>/dev/null || route -n 2>/dev/null || true
'

run_block "DNS resolvers" '
cat /etc/resolv.conf 2>/dev/null || true
'

run_block "Luisterende poorten" '
ss -tulpn 2>/dev/null || netstat -tulpn 2>/dev/null || true
'

section "Systemd"

if have systemctl; then
  run_block "Failed units" '
systemctl --failed --no-pager 2>/dev/null || true
'
else
  printf '%s\n' "systemctl niet gevonden." >> "$OUT"
fi

if [ "$RUN_JOURNAL" = "1" ]; then
  if have journalctl; then
    run_block "Laatste waarschuwingen uit journalctl" '
journalctl -p warning --since "24 hours ago" -n 80 --no-pager 2>/dev/null || true
'
  else
    printf '%s\n' "journalctl niet gevonden." >> "$OUT"
  fi
fi

section "Docker"

if [ "$RUN_DOCKER" = "1" ]; then
  if have docker; then
    run_block "Docker versie" '
docker version 2>/dev/null || true
docker compose version 2>/dev/null || true
'

    if docker ps >/dev/null 2>&1; then
      run_block "Containers" '
docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Image}}\t{{.Ports}}" 2>/dev/null || true
'

      run_block "Docker compose projecten" '
docker compose ls 2>/dev/null || true
'

      run_block "Container policies" '
ids="$(docker ps -aq 2>/dev/null || true)"
for id in $ids; do
  docker inspect --format "{{.Name}}  {{.State.Status}}  restart={{.HostConfig.RestartPolicy.Name}}  image={{.Config.Image}}  privileged={{.HostConfig.Privileged}}" "$id" 2>/dev/null | sed "s#^/##"
done
'
    else
      printf '%s\n' "Docker gevonden, maar deze gebruiker heeft geen toegang tot Docker." >> "$OUT"
    fi
  else
    printf '%s\n' "Docker niet gevonden." >> "$OUT"
  fi
else
  printf '%s\n' "Docker-check overgeslagen." >> "$OUT"
fi

section "Logscan"

if [ "$RUN_LOG_SCAN" = "1" ]; then
  run_block "Journalctl errors laatste 24 uur" '
if command -v journalctl >/dev/null 2>&1; then
  journalctl -p err --since "24 hours ago" -n 120 --no-pager 2>/dev/null || journalctl -p err -n 120 --no-pager 2>/dev/null || true
else
  echo "journalctl niet gevonden."
fi
'

  run_block "Foutregels in bekende logbestanden" '
PATTERN="error|failed|failure|fatal|exception|traceback|panic|segfault|oom|out of memory|no space left|disk full|permission denied|denied|connection refused|bad gateway|upstream timed out|timeout|(^|[^0-9])(502|503|504)([^0-9]|$)"
FOUND=0

for f in \
  /var/log/syslog \
  /var/log/messages \
  /var/log/daemon.log \
  /var/log/kern.log \
  /var/log/auth.log \
  /var/log/nginx/error.log \
  /var/log/apache2/error.log \
  /var/log/caddy/*.log \
  /var/log/traefik/*.log
do
  [ -f "$f" ] || continue

  if [ ! -r "$f" ]; then
    echo "Niet leesbaar: $f"
    continue
  fi

  MATCHES="$(tail -n 3000 "$f" 2>/dev/null | grep -Eai "$PATTERN" | tail -n 25 || true)"

  if [ -n "$MATCHES" ]; then
    FOUND=1
    echo
    echo "### $f"
    printf "%s\n" "$MATCHES"
  fi
done

if [ "$FOUND" != "1" ]; then
  echo "Geen matches gevonden in de standaard logbestanden, of er waren geen leesbare logs."
fi
'

  if [ "$RUN_DOCKER" = "1" ] && have docker; then
    if docker ps >/dev/null 2>&1; then
      run_block "Docker logs met foutregels" '
PATTERN="error|failed|failure|fatal|exception|traceback|panic|segfault|oom|out of memory|no space left|permission denied|connection refused|bad gateway|timeout|(^|[^0-9])(502|503|504)([^0-9]|$)"
FOUND=0

for name in $(docker ps -a --format "{{.Names}}" 2>/dev/null); do
  MATCHES="$(docker logs --since 24h --tail 500 "$name" 2>&1 \
    | grep -Eai "$PATTERN" \
    | grep -Eavi "error_handlers|request_timeout|dead_timeout|redis_idle_timeout|poll_interval|on_complex_arguments|Failed to get system UUID|failed to collect filesystem stats.*overlay2" \
    | tail -n 20 || true)"

  if [ -n "$MATCHES" ]; then
    FOUND=1
    echo
    echo "### Container: $name"
    printf "%s\n" "$MATCHES"
  fi
done

if [ "$FOUND" != "1" ]; then
  echo "Geen matches gevonden in Docker logs."
fi
'
    else
      printf '%s\n' "Docker logscan overgeslagen: deze gebruiker mag Docker niet uitlezen." >> "$OUT"
    fi
  fi
else
  printf '%s\n' "Logscan overgeslagen." >> "$OUT"
fi

if [ -n "$DOMAIN" ]; then
  section "Domeincheck: $DOMAIN"

  run_block "DNS" "
if command -v dig >/dev/null 2>&1; then
  echo 'A-records:'
  dig +short A '$DOMAIN'
  echo
  echo 'AAAA-records:'
  dig +short AAAA '$DOMAIN'
  echo
  echo 'MX-records:'
  dig +short MX '$DOMAIN'
  echo
  echo 'TXT-records:'
  dig +short TXT '$DOMAIN'
  echo
  echo 'DMARC-record:'
  dig +short TXT '_dmarc.$DOMAIN'
elif command -v nslookup >/dev/null 2>&1; then
  nslookup '$DOMAIN'
  nslookup -type=mx '$DOMAIN'
  nslookup -type=txt '$DOMAIN'
else
  getent hosts '$DOMAIN' || true
fi
"

  run_block "TLS-certificaat op poort 443" "
if command -v openssl >/dev/null 2>&1; then
  echo 'Certificaatinformatie:'
  echo | openssl s_client -servername '$DOMAIN' -connect '$DOMAIN:443' 2>/dev/null | openssl x509 -noout -subject -issuer -dates -ext subjectAltName 2>/dev/null || true

  echo
  echo 'Hostname-check:'
  if openssl s_client -verify_hostname '$DOMAIN' -servername '$DOMAIN' -connect '$DOMAIN:443' </dev/null 2>&1 | grep -q 'Verify return code: 0'; then
    echo 'OK: certificaat is geldig voor $DOMAIN'
  else
    echo 'FOUT: certificaat lijkt niet geldig voor $DOMAIN'
    openssl s_client -verify_hostname '$DOMAIN' -servername '$DOMAIN' -connect '$DOMAIN:443' </dev/null 2>&1 | grep -E 'Verify return code|verify error|subject=' || true
  fi
else
  echo 'openssl niet gevonden.'
fi
"
fi

section "Controle voordat je deelt"

cat >> "$OUT" <<'CHECKLIST'
Controleer vooral deze dingen handmatig:

- staan er nog wachtwoorden, tokens of API keys in?
- staan er privégegevens in die je niet wilt delen?
- staan er publieke IP-adressen in die je liever weglaat?
- bevat journalctl gevoelige applicatielogs?

Dit rapport is bedoeld als startpunt voor hulp op LANgenoten.
CHECKLIST

printf '\n✅ Rapport gemaakt: %s\n' "$OUT"
printf '%s\n' "Controleer het rapport altijd zelf voordat je het deelt."
printf '\n%s\n' "Bekijk met:"
printf '  cat %s\n' "$OUT"
printf '\n%s\n' "Delen op LANgenoten:"
printf '%s\n' "  1. Open op je desktop: https://dokter.langenoten.nl/delen.html"
printf '  2. Plak de output van: cat %s\n' "$OUT"
printf '%s\n' "  3. Kopieer de topictekst en open Selfhosting hulp."
