02_web/web-domains-in-scope.sh
#!/bin/bash

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Description:
#     This script takes a list of domain names and checks whether they are wihin a given scope of
#     IPv4 subnets.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Author:
#     @itm4n
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Usage:
#     ./domains_in_scope.sh <DOMAINS_FILE> <SCOPE_FILE> <OUTPUT_FILE>
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Example:
#     ./domains_in_scope.sh domains.txt scope.txt output.txt
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Requirement:
#     'domains.txt' must contain one domain name per line
#     'scope.txt' must contain one IPv4 subnet (CIDR notation) per line
#     'output.txt' will contain a list of "DOMAIN IP" of all the domaines that are in the scope
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

DEBUG=0
COLOR_RST="\e[0m"
COLOR_RED_BOLD="\e[1;31m"
COLOR_GREEN_BOLD="\e[1;32m"
COLOR_YELLOW_BOLD="\e[1;33m"
COLOR_CYAN_BOLD="\e[1;36m"

function usage() {
    echo "Usage:"
    echo "    $0 <DOMAINS_FILE> <SCOPE_FILE> <OUTPUT_FILE>"
}

function print_debug() {
    if [ $DEBUG == 1 ]
    then
        echo -e "${COLOR_CYAN_BOLD}[DEBUG]${COLOR_RST} $1"
    fi
}

function print_info() {
    echo -e "${COLOR_CYAN_BOLD}[*]${COLOR_RST} $1"
}

function print_success() {
    echo -e "${COLOR_GREEN_BOLD}[+]${COLOR_RST} $1"
}

function print_warning() {
    echo -e "${COLOR_YELLOW_BOLD}[!]${COLOR_RST} $1"
}

function print_error() {
    echo -e "${COLOR_RED_BOLD}[-]${COLOR_RST} $1"
}

# Some of the code is taken from:
# https://gist.github.com/thom-nic/2556a6cc3865fba6330f61b802438c05
function ip_in_subnet() {
    net=(${1//\// }) # extract subnet IP
    net_ip=(${net[0]//./ }) # extract IP address from subnet as array
    net_suffix=32 # assume CIDR suffix is 32 if no suffix is provided
    [[ $((${#net[@]})) -gt 1 ]] && net_suffix=${net[1]} # otherwise use the provided suffix

    # Convert mask to array (e.g. 255 255 255 0).
    if [[ ${net_suffix} = '\.' ]]; then  # already mask format like 255.255.255.0
        net_mask_arr=(${net_suffix//./ })
    else # assume CIDR like /24, convert to mask
        if [[ $((net_suffix)) -lt 8 ]]; then
            net_mask_arr=($((256-2**(8-net_suffix))) 0 0 0)
        elif  [[ $((net_suffix)) -lt 16 ]]; then
            net_mask_arr=(255 $((256-2**(16-net_suffix))) 0 0)
        elif  [[ $((net_suffix)) -lt 24 ]]; then
            net_mask_arr=(255 255 $((256-2**(24-net_suffix))) 0)
        elif [[ $((net_suffix)) -lt 32 ]]; then
            net_mask_arr=(255 255 255 $((256-2**(32-net_suffix))))
        elif [[ ${net_suffix} == 32 ]]; then
            net_mask_arr=(255 255 255 255)
        fi
    fi

    # Fix mask (e.g. 240.192.255.0 to 255.255.255.0).
    [[ ${net_mask_arr[2]} == 255 ]] && net_mask_arr[1]=255
    [[ ${net_mask_arr[1]} == 255 ]] && net_mask_arr[0]=255

    # apply mask to IP address and check whether the bytes of the masked IP
    # match the bytes of the subnet IP.
    ip_arr=(${2//./ }) # IP address to array
    ip_masked=($(( net_mask_arr[0] & ip_arr[0] )) $(( net_mask_arr[1] & ip_arr[1] )) $(( net_mask_arr[2] & ip_arr[2] )) $(( net_mask_arr[3] & ip_arr[3] )))
    if [ ${ip_masked[0]} -eq ${net_ip[0]} ] && [ ${ip_masked[1]} -eq ${net_ip[1]} ] && [ ${ip_masked[2]} -eq ${net_ip[2]} ] && [ ${ip_masked[3]} -eq ${net_ip[3]} ]; then
        return 0
    fi

    return 1
}

if [ $# -lt 3 ]
then
    print_error "Missing arguments"
    usage
    exit
fi

CNT_DOMAINS_NOT_RESOLVED=0
CNT_DOMAINS_NO_IPV4=0
CNT_DOMAINS_SCOPE_OUT=0
CNT_DOMAINS_SCOPE_IN=0

FILE_DOMAINS=$1
FILE_SCOPE=$2
FILE_OUTPUT=$3

print_debug "Domains file: '${FILE_DOMAINS}'"
print_debug "Scope file: '${FILE_SCOPE}'"
print_debug "Output file: '${FILE_OUTPUT}'"

if [ ! -f $FILE_DOMAINS ]
then
    print_error "File '${FILE_DOMAINS}' does not exist."
    exit
fi

if [ ! -f $FILE_SCOPE ]
then
    print_error "File '${FILE_SCOPE}' does not exist."
    exit
fi

file_scope_content=$(sort -u "$FILE_SCOPE")
file_scope_content_count=$(echo "$file_scope_content" | wc -l)

print_debug "File '${FILE_SCOPE}' contains ${file_scope_content_count} unique entries."

file_domains_content=$(sort -u "$FILE_DOMAINS")
file_domains_content_count=$(echo "$file_domains_content" | wc -l)

print_info "There are ${file_domains_content_count} unique domain names to check."

domains_in_scope=""

for domain in $file_domains_content
do
    host_result=$(host "$domain") # try to resolve domain name
    if [ $? == 0 ]
    then
        echo "$host_result" | grep -q "has address"
        if [ $? == 0 ]
        then
            ip=$(echo "$host_result" | grep "has address" | head -n1 | cut -d' ' -f4)
            print_debug "Domain name '${domain}' has IP address ${ip}."
            domains_resolved="${domains_resolved}${domain} ${ip}"$'\n'
        else
            print_error "Failed to find IPv4 address for domain '${domain}'."
            CNT_DOMAINS_NO_IPV4=$((CNT_DOMAINS_NO_IPV4+1))
        fi
    else
        print_error "Failed to resolve domain name '${domain}'."
        CNT_DOMAINS_NOT_RESOLVED=$((CNT_DOMAINS_NOT_RESOLVED+1))
    fi
done

domains_resolved_count=$(echo "$domains_resolved" | wc -l)

print_info "Domains that could be resolved: ${domains_resolved_count}"

OLDIFS=$IFS # save IFS
IFS=$'\n' # set new IFS
for domain_resolved in $domains_resolved
do
    in_scope=0 # assume domain is not in scope
    IFS=$OLDIFS # restore IFS
    OLDIFS=$IFS # save IFS
    IFS=' ' # set new IFS
    entry_arr=(${domain_resolved// / }) # convert entry "domain ip" to array { domain, ip }
    IFS=$OLDIFS # restore IFS
    entry_ip=${entry_arr[1]} # extract IP address from entry
    entry_domain=${entry_arr[0]} # extract domain name from entry
    for scope_item in $file_scope_content # loop through all subnets in scope
    do
        ip_in_subnet $scope_item $entry_ip # is the resolved IP within the subnet? ...
        if [ $? -eq 0 ]
        then
            in_scope=1 # ... if yes, mark entry as "in scope" ...
            break # ... and stop the search
        fi
    done

    if [ $in_scope -eq 1 ];
    then
        print_success "Domain name '${entry_arr[0]}' with IP address ${entry_arr[1]} is in scope."
        domains_in_scope="${domains_in_scope}${domain_resolved}"$'\n'
        CNT_DOMAINS_SCOPE_IN=$((CNT_DOMAINS_SCOPE_IN+1))
    else
        print_warning "Domain name '${entry_arr[0]}' with IP address ${entry_arr[1]} is not in scope."
        CNT_DOMAINS_SCOPE_OUT=$((CNT_DOMAINS_SCOPE_OUT+1))
    fi
done
IFS=$OLDIFS # restore IFS

domains_in_scope_count=$(echo "$domains_in_scope" | wc -l)

print_info "Domain names not resolved...............: ${CNT_DOMAINS_NOT_RESOLVED}"
print_info "Domain names with no IPv4 address.......: ${CNT_DOMAINS_NO_IPV4}"
print_info "Domain names that are not in the scope..: ${CNT_DOMAINS_SCOPE_OUT}"
print_info "Domain names that are in the scope......: ${CNT_DOMAINS_SCOPE_IN}"

if [ $CNT_DOMAINS_SCOPE_IN -gt 0 ]
then
    print_success "All done! Writing result to '${FILE_OUTPUT}'."
    echo "$domains_in_scope" > $FILE_OUTPUT
else
    print_warning "The script finished but nothing was found."
fi
