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

# docker-hardening-check.sh
#
# Purpose:
#   Inspect a Docker container or Docker run configuration for common hardening
#   settings before production rollout.
#
# What it checks:
#   - Non-root user inside the container
#   - Read-only root filesystem
#   - Writable mounts and tmpfs usage
#   - Linux capability settings
#   - Privileged mode
#   - Docker socket exposure
#   - Host networking usage
#   - Basic resource controls when available
#
# Safe by design:
#   - Read-only inspection only
#   - No destructive actions
#   - No credentials, secrets, or environment-specific endpoints
#
# Usage examples:
#   ./docker-hardening-check.sh my-container
#   ./docker-hardening-check.sh --image myimage:tag
#   ./docker-hardening-check.sh --help

usage() {
  cat <<'EOF'
Usage:
  docker-hardening-check.sh [OPTIONS] <container-name-or-id>
  docker-hardening-check.sh --image <image:tag>

Options:
  --image <image:tag>   Inspect a one-off container created from an image
  --help                Show this help text

Examples:
  docker-hardening-check.sh my-container
  docker-hardening-check.sh --image myapp:secure
EOF
}

say() {
  printf '%s\n' "$*"
}

warn() {
  printf 'WARN: %s\n' "$*"
}

fail() {
  printf 'FAIL: %s\n' "$*"
}

pass() {
  printf 'PASS: %s\n' "$*"
}

have_docker() {
  command -v docker >/dev/null 2>&1
}

inspect_or_die() {
  local target="$1"
  docker inspect "$target" >/dev/null
}

get_json() {
  local target="$1"
  local path="$2"
  docker inspect -f "{{json ${path}}}" "$target"
}

check_non_root() {
  local target="$1"
  local user
  user="$(docker inspect -f '{{.Config.User}}' "$target")"
  if [[ -z "$user" ]]; then
    warn "Container does not specify a runtime user; it may default to root."
    return 1
  fi
  if [[ "$user" == "0" || "$user" == "root" ]]; then
    fail "Container is configured to run as root ($user)."
    return 1
  fi
  pass "Runtime user configured: $user"
}

check_read_only_rootfs() {
  local target="$1"
  local ro
  ro="$(docker inspect -f '{{.HostConfig.ReadonlyRootfs}}' "$target")"
  if [[ "$ro" == "true" ]]; then
    pass "Read-only root filesystem is enabled"
  else
    warn "Read-only root filesystem is not enabled"
    return 1
  fi
}

check_privileged() {
  local target="$1"
  local privileged
  privileged="$(docker inspect -f '{{.HostConfig.Privileged}}' "$target")"
  if [[ "$privileged" == "true" ]]; then
    fail "Privileged mode is enabled"
    return 1
  fi
  pass "Privileged mode is disabled"
}

check_network_mode() {
  local target="$1"
  local network_mode
  network_mode="$(docker inspect -f '{{.HostConfig.NetworkMode}}' "$target")"
  case "$network_mode" in
    host)
      warn "Host networking is enabled"
      return 1
      ;;
    bridge|default|'')
      pass "Network mode is $network_mode"
      ;;
    *)
      say "INFO: Network mode is $network_mode"
      ;;
  esac
}

check_docker_socket_mount() {
  local target="$1"
  local mounts
  mounts="$(docker inspect -f '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' "$target")"
  if printf '%s' "$mounts" | grep -q '/var/run/docker.sock'; then
    fail "Docker socket appears to be mounted"
    return 1
  fi
  pass "Docker socket not mounted"
}

check_capabilities() {
  local target="$1"
  local cap_add cap_drop
  cap_add="$(docker inspect -f '{{json .HostConfig.CapAdd}}' "$target")"
  cap_drop="$(docker inspect -f '{{json .HostConfig.CapDrop}}' "$target")"

  if [[ "$cap_drop" == "[\"ALL\"]" || "$cap_drop" == "[\"ALL\"]" ]]; then
    pass "All capabilities are dropped by default"
  else
    say "INFO: CapDrop = $cap_drop"
  fi

  if [[ "$cap_add" == "null" || "$cap_add" == "[]" ]]; then
    say "INFO: No additional capabilities were added"
  else
    warn "Additional capabilities are added: $cap_add"
    return 1
  fi
}

check_resource_limits() {
  local target="$1"
  local memory cpu pids
  memory="$(docker inspect -f '{{.HostConfig.Memory}}' "$target")"
  cpu="$(docker inspect -f '{{.HostConfig.NanoCpus}}' "$target")"
  pids="$(docker inspect -f '{{.HostConfig.PidsLimit}}' "$target")"

  if [[ "$memory" != "0" ]]; then
    pass "Memory limit set"
  else
    warn "No memory limit detected"
  fi

  if [[ "$cpu" != "0" ]]; then
    pass "CPU limit set"
  else
    warn "No CPU limit detected"
  fi

  if [[ "$pids" != "0" && "$pids" != "-1" ]]; then
    pass "PIDs limit set"
  else
    warn "No PIDs limit detected"
  fi
}

check_user_inside_container() {
  local target="$1"
  if ! docker exec "$target" id >/dev/null 2>&1; then
    warn "Unable to exec into container to verify runtime identity"
    return 1
  fi
  local id_output
  id_output="$(docker exec "$target" id 2>/dev/null || true)"
  if printf '%s' "$id_output" | grep -q 'uid=0(root)'; then
    fail "Container process appears to run as root: $id_output"
    return 1
  fi
  pass "Container process identity looks non-root: $id_output"
}

main() {
  if [[ $# -eq 0 ]]; then
    usage
    exit 1
  fi

  if ! have_docker; then
    fail "Docker CLI is not installed or not on PATH"
    exit 1
  fi

  local target=""
  local image_mode=false

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --help)
        usage
        exit 0
        ;;
      --image)
        image_mode=true
        shift
        if [[ $# -eq 0 ]]; then
          fail "--image requires an image name"
          exit 1
        fi
        target="$1"
        ;;
      -*)
        fail "Unknown option: $1"
        usage
        exit 1
        ;;
      *)
        target="$1"
        ;;
    esac
    shift || true
  done

  if [[ -z "$target" ]]; then
    fail "No container or image provided"
    usage
    exit 1
  fi

  if [[ "$image_mode" == true ]]; then
    say "Inspecting image by creating a temporary container: $target"
    local tmp_container
    tmp_container="$(docker create "$target" >/dev/null 2>&1; docker create "$target")"
    trap 'docker rm -f "$tmp_container" >/dev/null 2>&1 || true' EXIT
    target="$tmp_container"
  else
    inspect_or_die "$target"
  fi

  say "Docker hardening checks for: $target"
  say "----------------------------------------"

  check_non_root "$target" || true
  check_read_only_rootfs "$target" || true
  check_privileged "$target" || true
  check_network_mode "$target" || true
  check_docker_socket_mount "$target" || true
  check_capabilities "$target" || true
  check_resource_limits "$target" || true

  if [[ "$image_mode" == false ]]; then
    check_user_inside_container "$target" || true
  else
    say "INFO: Skipping in-container id check for image-only mode"
  fi

  say "----------------------------------------"
  say "Review any WARN/FAIL items before production rollout."
}

main "$@"