#!/bin/bash


# Takes the first function argument as script name and outputs a "manual page".
#
help() {
  cat << EOH

NAME
    $1 - check zones for being DNSSEC-enabled

SYNOPSIS
    $1 [-d] [-D] [-l] [ZONE] ...
    $1 -h

DESCRIPTION
    This script checks given zones for being DNSSEC-enabled by reading the DS
    resource record(s), related to a particular zone. If no DS RR is available,
    the zone is not DNSSEC-enabled.

OPTIONS
    -h
        This "manual page".

    -d
        output the zones that are DNSSEC-enabled, one zone at a line, with
        a leading '+', e.g. '+ wu6ch.de'.

    -D
        output the zones that are not DNSSEC-enabled, one zone at a line, with
        a leading '-', e.g. '- google.com'.

    -l
        Output two separate lists: one list with the DNSSEC-enabled zones, the
        other list with the remaining zones.

    More than one option may be set, e.g. '-d -D' or '-d -D -l'.

    ZONE ...
        One or more zones, to be checked. If no zone is given, '$default_zone'
        is used as default.

EXIT STATUS
    If all given zones are DNSSEC-enabled, the script exits with 0, otherwise
    with a value unequal to 0.

EXAMPLES
    $1 ; echo \$?
    $1 -d wu6ch.de google.com
    $1 -D consorsbank.de microsoft.com
    $1 -l wu6ch.de google.com consorsbank.de microsoft.com errror.org
    $1 -d -D -l wu6ch.de google.com consorsbank.de microsoft.com errror.org

AUTHOR
    Wolfgang <w6g@wu6ch.de>

LICENSE
    MIT (https://wu6ch.de/bash/functional_bash/LICENSE)

SEE ALSO
    Functional Bash (https://wu6ch.de/bash/functional_bash/)
EOH
}


# Sets the script environment.
#
set_env() {
  source "/usr/local/lib/functional_bash.sh"

  declare -gr dig="dig"
  local default_zone="wu6ch.de"

  # shellcheck disable=SC2034
  local -A options=()
  zones=()

  get_options "dDl" options zones help "$@" && {
    dnssec="$(     get_arg options "d")"
    non_dnssec="$( get_arg options "D")"
    dnssec_list="$(get_arg options "l")"

    (( ${#zones[@]} == 0 )) && zones=( "$default_zone" )
    declare -gr dnssec non_dnssec dnssec_list zones
  }
}


# Converts the 'zones' array to a list and removes all elements that start with
# a dash.
#
filter_zone_arguments() {
  # The "inner function", used for 'lfilter': Takes a string as argument and
  # returns true, if the string does not start with a dash, otherwise false.
  #
  # shellcheck disable=SC2317
  _first_char_non_dash() {
    [ "${1:0:1}" != "-" ]
  }

  list "${zones[@]}" | lfilter _first_char_non_dash
}


# Takes a zone and outputs a (maybe empty) list of DS records, related to this
# zone.
#
get_ds() {
  "$dig" +short "$1" "DS"
}


# Takes a zone and evaluates the list of DS records, related to this zone.
# The function outputs a pair, where the first component is the zone and the
# second component is 'unset', if the DS list is empty, otherwise 'set'.
#
# The helper functions 'get_set' and 'get_unset' delivers opaque base64-encoded
# values (instead of a cleartext string) to prevent interference with a string
# "set" or "unset" or alike.
#
check_dnssec() {
  get_ds "$1" | lnull ; local rval=$?
  to_pair "$1" "$(put_cond "$(get_set)" "$(get_unset)" $rval)"
}


# Takes a list of zones from stdin and maps the function 'check_dnssec' over
# this list.
#
check_zones() {
  lmap check_dnssec
}


# Takes a list of pairs from stdin, where the first component is the zone and
# the second component is 'unset', if the DS list is empty, otherwise 'set'.
# Takes 'set' or 'unset' as function argument. The function outputs the first
# component of all pairs (the zones), where the second component is equal to the
# function argument.
#
filter_zones() {
  # The "inner function", used for 'lfilter': Takes a value as first and a pair
  # as second argument and returns true, if the value and second pair component
  # are equal, otherwise false.
  #
  # shellcheck disable=SC2317
  _compare() {
    [ "$(tsnd "$2")" == "$1" ]
  }

  tfst "$(lfilter _compare "$1")"
}


# Takes a string with DNSSEC zones as function argument, converts this string to
# a list, adds a trailing '+' to each list element, and outputs the list, unless
# the list is empty and if option '-d' is set.
#
put_dnssec() {
  is_unset "$dnssec" || {
    [ -z "$1" ] || list "$1" | lmap echo "+"
  }
}


# Takes a string with non-DNSSEC zones as function argument, converts this
# string to a list, adds a trailing '-' to each list element, and outputs the
# list, unless the list is empty and if option '-D' is set.
#
put_non_dnssec() {
  is_unset "$non_dnssec" || {
    [ -z "$1" ] || list "$1" | lmap echo "-"
  }
}


# Takes a 'set' vs. 'unset' value and outputs "DNSSEC" or "non-DNSSEC",
# depending on this value.
#
put_header() {
  local none
  is_set "$1" ; none="$(put_cond "non-" "" $?)"

  echo
  echo "${none}DNSSEC:"
  [ -n "$none" ] && echo -n "----"
  echo "-------"
}


# Takes a 'set' vs. 'unset' as first and a string with zones as second function
# argument, treats the zones as DNSSEC or non-DNSSEC zones (depending on the
# first function argument), and outputs the zones, together with a related
# header.
#
put_zones() {
  put_header "$1"
  [ -z "$2" ] ; put_cond "$2" "(none)" $?
}


# Takes a string with DNSSEC zones as first and a string with non-DNSSEC zones
# as second function argument and outputs both strings, if option '-l' is set.
#
put_dnssec_list() {
  is_unset "$dnssec_list" || {
    put_zones "$(get_set)" "$1" && put_zones "$(get_unset)" "$2"
  }
}


# Takes a string with non-DNSSEC zones as function argument and returns true,
# if this string is empty.
#
check_for_non_dnssec_zone() {
  [ -z "$1" ]
}


# "Top level" of the script; it contains only function calls.
#
set_env "$@" \
  && is_installed "$dig" \
  && checked_zones="$(filter_zone_arguments | check_zones)" \
  && dnssec_zones="$(filter_zones "$(get_set)" <<< "$checked_zones")" \
  && non_dnssec_zones="$(filter_zones "$(get_unset)" <<< "$checked_zones")" \
  && put_dnssec "$dnssec_zones" \
  && put_non_dnssec "$non_dnssec_zones" \
  && put_dnssec_list "$dnssec_zones" "$non_dnssec_zones" \
  && check_for_non_dnssec_zone "$non_dnssec_zones"


# EOF
