#!/usr/bin/env bash
set -euo pipefail

# dns-hardening-check.sh
#
# Purpose:
#   Provide a vendor-neutral, read-only baseline check for DNS resolver hardening.
#   This script does not make changes by default. It inspects common resolver and
#   host settings that help reduce DNS cache poisoning risk.
#
# What it checks:
#   - Resolver process presence (best-effort)
#   - Listening sockets on port 53
#   - Whether recursion is exposed broadly (best-effort, based on config hints)
#   - Common DNSSEC, cache, and forwarding-related config clues
#   - Basic network exposure and firewall hints where detectable
#
# Usage:
#   ./dns-hardening-check.sh
#   ./dns-hardening-check.sh --help
#
# Notes:
#   - This script is intentionally conservative and non-destructive.
#   - Resolver implementations differ. Some checks may be reported as "unknown".
#   - Run on a host where you have permission to inspect DNS configuration.

usage() {
  cat <<'EOF'
Usage: dns-hardening-check.sh [--help]

Performs a read-only, best-effort DNS hardening review.
No configuration changes are made.
EOF
}

log() {
  printf '[*] %s\n' "$*"
}

warn() {
  printf '[!] %s\n' "$*"
}

pass() {
  printf '[+] %s\n' "$*"
}

info() {
  printf '[-] %s\n' "$*"
}

has_cmd() {
  command -v "$1" >/dev/null 2>&1
}

check_listeners() {
  if has_cmd ss; then
    log "Checking DNS listeners on port 53"
    ss -luntp 2>/dev/null | awk '$5 ~ /:53$/ {print}' || true
  elif has_cmd netstat; then
    log "Checking DNS listeners on port 53"
    netstat -luntp 2>/dev/null | awk '$4 ~ /:53$/ {print}' || true
  else
    warn "Neither ss nor netstat is available; skipping listener check"
  fi
}

check_processes() {
  log "Looking for common recursive resolver processes"
  local found=0
  for p in named unbound dnsmasq knot-resolver pdns_recursor; do
    if pgrep -x "$p" >/dev/null 2>&1; then
      pass "Found running process: $p"
      found=1
    fi
  done
  if [[ "$found" -eq 0 ]]; then
    info "No common resolver process names detected"
  fi
}

check_config_hint() {
  log "Searching for common resolver configuration hints"
  local paths=(
    /etc/bind/named.conf
    /etc/bind/named.conf.options
    /etc/named.conf
    /etc/unbound/unbound.conf
    /etc/dnsmasq.conf
    /etc/knot-resolver/kresd.conf
    /etc/powerdns/recursor.conf
    /etc/powerdns/recursor.d
  )

  local hit=0
  for path in "${paths[@]}"; do
    if [[ -e "$path" ]]; then
      pass "Found config path: $path"
      hit=1
    fi
  done

  if [[ "$hit" -eq 0 ]]; then
    info "No common config paths detected"
  fi
}

check_dnssec_hints() {
  log "Checking for DNSSEC-related configuration hints"
  local files=()
  for f in /etc/bind/named.conf /etc/bind/named.conf.options /etc/named.conf /etc/unbound/unbound.conf /etc/dnsmasq.conf; do
    [[ -f "$f" ]] && files+=("$f")
  done

  if [[ ${#files[@]} -eq 0 ]]; then
    info "No readable DNS config files found for DNSSEC hint review"
    return 0
  fi

  local matched=0
  for f in "${files[@]}"; do
    if grep -Eiq 'dnssec|validate|trust-anchor|auto-trust-anchor-file|root.key' "$f"; then
      pass "DNSSEC-related setting found in: $f"
      matched=1
    fi
  done

  if [[ "$matched" -eq 0 ]]; then
    warn "No obvious DNSSEC settings found in readable config files"
  fi
}

check_recursion_exposure() {
  log "Checking for broad recursion exposure hints"
  local files=()
  for f in /etc/bind/named.conf /etc/bind/named.conf.options /etc/named.conf /etc/unbound/unbound.conf /etc/dnsmasq.conf; do
    [[ -f "$f" ]] && files+=("$f")
  done

  if [[ ${#files[@]} -eq 0 ]]; then
    info "No readable DNS config files found for recursion review"
    return 0
  fi

  local exposed=0
  for f in "${files[@]}"; do
    if grep -Eiq 'allow-recursion|allow-query-cache|recursion.*yes|allow-from|interface|access-control' "$f"; then
      pass "Recursion/access control hints found in: $f"
      exposed=1
    fi
  done

  if [[ "$exposed" -eq 0 ]]; then
    warn "Could not confirm recursion restrictions from readable config files"
  fi
}

check_cache_hints() {
  log "Checking for cache TTL or stale-answer tuning hints"
  local files=()
  for f in /etc/bind/named.conf /etc/bind/named.conf.options /etc/named.conf /etc/unbound/unbound.conf /etc/dnsmasq.conf; do
    [[ -f "$f" ]] && files+=("$f")
  done

  if [[ ${#files[@]} -eq 0 ]]; then
    info "No readable DNS config files found for cache review"
    return 0
  fi

  local matched=0
  for f in "${files[@]}"; do
    if grep -Eiq 'max-cache-ttl|max-negative-ttl|stale|prefetch|min-cache-ttl|rrset-cache-size|cache-size' "$f"; then
      pass "Cache-related tuning hints found in: $f"
      matched=1
    fi
  done

  if [[ "$matched" -eq 0 ]]; then
    info "No obvious cache tuning settings found in readable config files"
  fi
}

check_firewall_hints() {
  log "Checking for firewall tool availability"
  if has_cmd ufw; then
    ufw status verbose 2>/dev/null || true
  elif has_cmd firewall-cmd; then
    firewall-cmd --list-all 2>/dev/null || true
  elif has_cmd iptables; then
    iptables -S 2>/dev/null | grep -E '(:53|dport 53|sport 53)' || true
  else
    info "No common firewall tool detected; skipping firewall hint review"
  fi
}

main() {
  case "${1:-}" in
    -h|--help)
      usage
      exit 0
      ;;
    "")
      ;;
    *)
      warn "Unknown argument: $1"
      usage
      exit 1
      ;;
  esac

  log "DNS cache-poisoning hardening baseline review starting"
  check_processes
  check_listeners
  check_config_hint
  check_dnssec_hints
  check_recursion_exposure
  check_cache_hints
  check_firewall_hints
  log "Review complete"
  cat <<'EOF'

Suggested next steps:
1. Confirm recursion is limited to trusted client networks only.
2. Verify DNSSEC validation is enabled and healthy for signed zones.
3. Ensure source port and transaction ID randomness are active.
4. Review cache TTL overrides and stale-answer behavior.
5. Test changes in a staging or canary resolver before production rollout.
EOF
}

main "$@"