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

# ubuntu-login-audit.sh
#
# Query Ubuntu systemd journal entries related to login activity.
#
# What this script does:
# - Searches journal entries for common authentication, SSH, PAM, sudo, and session events
# - Supports an optional time window
# - Prints a concise event list and a simple summary
# - Avoids destructive actions and makes no changes to the system
#
# What this script does not do:
# - It does not prove user intent or ownership of a source address
# - It does not replace firewall logs, Fail2Ban logs, or centralized SIEM evidence
# - It does not modify journal retention or logging settings
#
# Usage examples:
#   ./ubuntu-login-audit.sh
#   ./ubuntu-login-audit.sh --since "2026-08-01 00:00:00" --until "2026-08-01 23:59:59"
#   ./ubuntu-login-audit.sh --since -24h
#   ./ubuntu-login-audit.sh --unit ssh
#
# Notes:
# - Run as a user with permission to read the system journal, or with sudo if required.
# - Adjust patterns below if your environment uses different authentication services.

show_help() {
  cat <<'EOF'
Usage: ubuntu-login-audit.sh [OPTIONS]

Query Ubuntu systemd journal entries related to login activity.

Options:
  --since TIME     Start of the time window (e.g. "-24h", "2026-08-01 00:00:00")
  --until TIME     End of the time window (e.g. "now", "2026-08-01 23:59:59")
  --unit UNIT      Journal unit to filter on (e.g. ssh, sshd, systemd-logind)
  --help           Show this help message

Examples:
  ./ubuntu-login-audit.sh
  ./ubuntu-login-audit.sh --since -24h
  ./ubuntu-login-audit.sh --since "2026-08-01 00:00:00" --until "2026-08-01 12:00:00"
  ./ubuntu-login-audit.sh --unit ssh
EOF
}

since_arg=()
until_arg=()
unit_arg=()

while [[ $# -gt 0 ]]; do
  case "$1" in
    --since)
      [[ $# -ge 2 ]] || { echo "Error: --since requires a value" >&2; exit 1; }
      since_arg=(--since "$2")
      shift 2
      ;;
    --until)
      [[ $# -ge 2 ]] || { echo "Error: --until requires a value" >&2; exit 1; }
      until_arg=(--until "$2")
      shift 2
      ;;
    --unit)
      [[ $# -ge 2 ]] || { echo "Error: --unit requires a value" >&2; exit 1; }
      unit_arg=(--unit "$2")
      shift 2
      ;;
    -h|--help)
      show_help
      exit 0
      ;;
    *)
      echo "Error: unknown option: $1" >&2
      show_help >&2
      exit 1
      ;;
  esac
done

command -v journalctl >/dev/null 2>&1 || {
  echo "Error: journalctl not found on this system." >&2
  exit 1
}

# Common event sources and message patterns for login auditing.
# These are intentionally broad to support different Ubuntu configurations.
patterns=(
  'sshd'
  'pam_unix'
  'session opened for user'
  'session closed for user'
  'Accepted password for'
  'Accepted publickey for'
  'Failed password for'
  'Invalid user'
  'authentication failure'
  'sudo:'
  'systemd-logind'
)

# Build a journalctl filter expression.
# We use a broad message filter rather than assuming one exact logging format.
journal_args=(journalctl --no-pager --output short-iso)
if [[ ${#since_arg[@]} -gt 0 ]]; then
  journal_args+=("${since_arg[@]}")
fi
if [[ ${#until_arg[@]} -gt 0 ]]; then
  journal_args+=("${until_arg[@]}")
fi
if [[ ${#unit_arg[@]} -gt 0 ]]; then
  journal_args+=("${unit_arg[@]}")
fi

# Try to capture relevant entries. If no entries are found, continue cleanly.
# This script is read-only and safe to run repeatedly.
raw_output=""
for pattern in "${patterns[@]}"; do
  # shellcheck disable=SC2207
  matches=($("${journal_args[@]}" --grep "$pattern" 2>/dev/null || true))
  if [[ ${#matches[@]} -gt 0 ]]; then
    # If grep output is successful, collect via a second call to preserve full lines.
    raw_output+=$'\n'
    raw_output+=$("${journal_args[@]}" --grep "$pattern" 2>/dev/null || true)
  fi
done

# If the journalctl --grep approach returned nothing, try a plain query and filter locally.
if [[ -z ${raw_output//[[:space:]]/} ]]; then
  raw_output=$("${journal_args[@]}" 2>/dev/null || true)
fi

if [[ -z ${raw_output//[[:space:]]/} ]]; then
  echo "No matching login-related journal entries found for the selected window." 
  exit 0
fi

# Deduplicate and sort the output for easier review.
# This is a simple operational aid, not a forensic normalization step.
summary_total=0
summary_success=0
summary_failure=0
summary_session=0
summary_sudo=0
summary_ssh=0
summary_logind=0

printf '%s\n' "$raw_output" \
  | sed '/^[[:space:]]*$/d' \
  | sort -u \
  | while IFS= read -r line; do
      summary_total=$((summary_total + 1))
      case "$line" in
        *'Accepted password for'*|*'Accepted publickey for'*) summary_success=$((summary_success + 1)) ;;
        *'Failed password for'*|*'Invalid user'*|*'authentication failure'*) summary_failure=$((summary_failure + 1)) ;;
      esac
      [[ "$line" == *'session opened for user'* || "$line" == *'session closed for user'* ]] && summary_session=$((summary_session + 1)) || true
      [[ "$line" == *'sudo:'* ]] && summary_sudo=$((summary_sudo + 1)) || true
      [[ "$line" == *'sshd'* ]] && summary_ssh=$((summary_ssh + 1)) || true
      [[ "$line" == *'systemd-logind'* ]] && summary_logind=$((summary_logind + 1)) || true
      printf '%s\n' "$line"
    done

cat <<EOF

--- Summary ---
Selected window: ${since_arg[*]:-default start} -> ${until_arg[*]:-default end}
Journal unit: ${unit_arg[*]:-all units}

Note: Counts below are best-effort indicators derived from matching lines, not authoritative forensic totals.
EOF

# Recompute summary from the deduplicated lines using a second pass for reliability.
# This avoids depending on subshell variable scope from the pipeline above.
mapfile -t deduped_lines < <(printf '%s\n' "$raw_output" | sed '/^[[:space:]]*$/d' | sort -u)
for line in "${deduped_lines[@]}"; do
  summary_total=$((summary_total + 0))
  [[ "$line" == *'Accepted password for'* || "$line" == *'Accepted publickey for'* ]] && summary_success=$((summary_success + 1)) || true
  [[ "$line" == *'Failed password for'* || "$line" == *'Invalid user'* || "$line" == *'authentication failure'* ]] && summary_failure=$((summary_failure + 1)) || true
  [[ "$line" == *'session opened for user'* || "$line" == *'session closed for user'* ]] && summary_session=$((summary_session + 1)) || true
  [[ "$line" == *'sudo:'* ]] && summary_sudo=$((summary_sudo + 1)) || true
  [[ "$line" == *'sshd'* ]] && summary_ssh=$((summary_ssh + 1)) || true
  [[ "$line" == *'systemd-logind'* ]] && summary_logind=$((summary_logind + 1)) || true
done

echo "Matched lines: ${#deduped_lines[@]}"
echo "Likely successful authentications: $summary_success"
echo "Likely failed authentications: $summary_failure"
echo "Session-related lines: $summary_session"
echo "sudo-related lines: $summary_sudo"
echo "sshd-related lines: $summary_ssh"
echo "systemd-logind-related lines: $summary_logind"

echo
cat <<'EOF'
Review tips:
- Verify that timestamps align with the incident window.
- Confirm source IPs and usernames against approved access paths.
- Cross-check with firewall, Fail2Ban, or remote access logs before treating results as evidence.
- If the journal is not persistent or the window is incomplete, treat the result as partial.
EOF