#!/usr/bin/env sh
#
# scripts/policy/verify.sh: standalone copy
#
# Generated from https://hutzelmann.github.io/thi-linux-macos-setup/
# Community-maintained and unofficial. Not THI IT Support. No warranty.
#
# Read it before running it. Every script here supports --dry-run, which prints
# what it would do and changes nothing.
#
# Released into the public domain under CC0 1.0.

# --- documented values, baked in at build time ----------------------------
# Generated from facts/*.yaml. Do not edit here: change the value in that
# repository and the next build regenerates this file.
#
# fact_env is consulted first, exactly as scripts/lib/facts.sh does it, so a
# downloaded script honours FACT_<DOMAIN>_<KEY> like the repository one and a
# substituted value shows up in the result either way.
fact() {
  if fact_env "$1" "$2"; then
    return 0
  fi

  case "$1 $2" in
    'network registration_form_url') printf '%s\n' 'https://forms.office.com/e/rhkQsdbp2R' ;;
    'network registration_validity') printf '%s\n' '1 year' ;;
    'network official_url') printf '%s\n' 'https://mythi.de/index.php?kc=13&pid=moid&objectid=61' ;;
    'network wired_eap') printf '%s\n' 'PEAP' ;;
    'network wired_phase2') printf '%s\n' 'MSCHAPv2' ;;
    'network wired_ca') printf '%s\n' 'HARICA TLS RSA Root CA 2021' ;;
    'network wired_domain_suffix') printf '%s\n' 'rz.fh-ingolstadt.de' ;;
    'network wired_server_name') printf '%s\n' 'rz-radius-01.rz.fh-ingolstadt.de' ;;
    'network wired_official_pdf') printf '%s\n' 'https://mythi.de/kcdownload/kc_13/fileid_285/802.1X-Konfiguration-LAN.pdf' ;;
    'official service_url') printf '%s\n' 'https://www.thi.de/service/it-service/' ;;
    'official kb_url') printf '%s\n' 'https://help.thi.de/help/de-de' ;;
    'official support_mail') printf '%s\n' 'support@thi.de' ;;
    'official ticket_url') printf '%s\n' 'https://help.thi.de/' ;;
    'official department') printf '%s\n' 'Zentraler IT Service' ;;
    'printing server') printf '%s\n' 'rz-print-marb.rz.fh-ingolstadt.de' ;;
    'printing server_previous') printf '%s\n' 'marb-mfp.rz.fh-ingolstadt.de' ;;
    'printing queue') printf '%s\n' 'MARB-color' ;;
    'printing queue_students') printf '%s\n' 'STUD-color' ;;
    'printing model') printf '%s\n' 'Kyocera TASKalfa 6054ci' ;;
    'printing smb_domain') printf '%s\n' 'ESPL_001' ;;
    'printing ppd_path') printf '%s\n' '/usr/share/kyocera10.0/ppd10.0/Kyocera_TASKalfa_6054ci.ppd' ;;
    'printing ppd_model') printf '%s\n' 'kyocera/Kyocera_TASKalfa_6054ci.ppd' ;;
    'printing driver_url') printf '%s\n' 'https://www.kyoceradocumentsolutions.us/en/support/downloads.name-L3VzL2VuL21mcC9UQVNLQUxGQTYwNTRDSQ==.html#tab=driver' ;;
    'printing driver_deb') printf '%s\n' 'Ubuntu/EU/kyodialog_amd64/kyodialog_10.0-0_amd64.deb' ;;
    'printing driver_aur') printf '%s\n' 'kyocera-cups' ;;
    'printing driver_aur_url') printf '%s\n' 'https://aur.archlinux.org/packages/kyocera-cups' ;;
    'printing job_retention') printf '%s\n' 'approximately 24 hours' ;;
    'printing webprint_url') printf '%s\n' 'https://print.thi.de' ;;
    'printing official_url') printf '%s\n' 'https://www.thi.de/service/it-service/drucken/' ;;
    'project repo') printf '%s\n' 'hutzelmann/thi-linux-macos-setup' ;;
    'project stale_after_days') printf '%s\n' '180' ;;
    'shares home_server') printf '%s\n' 'rz-home.rz.fh-ingolstadt.de' ;;
    'shares file_server') printf '%s\n' 'rz-fs.rz.fh-ingolstadt.de' ;;
    'shares research_server') printf '%s\n' 'carissma.rz.fh-ingolstadt.de' ;;
    'shares domain') printf '%s\n' 'ESPL_001' ;;
    'shares official_url') printf '%s\n' 'https://help.thi.de/help/de-de/6-netzlaufwerke' ;;
    'vpn host') printf '%s\n' 'vpn.thi.de' ;;
    'vpn port') printf '%s\n' '443' ;;
    'vpn auth') printf '%s\n' 'saml' ;;
    'vpn issuer') printf '%s\n' 'GEANT TLS ECC 1' ;;
    'vpn root_ca') printf '%s\n' 'HARICA TLS ECC Root CA 2021' ;;
    'vpn intermediate_url') printf '%s\n' 'http://crt.harica.gr/HARICA-GEANT-TLS-E1.cer' ;;
    'vpn intermediate_file') printf '%s\n' 'HARICA-GEANT-TLS-E1.pem' ;;
    'vpn ca_bundle') printf '%s\n' '/etc/openfortivpn/ca-bundle.pem' ;;
    'vpn system_roots') printf '%s\n' '/etc/ssl/certs/ca-certificates.crt' ;;
    'vpn macos_root_keychain') printf '%s\n' '/System/Library/Keychains/SystemRootCertificates.keychain' ;;
    'vpn official_url') printf '%s\n' 'https://www.thi.de/service/it-service/vpn/' ;;
    'wifi eduroam_ssid') printf '%s\n' 'eduroam' ;;
    'wifi eduroam_eap') printf '%s\n' 'PEAP' ;;
    'wifi eduroam_phase2') printf '%s\n' 'MSCHAPv2' ;;
    'wifi eduroam_ca') printf '%s\n' 'HARICA TLS RSA Root CA 2021' ;;
    'wifi eduroam_ca_path_arch') printf '%s\n' '/etc/ca-certificates/extracted/cadir/HARICA_TLS_RSA_Root_CA_2021.pem' ;;
    'wifi eduroam_ca_path_debian') printf '%s\n' '/etc/ssl/certs/HARICA_TLS_RSA_Root_CA_2021.pem' ;;
    'wifi eduroam_domain_suffix') printf '%s\n' 'rz.fh-ingolstadt.de' ;;
    'wifi eduroam_realm') printf '%s\n' 'thi.de' ;;
    'wifi cat_url') printf '%s\n' 'https://cat.eduroam.org/' ;;
    'wifi cat_profile') printf '%s\n' '5182' ;;
    'wifi thi_ssid') printf '%s\n' '@thi' ;;
    'wifi thi_eap') printf '%s\n' 'TTLS' ;;
    'wifi thi_phase2') printf '%s\n' 'MSCHAPv2' ;;
    'wifi thi_ca') printf '%s\n' 'HARICA TLS RSA Root CA 2021' ;;
    'wifi thi_domain_suffix') printf '%s\n' 'rz.fh-ingolstadt.de' ;;
    'wifi onboard_ssid') printf '%s\n' 'thi-onboard' ;;
    'wifi official_url') printf '%s\n' 'https://www.thi.de/service/it-service/wifi/' ;;
    *) echo "unknown fact: $1 $2" >&2; return 1 ;;
  esac
}

# --- shared helpers -------------------------------------------------------
# Shared helpers. Sourceable without side effects: define, never execute.
#
# Every script here follows the same shape so that tests can source it and call
# individual functions. Nothing runs until main() is called behind a guard.

DRY_RUN=${DRY_RUN:-0}
JSON=${JSON:-0}
REPORT=${REPORT:-0}

# Where captured evidence is written, or empty for not capturing any.
# Set by --evidence; see evidence_open below for why this exists at all.
EVIDENCE_DIR=${EVIDENCE_DIR:-}

log() {
  [ "$JSON" = "1" ] && return 0
  printf '%s\n' "$*"
}

warn() {
  printf 'warning: %s\n' "$*" >&2
}

die() {
  printf 'error: %s\n' "$*" >&2
  exit 1
}

# run <command...>: execute, or print it under --dry-run.
#
# Dry-run output is exactly what the page tells the reader to type, which is
# what makes it testable off-campus: assert the emitted command matches the
# documented one.
run() {
  if [ "$DRY_RUN" = "1" ]; then
    printf '  %s\n' "$*"
    return 0
  fi
  "$@"
}

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

# resolved_addresses <host>: what this machine's resolver answers, verbatim.
#
# Captured as evidence rather than reduced to yes or no, because "the name
# resolves" and "the name resolves to the address it used to" are different
# observations and only the second one catches a server being moved. Four
# implementations because a stock macOS has no getent and a stock Arch has no
# host.
resolved_addresses() {
  if have getent; then
    getent ahosts "$1" 2>/dev/null
  elif have dscacheutil; then
    dscacheutil -q host -a name "$1" 2>/dev/null
  elif have host; then
    host "$1" 2>/dev/null
  else
    nslookup "$1" 2>/dev/null
  fi
}

# --- evidence -----------------------------------------------------------------
#
# A check that prints pass or fail can only be debugged by the person standing
# on the network it failed on. That is the expensive part of this project: the
# campus is a place you have to travel to, and a failure you did not record in
# full is a second trip.
#
# So every check can also write down what it saw: the whole certificate chain,
# not "verifies: no"; the resolved addresses, not "reachable: no"; the printer's
# attributes, not "queue: missing". Those go to files rather than into the JSON
# result, because they are multi-line and because the result is meant to stay
# small enough to paste.
#
# Nothing is uploaded. The directory is left on the machine that made it, and
# the closing note says to read it before it goes anywhere, because it is the
# richest thing this repository can produce about somebody's computer.

# evidence_open <domain>: start a capture directory, if --evidence asked for one.
#
# Called by main() before any observation. A no-op when capture is off, so the
# evidence calls below can stand unguarded in the body of a check.
evidence_open() {
  [ -n "$EVIDENCE_DIR" ] || return 0

  if [ "$EVIDENCE_DIR" = "auto" ]; then
    EVIDENCE_DIR="./evidence-$1-$(date +%Y%m%d-%H%M%S)"
  fi

  mkdir -p "$EVIDENCE_DIR" || die "cannot write to $EVIDENCE_DIR"

  {
    printf 'What this is\n'
    printf '\n'
    printf 'Output captured by %s on %s.\n' "${0##*/}" "$(date +%Y-%m-%d)"
    printf 'Operating system: %s\n' "$(detect_os)"
    _overrides=$(facts_overridden)
    [ -n "$_overrides" ] &&
      printf 'Documented values replaced for this run: %s\n' "$_overrides"
    printf '\n'
    printf 'Usernames, home paths and hardware addresses have been replaced with\n'
    printf 'placeholders. Nothing else has. Read these files before you attach them\n'
    printf 'to anything public: this is a description of your machine, and only you\n'
    printf 'can tell whether some line of it names you.\n'
  } >"$EVIDENCE_DIR/README.txt"
}

# evidence <name>: write stdin to <name>.txt in the capture directory.
#
# Reads stdin either way, so a pipeline into it behaves the same whether or not
# capture is on and a check cannot accidentally block on an unread pipe.
evidence() {
  if [ -z "$EVIDENCE_DIR" ]; then
    cat >/dev/null
    return 0
  fi
  redact >"$EVIDENCE_DIR/$1.txt"
}

# evidence_stream <name>: like evidence, but appends.
#
# For a check that observes the same kind of thing several times over, one file
# per host would be a directory nobody reads. Same file, one block per subject.
evidence_stream() {
  if [ -z "$EVIDENCE_DIR" ]; then
    cat >/dev/null
    return 0
  fi
  redact >>"$EVIDENCE_DIR/$1.txt"
}

# evidence_close: say where it went, once, at the end of a readable run.
evidence_close() {
  [ -n "$EVIDENCE_DIR" ] || return 0
  [ "$JSON" = "1" ] && return 0
  printf '\n'
  printf 'What this run observed is written out in full here:\n'
  printf '  %s\n' "$EVIDENCE_DIR"
  printf 'Read it before attaching it to an issue. It describes your machine.\n'
}

# --- documented values, and running against something else --------------------
#
# fact_env <domain> <key>: print an override from the environment, or fail.
#
# Any documented value can be replaced for one run by setting FACT_<DOMAIN>_<KEY>
# uppercased: FACT_VPN_CA_BUNDLE, FACT_PRINTING_SERVER. Both the repository's
# fact() (scripts/lib/facts.sh) and the generated one in the standalone
# downloads consult this first, so the two behave the same.
#
# This exists so a check can run without changing the machine it documents.
# Verifying the VPN bundle from off campus otherwise means writing to /etc, and
# the container that exercises the print queue has no campus print server to
# point at. The documented value stays the default, so an override is only ever
# visible in the command that set it.
fact_env() {
  # The name is built into an eval below. Keys in facts/*.yaml are [a-z0-9_] by
  # contract (AGENTS.md), so anything else is a caller bug, not a lookup.
  case "$1$2" in
    *[!a-z0-9_]*) return 1 ;;
  esac

  _name=$(printf 'FACT_%s_%s' "$1" "$2" | tr '[:lower:]' '[:upper:]')
  # shellcheck disable=SC2154  # _value is assigned by the eval on the line above
  eval "_value=\${${_name}-}"
  [ -n "$_value" ] || return 1

  printf '%s\n' "$_value"
}

# facts_overridden: the overrides in force, as "domain.key", space separated.
#
# Read back out of the environment rather than recorded as fact_env runs,
# because fact() is almost always called inside a command substitution and a
# variable set in a subshell does not survive it.
#
# Everything that reports a result says what was substituted. A run against a
# replaced value observed something real, but it did not observe what the page
# documents, and the two must not be able to look alike. Splitting at the first
# underscore is exact while every facts/ domain is one word, which is what the
# filenames are.
facts_overridden() {
  env |
    sed -n 's/^FACT_\([A-Z0-9]\{1,\}\)_\([A-Z0-9_]\{1,\}\)=..*/\1.\2/p' |
    tr '[:upper:]' '[:lower:]' |
    sort |
    tr '\n' ' ' |
    sed 's/ *$//'
}


detect_os() {
  case "$(uname -s)" in
    Darwin) printf 'macos\n' ;;
    Linux)
      if [ -f /etc/os-release ]; then
        # shellcheck disable=SC1091
        . /etc/os-release
        case "${ID:-}${ID_LIKE:-}" in
          *arch*) printf 'arch\n' ;;
          *debian*|*ubuntu*) printf 'debian\n' ;;
          *) printf 'unknown\n' ;;
        esac
      else
        printf 'unknown\n'
      fi
      ;;
    *) printf 'unknown\n' ;;
  esac
}

# Strip anything that identifies a person before results are pasted into a
# public issue. Contributors should not have to remember this.
#
# A hardware address is on this list because of the wired page: the campus
# registers a MAC against a person for a year, so a MAC in a public issue is
# that person's name with an extra step. The reader still sees their own in the
# readable output, which is theirs to look at; only what leaves the machine is
# replaced. Same for the 802.1X identity, which is the campus username itself.
redact() {
  sed -E \
    -e "s#/home/[^/ ]+#/home/<kennung>#g" \
    -e "s#/Users/[^/ ]+#/Users/<kennung>#g" \
    -e "s#[A-Za-z0-9._%+-]+@(thi|fh-ingolstadt)\.de#<vorname.nachname>@thi.de#g" \
    -e "s#(user|username|-U)[= ][A-Za-z0-9._-]+#\1=<kennung>#g" \
    -e "s#([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}#<mac>#g" \
    -e "s#(identity|anonymous-identity)[:= ]+[A-Za-z0-9._@-]+#\1=<kennung>#g"
}

# json_result <status> <message> [key=value ...]
#
# Stamps any environment override into every result, in one place, so no caller
# can produce a result that hides which value it actually used.
json_result() {
  _status="$1"
  _message="$2"
  shift 2
  printf '{"status":"%s","message":"%s"' "$_status" "$_message"
  for _pair in "$@"; do
    printf ',"%s":"%s"' "${_pair%%=*}" "${_pair#*=}"
  done
  _overrides=$(facts_overridden)
  [ -n "$_overrides" ] && printf ',"facts_overridden":"%s"' "$_overrides"
  printf '}\n'
}

parse_common_args() {
  for _arg in "$@"; do
    case "$_arg" in
      --dry-run) DRY_RUN=1 ;;
      --json) JSON=1 ;;
      --report) REPORT=1 ;;
      --evidence) EVIDENCE_DIR=auto ;;
      --evidence=*) EVIDENCE_DIR=${_arg#--evidence=} ;;
    esac
  done
}

# Percent-encode one argument.
#
# Hand-rolled because the payload has to survive a URL on a stock macOS shell,
# where there is no jq and no python guaranteed. Slow, and it only ever runs on
# a few hundred characters when somebody asks to file a report.
urlencode() {
  _string="$1"
  while [ -n "$_string" ]; do
    _char=$(printf '%s' "$_string" | cut -c1)
    case "$_char" in
      [a-zA-Z0-9.~_-]) printf '%s' "$_char" ;;
      *) printf '%%%02X' "'$_char" ;;
    esac
    _string=$(printf '%s' "$_string" | cut -c2-)
  done
}

# report_url <page> <outcome> <json>: a check-record form, already filled in.
#
# The reader has followed a page and the script knows almost everything the form
# asks for: which page, which OS, what date, and what it observed. Asking them
# to retype it is how a report stops being worth filing.
#
# Prints only. Nothing is sent, and the contributor sees the whole payload in
# the address bar before deciding to publish it. This repository is public and
# the payload describes their machine, so that order matters.
report_url() {
  _page="$1"
  _outcome="$2"
  _json="$3"
  _repo=$(fact project repo) || return 1

  case "$(detect_os)" in
    arch) _os='Arch Linux' ;;
    debian) _os='Debian / Ubuntu' ;;
    macos) _os='macOS' ;;
    *) _os='Other Linux' ;;
  esac

  printf 'https://github.com/%s/issues/new?template=check-record.yml' "$_repo"
  printf '&labels=check-record'
  printf '&page=%s' "$(urlencode "$_page")"
  printf '&os=%s' "$(urlencode "$_os")"
  printf '&date=%s' "$(urlencode "$(date +%Y-%m-%d)")"
  # Prefilled only on a clean pass. A failing script may mean the page is wrong,
  # or may only mean the reader is off campus, and it cannot tell the two apart.
  [ -n "$_outcome" ] && printf '&outcome=%s' "$(urlencode "$_outcome")"
  printf '&report=%s' "$(urlencode "$_json")"
  printf '\n'
}

# report_outcome <status>: the form's outcome field, or nothing.
#
# Prefilled only on a clean pass. A failing script may mean the page is wrong,
# or may only mean the reader is off campus, and it cannot tell those apart. An
# unset dropdown asks the one question only the person who followed the page can
# answer; a wrong default would be answered by reflex.
#
# An overridden value is the same situation with the doubt made explicit: the
# run passed against a value the page does not document, so it says nothing
# about whether the page works. Never prefill an outcome from one.
report_outcome() {
  [ "$1" = ok ] || return 0
  [ -z "$(facts_overridden)" ] || return 0
  # The dropdown option verbatim, German gloss included, because GitHub selects
  # an option by exact text and would otherwise leave the field empty. The gloss
  # is taken off again by stripGloss() in tools/check-record.mjs.
  printf 'Worked exactly as written (hat genau so funktioniert)\n'
}

# print_report <page> <outcome> <json>: the closing block of verify.sh --report.
print_report() {
  printf '\n'
  printf 'File this as a check record. Nothing is sent until you press submit:\n\n'
  printf '  %s\n\n' "$(report_url "$1" "$2" "$3")"
  printf 'The form arrives filled in. Correct the outcome if the page misled you.\n'

  _overrides=$(facts_overridden)
  if [ -n "$_overrides" ]; then
    printf '\n'
    printf 'This run replaced documented values from the environment:\n'
    printf '  %s\n' "$_overrides"
    printf 'It checked those, not the ones on the page, so the outcome is left\n'
    printf 'unset and this is not a check record for the page as written.\n'
  fi
}

# --- script ---------------------------------------------------------------
# Look at the settings the device rules describe, on this machine.
#
# Usage:
#   ./verify.sh           readable
#   ./verify.sh --json    machine-readable, identifiers stripped
#   ./verify.sh --evidence  write down everything it saw, for debugging later
#
# This is not a compliance check and there is nothing to pass. Nothing is sent
# anywhere, nothing is scored, and no result here says whether anybody with
# authority would agree. The page lists six requirements and most people have
# never looked at more than two of them; this prints the state of the ones a
# machine can answer for itself, so the rest of the conversation starts from
# what is actually set.
#
# Read-only throughout, and it does not ask for a password. Two of the six need
# root to see properly, and those say so rather than being guessed at.
set -eu


# The page these checks belong to, for --report.
REPORT_PAGE=/en/policy/secure-device-config

# The inventory number is never printed, recorded or reported.
#
# The requirement is that the hostname matches the sticker, and the sticker
# carries an `IF…` inventory number, which AGENTS.md rule 1 names as personal
# data: it identifies one machine issued to one person. So this reports whether
# the name has the documented shape and never what the name is. Whether it
# matches the sticker is something only the person looking at the sticker can
# say, and this cannot see it.
INVENTORY_PATTERN='^IF[0-9]'

hostname_shape() {
  _name=$(hostname 2>/dev/null || uname -n)
  if printf '%s' "$_name" | grep -qE "$INVENTORY_PATTERN"; then
    printf 'inventory-format\n'
  else
    printf 'other\n'
  fi
}

# --- Linux --------------------------------------------------------------------

# Whether the filesystem holding / sits on an encrypted volume.
#
# Walks from the mount point down to the physical device rather than looking for
# the word LUKS anywhere in lsblk, because a machine with an encrypted backup
# disk attached and a plaintext root would otherwise report as encrypted.
linux_disk_encryption() {
  have lsblk || {
    printf 'unknown\n'
    return
  }
  # findmnt names a btrfs subvolume as `/dev/dm-0[/@]`, and that is not a path
  # lsblk can be given. Only the device part is wanted.
  _root=$(findmnt -no SOURCE / 2>/dev/null | sed 's/\[.*\]$//') || _root=""
  [ -n "$_root" ] || {
    printf 'unknown\n'
    return
  }
  if lsblk -nlo NAME,FSTYPE --inverse "$_root" 2>/dev/null | grep -q crypto_LUKS; then
    printf 'yes\n'
  else
    printf 'no\n'
  fi
}

# Which firewall is running, if any.
#
# The requirement is two things, running and default deny inbound, and only the
# first is visible without root. Reporting the pair as one word would claim the
# half it cannot see.
linux_firewall() {
  for _unit in ufw firewalld nftables iptables; do
    if systemctl is-active --quiet "$_unit" 2>/dev/null; then
      printf '%s\n' "$_unit"
      return
    fi
  done
  printf 'none\n'
}

# Default inbound policy, where it can be read without asking for a password.
#
# /etc/default/ufw is world-readable on both distributions the page covers, and
# it is the same value `ufw status verbose` prints. Everything else answers
# needs-root, which is an honest answer and not a failure.
linux_inbound_policy() {
  case "$1" in
    ufw)
      if [ -r /etc/default/ufw ]; then
        sed -n 's/^DEFAULT_INPUT_POLICY="\{0,1\}\([A-Z]*\)"\{0,1\}.*/\1/p' /etc/default/ufw |
          head -1 | tr '[:upper:]' '[:lower:]'
      else
        printf 'needs-root\n'
      fi
      ;;
    none) printf 'n/a\n' ;;
    *) printf 'needs-root\n' ;;
  esac
}

linux_guest_account() {
  if grep -qE '^guest:' /etc/passwd 2>/dev/null; then
    printf 'present\n'
  else
    printf 'absent\n'
  fi
}

# Whether anything is set up to install updates without being asked.
#
# Arch has no such mechanism and the page does not ask for one: the requirement
# is that updates happen, and on a rolling release that is a person running
# pacman. `manual` is the documented state there, not a shortfall.
linux_updates() {
  if systemctl is-enabled --quiet unattended-upgrades 2>/dev/null; then
    printf 'unattended-upgrades\n'
  elif systemctl is-enabled --quiet dnf-automatic.timer 2>/dev/null; then
    printf 'dnf-automatic\n'
  else
    printf 'manual\n'
  fi
}

linux_scanner() {
  if systemctl is-active --quiet clamav-daemon 2>/dev/null ||
    systemctl is-active --quiet clamav-freshclam 2>/dev/null; then
    printf 'clamav\n'
  elif have clamscan; then
    printf 'clamav-installed-not-running\n'
  else
    printf 'none\n'
  fi
}

# --- macOS --------------------------------------------------------------------

macos_disk_encryption() {
  case "$(fdesetup status 2>/dev/null)" in
    *"FileVault is On"*) printf 'yes\n' ;;
    *"FileVault is Off"*) printf 'no\n' ;;
    *) printf 'unknown\n' ;;
  esac
}

macos_firewall() {
  case "$(/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null)" in
    *enabled*) printf 'application-firewall\n' ;;
    *disabled*) printf 'none\n' ;;
    *) printf 'unknown\n' ;;
  esac
}

macos_guest_account() {
  case "$(defaults read /Library/Preferences/com.apple.loginwindow GuestEnabled 2>/dev/null)" in
    1) printf 'present\n' ;;
    0) printf 'absent\n' ;;
    *) printf 'absent\n' ;;
  esac
}

macos_updates() {
  case "$(softwareupdate --schedule 2>/dev/null)" in
    *on*) printf 'automatic\n' ;;
    *off*) printf 'manual\n' ;;
    *) printf 'unknown\n' ;;
  esac
}

main() {
  parse_common_args "$@"
  evidence_open policy

  _os=$(detect_os)
  _hostname=$(hostname_shape)

  if [ "$_os" = macos ]; then
    _encrypted=$(macos_disk_encryption)
    _firewall=$(macos_firewall)
    _policy=n/a
    _guest=$(macos_guest_account)
    _updates=$(macos_updates)
    # XProtect is part of the system and cannot be turned off, which is what
    # the page says. There is no state to read.
    _scanner=xprotect

    fdesetup status 2>&1 | evidence disk-encryption
    /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>&1 | evidence firewall
    softwareupdate --schedule 2>&1 | evidence updates
  else
    _encrypted=$(linux_disk_encryption)
    _firewall=$(linux_firewall)
    _policy=$(linux_inbound_policy "$_firewall")
    _guest=$(linux_guest_account)
    _updates=$(linux_updates)
    _scanner=$(linux_scanner)

    # The block device tree, with no mount points: those carry usernames.
    lsblk -o NAME,TYPE,FSTYPE,SIZE 2>&1 | evidence disk-encryption
    {
      printf 'active firewall unit: %s\n\n' "$_firewall"
      [ -r /etc/default/ufw ] && cat /etc/default/ufw
    } 2>&1 | evidence firewall
  fi

  # Only the things the page states as requirements, and only where this could
  # actually see them. `unknown` and `needs-root` are neither pass nor fail.
  _attention=0
  [ "$_encrypted" = no ] && _attention=1
  [ "$_firewall" = none ] && _attention=1
  [ "$_policy" = allow ] && _attention=1
  [ "$_guest" = present ] && _attention=1

  _status=ok
  [ "$_attention" = 1 ] && _status=attention

  # Built once, whoever asked for it. --json prints it, --report puts it in the
  # form, and both are the same observation so they cannot disagree.
  #
  # The hostname is a shape, never a name: see INVENTORY_PATTERN above.
  _json=$(json_result "$_status" "device settings" \
    "disk_encryption=$_encrypted" "firewall=$_firewall" "inbound_policy=$_policy" \
    "guest_account=$_guest" "updates=$_updates" "scanner=$_scanner" \
    "hostname_shape=$_hostname" "os=$_os" | redact)

  if [ "$JSON" = "1" ]; then
    printf '%s\n' "$_json"
    evidence_close
    return 0
  fi

  log "Disk encryption:       ${_encrypted}"
  log "Firewall running:      ${_firewall}"
  log "Default inbound:       ${_policy}"
  log "Guest account:         ${_guest}"
  log "Updates:               ${_updates}"
  log "Virus scanner:         ${_scanner}"
  log "Hostname:              ${_hostname}"

  log ""
  log "Nothing here is a verdict. The requirements are a list somebody else"
  log "wrote and this only says what is currently set."

  if [ "$_hostname" != inventory-format ]; then
    log ""
    log "The hostname does not look like an inventory number. That is expected on"
    log "a personal machine and is the requirement on a university one. This"
    log "never prints or records the name itself."
  fi
  if [ "$_policy" = needs-root ]; then
    log ""
    log "The default inbound policy needs root to read. The page's own command is"
    log "  sudo ufw status verbose"
  fi

  # After the advice, not instead of it: a report of what did not work is worth
  # filing too, and it is the report the page most needs.
  #
  # `attention` is a state of this machine, not a fault in the page, so the page
  # can still have worked exactly as written.
  [ "$REPORT" = "1" ] && print_report "$REPORT_PAGE" "$(report_outcome ok)" "$_json"
  evidence_close

  # Always zero, unlike every other check here. The others exit non-zero when
  # the thing the page sets up is not working. This one has nothing to set up:
  # a non-zero exit would be this repository telling somebody their machine
  # fails a policy, which it has no standing to say and no way to know.
  return 0
}


# Entry point. Guarded so tests can source this file and call individual
# functions; the glob also matches the standalone build, which is named
# <domain>-verify.sh.
case "${0##*/}" in
  *verify.sh) main "$@" ;;
esac
