#!/bin/sh
#
# title             : blcheck
# description       : Test any domain against more then 200 black lists.
# author            : Intellex
# contributors      : Darko Poljak
# date              : 20160117
# version           : 0.5.0
# usage             : blcheck [options] <domain_or_ip>
#===============================================================================

# Config {


    # How many tries and for how long to wait for DNS queries
    CONF_DNS_TRIES=2
    CONF_DNS_DURATION=3

    # Blacklists to check
    CONF_BLACKLISTS="
        bl.spamcop.net
        b.barracudacentral.org
        zen.spamhaus.org
        cbl.abuseat.org
        spam.dnsbl.sorbs.net
        dnsbl-0.uceprotect.net
        dnsbl-1.uceprotect.net
        dnsbl-2.uceprotect.net
        dnsbl-3.uceprotect.net
        sbl.spamhaus.org
        xbl.spamhaus.org
        zen.spamhaus.org"


#~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ }


# Definitions {

    # Common regular expresions
    REGEX_IP='\([0-9]\{1,3\}\)\.\([0-9]\{1,3\}\)\.\([0-9]\{1,3\}\)\.\([0-9]\{1,3\}\)'
    REGEX_DOMAIN='\([a-zA-Z0-9]\+\(-[a-zA-Z0-9]\+\)*\.\)\+[a-zA-Z]\{2,\}'
    REGEX_TDL='\([a-zA-Z0-9]\+\(-[a-zA-Z0-9]\+\)*\.\)[a-zA-Z]\{2,\}$'

    # Colors
    RED=$(tput setaf 1)
    GREEN=$(tput setaf 2)
    YELLOW=$(tput setaf 3)
    CLEAR=$(tput sgr0)

    # Define spinner
    SPINNER="-\|/"
    #SPINNER=".oO@*"
    #SPINNER="▉▊▋▌▍▎▏▎▍▌▋▊▉"
    #SPINNER="←↖↑↗→↘↓↙"
    #SPINNER="▁▂▃▄▅▆▇█▇▆▅▄▃▁"
    #SPINNER="▖▘▝▗"
    #SPINNER="┤┘┴└├┌┬┐"
    #SPINNER="◢◣◤◥"
    #SPINNER="◰◳◲◱"
    #SPINNER="◴◷◶◵"
    #SPINNER="◐◓◑◒"


#~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ }


# Macros {

    # Verbose printing
    VERBOSE=0
    info() {
        if [ $VERBOSE -ge "$1" ]; then
            echo "$2"
        fi
    }

    # Error handling
    error() {
        echo "ERROR: $1" >&2
        exit 2
    }

    # Show progres
    progress() {

        # Bar
        x=$(($1 % ${#SPINNER} + 1))
        BAR=$(echo $SPINNER | awk "{ print substr(\$0, ${x}, 1) }")
        if test -z "$PLAIN"; then
            printf "\r ";
        fi

        # BAR as printf arg so that backslash will be litteraly interpreted
        printf "[ %s %3s%% ] checking... %4s / $2  " "$BAR" $(($1 * 100 / $2)) "$1";
    }

    # Resolve the IP
    resolve() {

        # IP already?
        IP=$(echo "$1" | grep "^$REGEX_IP$")
        if [ "$IP" ]; then
            echo "$IP"

        # Resolve domain
        else

            # Handle special resolve types
            case "$2" in
                "ns" ) TYPE="ns"; REGEX="$REGEX_DOMAIN\.$";;
                   * ) TYPE="a";  REGEX="$REGEX_IP$";;
            esac

            case "$CMD" in
                $CMD_DIG ) "$CMD" +short -t "$TYPE" +time=$CONF_DNS_DURATION +tries=$CONF_DNS_TRIES "$1" | grep -om 1 "$REGEX";;
                $CMD_HOST ) "$CMD" -t "$TYPE" -W $CONF_DNS_DURATION -R $CONF_DNS_TRIES "$1" | grep -om 1 "$REGEX";;
            esac
        fi
    }

    # Load the blacklist from file
    loadBlacklists() {

        # Make sure the file is readable
        if [ ! "$1" ]; then
            error "Option -l requires an additional parameter";
        elif [ ! -r $1 ]; then
            error "File $1 cannot be opened for reading, make sure it exists and that you have appropriate privileges"
        fi

        CONF_BLACKLISTS=$(cat "$1")
    }

    # Show help
    help() {
        cat <<HELP
blcheck [options] <domain_or_IP>
Supplied domain must be full qualified domain name.
If the IP is supplied, the PTR check cannot be executed and will be skipped.
-l file     Load blacklists from file, separated by space or new line
-c          Warn if the top level domain of the blacklist has expired
-v          Verbose mode, can be used multiple times (up to -vvv)
-q          Quiet modem with absolutely no output (useful for scripts)
-p          Plain text output (no coloring, no interactive status)
-h          The help you are just reading
Result of the script is the number of blacklisted entries. So if the supplied
IP is not blacklisted on any of the servers the result is 0.
HELP
        exit;
    }

#~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ }

# Parse the params
while getopts :vqphcl: arg; do
    case "$arg" in
        l) loadBlacklists $OPTARG;;
        c) VERIFY_BL=TRUE;;
        v) VERBOSE=$(( (VERBOSE + 1) % 4));;
        q) VERBOSE=-1;;
        p) PLAIN=1 RED="" GREEN="" YELLOW="" CLEAR="" ;;
        h) help;;
        ?) error "Unknown option $OPTARG";;
    esac
done
shift $((OPTIND - 1))

# Get the domain
if [ $# -eq 0 ]; then
    echo "Missing target domain or IP."
    help
fi
TARGET=$1

# Get the command we will use: dig or host
CMD_DIG=$(which dig)
CMD_HOST=$(which host)
if [ "$CMD_DIG" ]; then
    CMD=$CMD_DIG
elif [ "$CMD_HOST" ]; then
    CMD=$CMD_HOST
fi
if [ ! "$CMD" ]; then
    error "Either dig or host command is required."
fi
info 3 "Using $CMD to reslove DNS queries"

# Parse IP
IP=$(resolve "$TARGET")
if [ ! "$IP" ]; then
    error "No DNS record found for $TARGET"
elif [ "$IP" != "$TARGET" ]; then
    DOMAIN=$TARGET
    info 2 "Using $TARGET for target, resolved to $IP"
else
    info 3 "Using $TARGET for target"
fi

# Reverse the IP
REVERSED=$(echo "$IP" | sed -ne "s~^$REGEX_IP$~\4.\3.\2.\1~p")
info 3 "Using $REVERSED for reversed IP"

# Get the PTR
info 3 "Checking the PTR record"
case "$CMD" in
    $CMD_DIG ) PTR=$("$CMD" +short -x "$IP" | sed s/\.$//);;
    $CMD_HOST ) PTR=$("$CMD" "$IP" | grep -o '[^ ]\+$' | sed s/\.$//)
esac

# Validate PTR
if [ ! "$PTR" ]; then
    info 0 "${YELLOW}Warning: PTR lookup failed${CLEAR}"

else

    # Match against supplied domain
    info 1 "PTR resolves to $PTR"
    if [ "$DOMAIN" ]; then
        if [ "$DOMAIN" != "$PTR" ]; then
            info 0 "${YELLOW}Warning: PTR record does not match supplied domain: $TARGET != $PTR${CLEAR}"
        else
            info 1 "${GREEN}PTR record matches supplied domain $PTR${CLEAR}"
        fi
    fi
fi

# Filter out the blacklists
BLACKLISTS=""
for BL in $CONF_BLACKLISTS; do
    if [ "$BL" ]; then

        # Make sure the domain is a proper one
        DOMAIN=$(echo "$BL" | sed -e 's/^[ \t]*//' | grep ^"$REGEX_DOMAIN"$)
        if [ ! "$DOMAIN" ]; then
            info 0 "${YELLOW}Warning: blacklist '$BL' is not valid and will be ignored${CLEAR}"

        else

            # It is a proper blacklist
            if [ "$BLACKLISTS" ]; then
                BLACKLISTS=$(echo -e "$BLACKLISTS\n$DOMAIN")
            else
                BLACKLISTS="$BL"
            fi
        fi
    fi
done

# Make sure we have at least one blacklist
COUNT=$(($(echo "$BLACKLISTS" | wc -l)))
if [ ! "$BLACKLISTS" ] || [ "$COUNT" -eq 0 ]; then
    error "No blacklists have been specified"
fi
info 1 "Matching against $COUNT blacklists"

# Initialize the counters
INVALID=0
PASSED=0
FAILED=0

# Interate over all blacklists
I=0;
for BL in $BLACKLISTS; do
    PREFIX=
    I=$((I + 1))

    # What should we test
    TEST="$REVERSED.$BL."

    # Make sure the info is shown if we are cheking the servers
    if [ "$VERIFY_BL" ] && [ $VERBOSE -lt 1 ]; then
        VERBOSE=1
    fi

    # For verbose output
    if [ $VERBOSE -ge 1 ]; then

        # Show percentage
        STATUS=$(printf " %3s" $((I * 100 / COUNT)))
        STATUS="$STATUS%% "

        # Show additional info
        if [ $VERBOSE -ge 3 ]; then
            PREFIX=$(printf "%-60s" "$TEST")
        else
            PREFIX=$(printf "%-50s" "$BL")
        fi

        PREFIX="$STATUS $PREFIX"
        if test -z "$PLAIN"; then
            printf "%s \b" "$PREFIX"
        fi

    elif [ $VERBOSE -ge 0 ]; then
        if test -z "$PLAIN"; then
            progress "$I" "$COUNT"
        fi
    fi

    # Get the status
    RESPONSE=$(resolve "$TEST")
    START=$(echo "$RESPONSE" | cut -c1-4)

    # Not blacklisted
    if [ ! "$RESPONSE" ]; then

        # Make sure the server is viable
        ERROR=""
        if [ "$VERIFY_BL" ]; then
            TDL=$(echo "$BL" | grep -om 1 '\([a-zA-Z0-9]\+\(-[a-zA-Z0-9]\+\)*\.\)[a-zA-Z]\{2,\}$')
            if [ ! "$(resolve "$TDL" ns)" ]; then
                if test -z "$PLAIN"; then printf "\r"; fi
                printf "%s%sUnreachable server%s\n" "$YELLOW" "$PREFIX" "$CLEAR";
                INVALID=$((INVALID + 1))
                ERROR=TRUE
            fi
        fi

        if [ ! "$ERROR" ]; then
            if [ "$VERIFY_BL" ] || [ $VERBOSE -ge 1 ]; then
                if test -z "$PLAIN"; then printf "\r"; fi
                printf "%s%s✓%s\n" "$CLEAR" "$PREFIX" "$CLEAR";
            fi
            PASSED=$((PASSED + 1))
        fi;

    # Invalid response
    elif [ "$START" != "127."  ]; then
        if [ $VERBOSE -ge 1 ]; then
            if test -z "$PLAIN"; then printf "\r"; fi
            printf "%s%sinvalid response (%s)%s\n" "$YELLOW" "$PREFIX" "$RESPONSE" "$CLEAR";
        fi;
        INVALID=$((INVALID + 1))

    # Blacklisted
    else
        if [ $VERBOSE -ge 1 ]; then
            if test -z "$PLAIN"; then printf "\r"; fi
            printf "%s%sblacklisted (%s)%s\n" "$RED" "$PREFIX" "$RESPONSE" "$CLEAR" "$TARGET";
        elif [ $VERBOSE -ge 0 ]; then
            if test -z "$PLAIN"; then printf "\r                                                          "; printf "\r"; fi
            printf "%s%s%s : %s\n" "$RED" "$BL" "$CLEAR" "$RESPONSE" "$TARGET"
        fi
        FAILED=$((FAILED + 1))
    fi
done

# Print results
if [ $VERBOSE -ge 0 ]; then
    if test -z "$PLAIN"; then
        printf "\r                                                    \n"
    else
        printf "                                                     \n"
    fi
    echo "----------------------------------------------------------"
    echo Results for "$TARGET"
    echo
    printf "%-15s" "Tested:";   echo "${COUNT}"
    printf "%-15s" "Passed:";   echo "${GREEN}${PASSED}${CLEAR}"
    printf "%-15s" "Invalid:";  echo "${YELLOW}${INVALID}${CLEAR}"
    printf "%-15s" "Blacklisted:";  echo "${RED}${FAILED}${CLEAR}"
    echo "----------------------------------------------------------"
fi

exit $FAILED;
