#!/usr/bin/env bash
# ╭──────────────────────────────────────────────────────────────────────────────────────╮
# │ ASN / IPv4 / IPv6 / Prefix / AS Path / Organization lookup and server tool │
# │ │
# │Project homepage: │
# │ │
# │ https://github.com/nitefood/asn │
# │ │
# │Usage: │
# │ │
# │ (Launch the script without parameters or visit the project's homepage for usage info)│
# ╰──────────────────────────────────────────────────────────────────────────────────────╯
ASN_VERSION="0.78.5"
ASN_LOGFILE="$HOME/asndebug.log"
# ╭──────────────────╮
# │ Helper functions │
# ╰──────────────────╯
docurl(){
# shellcheck disable=SC2124
if [ "$ASN_DEBUG" = true ]; then
parm="$@"
DebugPrint "${yellow}curl $parm${default}"
curloutput=$(curl "$@")
echo "$curloutput"
curloutput_jsonp=$(jq -C '.' <<<"$curloutput" 2>/dev/null)
[[ -n "$curloutput_jsonp" ]] && echo -e "$curloutput_jsonp" >> "$ASN_LOGFILE" || echo "$curloutput" >> "$ASN_LOGFILE"
echo "" >> "$ASN_LOGFILE"
else
curl "$@"
fi
}
WhoisASN(){
found_asname=$(host -t TXT "AS${1}.asn.cymru.com" | grep -v "NXDOMAIN" | awk -F'|' 'NR==1{print substr($NF,2,length($NF)-2)}')
if [ -n "$found_asname" ]; then
((json_resultcount++))
pwhois_full_asn_info=$(whois -h whois.pwhois.org "registry source-as=$1")
# fetch last org only (in case there are multiple orgs listed for this AS)
pwhois_asn_info=$(tac <<<"$pwhois_full_asn_info" | grep -m1 -E "^Org-Name")
pwhois_asn_info+="\n"
pwhois_asn_info+=$(tac <<<"$pwhois_full_asn_info" | grep -m1 -E "^Create-Date")
found_holder=$(docurl -m5 -s "https://stat.ripe.net/data/as-overview/data.json?resource=AS$1&sourceapp=nitefood-asn" | jq -r 'select (.data.holder != null) | .data.holder')
# RIPE usually outputs holder as "ASNAME - actual company name", trim it to just the company name in such cases
found_holder=$(awk -F' - ' '{ if ( $2 ) {print $2} else {print} }' <<<"$found_holder")
found_org=$(echo -e "$pwhois_asn_info" | grep -E "^Org-Name:" | cut -d ':' -f 2 | sed 's/^[ \t]*//')
[[ -z "$found_org" ]] && found_org="N/A"
found_abuse_contact=$(docurl -m5 -s "https://stat.ripe.net/data/abuse-contact-finder/data.json?resource=AS$asn&sourceapp=nitefood-asn" | jq -r 'select (.data.abuse_contacts[0] != null) | .data.abuse_contacts[0]')
[[ -z "$found_abuse_contact" ]] && found_abuse_contact="-"
pwhois_createdate=$(echo -e "$pwhois_asn_info" | grep -E "^Create-Date:" | cut -d ':' -f 2- | sed 's/^[ \t]*//')
if [ "$JSON_OUTPUT" = true ]; then
[[ -z "$pwhois_createdate" ]] && found_createdate="" || found_createdate=$(date -d "$pwhois_createdate" "+%Y-%m-%dT%H:%M:%S")
else
[[ -z "$pwhois_createdate" ]] && found_createdate="N/A" || found_createdate=$(date -d "$pwhois_createdate" "+%Y-%m-%d %H:%M:%S")
fi
fi
}
QueryRipestat(){
StatusbarMessage "Retrieving BGP data for AS$1 ($found_asname)"
# BGP routing stats
ripestat_routing_data=$(docurl -m5 -s "https://stat.ripe.net/data/routing-status/data.json?resource=AS$1&sourceapp=nitefood-asn")
if [ -n "$ripestat_routing_data" ]; then
ripestat_ipv4=$(jq -r '.data.announced_space.v4.prefixes' <<<"$ripestat_routing_data")
ripestat_ipv6=$(jq -r '.data.announced_space.v6.prefixes' <<<"$ripestat_routing_data")
ripestat_bgp=$(jq -r '.data.observed_neighbours' <<<"$ripestat_routing_data")
fi
# BGP neighbours list
StatusbarMessage "Retrieving peering data for AS$1 ($found_asname)"
ripestat_neighbours_data=$(docurl -m10 -s "https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS$1&sourceapp=nitefood-asn")
upstream_peers=$(jq -r '.data.neighbours | sort_by(.power) | reverse[] | select (.type=="left") | .asn' <<<"$ripestat_neighbours_data")
downstream_peers=$(jq -r '.data.neighbours | sort_by(.power) | reverse[] | select (.type=="right") | .asn' <<<"$ripestat_neighbours_data")
uncertain_peers=$(jq -r '.data.neighbours | sort_by(.power) | reverse[] | select (.type=="uncertain") | .asn' <<<"$ripestat_neighbours_data")
if [ "$JSON_OUTPUT" = true ]; then
json_abuse_contacts=$(docurl -m5 -s "https://stat.ripe.net/data/abuse-contact-finder/data.json?resource=AS$1&sourceapp=nitefood-asn" | jq -cM 'select (.data.abuse_contacts != null) | .data.abuse_contacts')
[[ -z "$json_abuse_contacts" ]] && json_abuse_contacts="[]"
json_upstream_peers=$(jq -c --slurp --raw-input 'split("\n") | map(select(length > 0))' <<<"$upstream_peers")
json_downstream_peers=$(jq -c --slurp --raw-input 'split("\n") | map(select(length > 0))' <<<"$downstream_peers")
json_uncertain_peers=$(jq -c --slurp --raw-input 'split("\n") | map(select(length > 0))' <<<"$uncertain_peers")
else
RESOLVE_COUNT=8
OUTPUT_PEERS_PER_LINE=4
# resolve AS names of the first n upstreams
upstream_peercount=$(echo "$upstream_peers" | wc -l)
resolved_upstream_peers=""
count=0
for peer in $(echo -e "$upstream_peers" | head -n $RESOLVE_COUNT); do
(( count++ ))
peername=$(docurl -s "https://stat.ripe.net/data/as-overview/data.json?resource=AS$peer&sourceapp=nitefood-asn" | jq -r '.data.holder' | sed 's/ - .*//' )
if [ "$IS_ASN_CHILD" = true ]; then
resolved_upstream_peers+="${greenbg} $peername ($peer) ${default} "
else
resolved_upstream_peers+="${greenbg} $peername ($peer) ${default} "
fi
[[ $(( count % OUTPUT_PEERS_PER_LINE )) -eq 0 ]] && resolved_upstream_peers+="\n"
done
# and add the remaining ones as AS numbers only
unresolved_peercount=$(( upstream_peercount - RESOLVE_COUNT ))
if [ "$unresolved_peercount" -ge 1 ]; then
resolved_upstream_peers+="and more: "
for peer in $(echo -e "$upstream_peers" | tail -n $unresolved_peercount ); do
if [ "$IS_ASN_CHILD" = true ]; then
resolved_upstream_peers+="$peer${default} "
else
resolved_upstream_peers+="${green}${peer}${default} "
fi
done
fi
upstream_peers="$resolved_upstream_peers"
# resolve AS names of the first n downstreams
downstream_peercount=$(echo "$downstream_peers" | wc -l)
resolved_downstream_peers=""
count=0
for peer in $(echo -e "$downstream_peers" | head -n $RESOLVE_COUNT); do
(( count++ ))
peername=$(docurl -s "https://stat.ripe.net/data/as-overview/data.json?resource=AS$peer&sourceapp=nitefood-asn" | jq -r '.data.holder' | sed 's/ - .*//' )
if [ "$IS_ASN_CHILD" = true ]; then
resolved_downstream_peers+="${yellowbg} $peername ($peer) ${default} "
else
resolved_downstream_peers+="${yellowbg} $peername ($peer) ${default} "
fi
[[ $(( count % OUTPUT_PEERS_PER_LINE )) -eq 0 ]] && resolved_downstream_peers+="\n"
done
# and add the remaining ones as AS numbers only
unresolved_peercount=$(( downstream_peercount - RESOLVE_COUNT ))
if [ "$unresolved_peercount" -ge 1 ]; then
resolved_downstream_peers+="and more: "
for peer in $(echo -e "$downstream_peers" | tail -n $unresolved_peercount ); do
if [ "$IS_ASN_CHILD" = true ]; then
resolved_downstream_peers+="$peer${default} "
else
resolved_downstream_peers+="${yellow}${peer}${default} "
fi
done
fi
downstream_peers="$resolved_downstream_peers"
# resolve AS names of the first n uncertains
uncertain_peercount=$(echo "$uncertain_peers" | wc -l)
resolved_uncertain_peers=""
count=0
for peer in $(echo -e "$uncertain_peers" | head -n $RESOLVE_COUNT); do
(( count++ ))
peername=$(docurl -s "https://stat.ripe.net/data/as-overview/data.json?resource=AS$peer&sourceapp=nitefood-asn" | jq -r '.data.holder' | sed 's/ - .*//' )
if [ "$IS_ASN_CHILD" = true ]; then
resolved_uncertain_peers+="${lightgreybg} $peername ($peer) ${default} "
else
resolved_uncertain_peers+="${lightgreybg} $peername ($peer) ${default} "
fi
[[ $(( count % OUTPUT_PEERS_PER_LINE )) -eq 0 ]] && resolved_uncertain_peers+="\n"
done
# and add the remaining ones as AS numbers only
unresolved_peercount=$(( uncertain_peercount - RESOLVE_COUNT ))
if [ "$unresolved_peercount" -ge 1 ]; then
resolved_uncertain_peers+="and more: "
for peer in $(echo -e "$uncertain_peers" | tail -n $unresolved_peercount ); do
if [ "$IS_ASN_CHILD" = true ]; then
resolved_uncertain_peers+="$peer${default} "
else
resolved_uncertain_peers+="${white}${peer}${default} "
fi
done
fi
uncertain_peers="$resolved_uncertain_peers"
fi
StatusbarMessage "Retrieving prefix allocations and announcements for AS$1 ($found_asname)"
ipv4_inetnums=""
ipv6_inetnums=""
json_ipv4_other_inetnums=""
json_ipv6_other_inetnums=""
ripe_prefixes=$(docurl -m10 -s "https://stat.ripe.net/data/announced-prefixes/data.json?resource=$1&sourceapp=nitefood-asn" | jq -r '.data.prefixes[].prefix')
json_ripe_prefixes=$(jq -cM --slurp --raw-input 'split("\n") | map(select(length > 0)) | {v4:map(select(contains(":")|not)), v6:map(select(contains(":")))}' <<<"$ripe_prefixes")
ipv4_ripe_prefixes=$(grep -v ":" <<<"$ripe_prefixes" | grep -Ev "^$" | sort)
ipv4_ripe_prefixes_count=$(wc -l <<<"$ipv4_ripe_prefixes")
ipv6_ripe_prefixes=$(grep ":" <<<"$ripe_prefixes" | grep -Ev "^$" | sort)
ipv6_ripe_prefixes_count=$(wc -l <<<"$ipv6_ripe_prefixes")
# open persistent tcp connection to RIPE whois server
exec 6<>/dev/tcp/whois.ripe.net/43
prefixcounter=0
for prefix in $ipv6_ripe_prefixes; do
((prefixcounter++))
StatusbarMessage "Retrieving information for IPv6 prefix $prefixcounter/$ipv6_ripe_prefixes_count"
# old way (one whois lookup per prefix)
# inet6nums=$(whois -h whois.ripe.net -- "-T inet6num -K -L --resource $prefix" | \
# grep -m2 inet6num | cut -d ':' -f 2- | sed 's/^[ \t]*//')
# new way (direct tcp connection to whois server with persistent whois connection)
echo -e "-k -T inet6num -K -L --resource $prefix" >&6
whoisoutput=""
# read whois output from the tcp stream line by line
while IFS= read -r -u 6 whoisoutputline; do
if [ -n "$whoisoutputline" ]; then
whoisoutput+="$whoisoutputline\n"
continue
fi
# last line was empty, check if next line is empty too
# if we get two empty lines in a row, the whois output is finished
IFS= read -r -u 6 whoisoutputline
[[ -z "$whoisoutputline" ]] && break || whoisoutput+="$whoisoutputline\n"
done
inet6nums=$(echo -e "$whoisoutput" | grep -m2 inet6num | cut -d ':' -f 2- | sed 's/^[ \t]*//')
for inet6num in $inet6nums; do
# exclude RIR supernets
prefix_size=$(echo "$inet6num" | cut -d '/' -f 2)
[[ "$prefix_size" -le 12 ]] && continue || ipv6_inetnums+="${inet6num}\n"
done
done
prefixcounter=0
lookedup_parents_cache=""
for prefix in $ipv4_ripe_prefixes; do
((prefixcounter++))
StatusbarMessage "Retrieving information for IPv4 prefix $prefixcounter/$ipv4_ripe_prefixes_count"
# old way (one whois lookup per prefix)
# parent_inetnum=$(whois -h whois.ripe.net -- "-T inetnum -K -L --resource $prefix" | \
# grep -E -m1 "^inetnum" | awk '{print $2"-"$4}' | xargs ipcalc -r 2>/dev/null | grep -v "deaggregate")
# new way (direct tcp connection to whois server with persistent whois connection)
echo -e "-k -T inetnum -K -L --resource $prefix" >&6
whoisoutput=""
# read whois output from the tcp stream line by line
while IFS= read -r -u 6 whoisoutputline; do
if [ -n "$whoisoutputline" ]; then
whoisoutput+="$whoisoutputline\n"
continue
fi
# last line was empty, check if next line is empty too
# if we get two empty lines in a row, the whois output is finished
IFS= read -r -u 6 whoisoutputline
[[ -z "$whoisoutputline" ]] && break || whoisoutput+="$whoisoutputline\n"
done
parent_inetnum=$(echo -e "$whoisoutput" | grep -E -m1 "^inetnum")
if [ -n "$parent_inetnum" ]; then
parent_inetnum=$(awk '{print $2"-"$4}' <<<"$parent_inetnum")
parent_inetnum=$(IpcalcDeaggregate "$parent_inetnum")
# check if the inetnum containing this prefix is being announced by the same AS as the prefix itself, otherwise
# it means it's part of a larger supernet by some other AS (e.g. larger carrier allocating own prefix to smaller customer)
if ! grep -q "$parent_inetnum" <<<"$lookedup_parents_cache"; then
# this parent inetnum hasn't been looked up yet
lookedup_parents_cache+="$parent_inetnum\n"
LookupASNAndRouteFromIP "$parent_inetnum"
if [ -z "$found_asname" ] || [ "$1" = "$found_asn" ]; then
# the target AS is also announcing the larger inetnum, or nobody is announcing it. Either way consider it part of the target's resources
ipv4_inetnums+="$parent_inetnum\n"
else
# the larger inetnum is being announced by another AS, only add the announced (smaller) prefix to the list
ipv4_inetnums+="$prefix\n"
fi
else
# this parent inetnum has already been looked up
# if it's not present in the list of ipv4_inetnums, it means it's part of a larger supernet by some other AS.
# therefore we only add the announced (smaller) prefix to the list
if ! grep -q "$parent_inetnum" <<<"$ipv4_inetnums"; then
ipv4_inetnums+="$prefix\n"
fi
fi
else
ipv4_inetnums+="$prefix\n"
fi
done
# close persistent tcp connection to RIPE whois server
if { true >&6; } 2<> /dev/null; then
echo -e "-k" >&6
fi
if [ "$ADDITIONAL_INETNUM_LOOKUP" = true ]; then
# fetch further inetnums allocated to this AS from pWhois
StatusbarMessage "Identifying additional INETNUMs (not announced or announced by other AS) allocated to AS$1"
pwhois_prefixes=$(PwhoisListPrefixesForOrg "$found_org")
pwhois_unique_prefixes=$(comm -13 <(echo -e "$ipv4_ripe_prefixes" | sort) <(echo -e "$pwhois_prefixes" | sort))
pwhois_unique_prefixes=$(comm -13 <(echo -e "$lookedup_parents_cache" | sort) <(echo -e "$pwhois_unique_prefixes" | sort))
if [ -n "$pwhois_unique_prefixes" ]; then
pwhois_unique_prefixes_count=$(wc -l <<<"$pwhois_unique_prefixes")
StatusbarMessage "Identifying origin AS for $pwhois_unique_prefixes_count additional IPv4 prefix(es)"
# NEW WAY (bulk query to Team Cymru whois server)
# map the unique prefixes pWhois reported to an array
mapfile -t pwhois_unique_prefixes_array < <(echo -e "$pwhois_unique_prefixes")
# assemble a bulk Team Cymru whois lookup query for the new prefixes found in pWhois.
# we'll check if they're announced by the target AS, by different one, or by no one at all
# (e.g. target AS has delegated announcements for this prefix to another AS, or is not announcing it)
# and compile a list to integrate into the allocated IP resources for this AS
teamcymru_bulk_query="begin\n"
for prefix in $pwhois_unique_prefixes; do
teamcymru_bulk_query+="$prefix\n"
done
teamcymru_bulk_query+="end"
prefixcounter=0
for single_prefix_data in $(echo -e "$teamcymru_bulk_query" | ncat --no-shutdown whois.cymru.com 43 | grep "|" | sed 's/\ *|\ */|/g'); do
prefix="${pwhois_unique_prefixes_array[$prefixcounter]}"
prefix_originator_asn=$(echo "$single_prefix_data" | cut -d '|' -f 1)
if [ "$prefix_originator_asn" = "$1" ]; then
# prefix originator is same as target AS, add this prefix to the allocated IP resources for this AS
ipv4_inetnums+="$prefix\n"
elif [ "$prefix_originator_asn" = "NA" ]; then
# prefix not announced, add this prefix to the allocated IP resources for this AS with a "not announced" remark
if [ "$JSON_OUTPUT" = true ]; then
[[ -n "$json_ipv4_other_inetnums" ]] && json_ipv4_other_inetnums+=","
json_ipv4_other_inetnums+="{\"prefix\":\"$prefix\",\"origin_asn\":\"\",\"origin_org\":\"\", \"is_announced\":false}"
elif [ "$IS_ASN_CHILD" = true ]; then
# skip colors, they will be added along with hyperlinks later
ipv4_inetnums+=$(printf "%-18s → not announced" "$prefix")
ipv4_inetnums+="\n"
else
ipv4_inetnums+=$(printf "${dim}%-18s → ${red}not announced${default}${green}" "$prefix")
ipv4_inetnums+="\n"
fi
else
# prefix is announced by a different AS, add this prefix to the allocated IP resources for this AS
prefix_originator_org=$(echo "$single_prefix_data" | cut -d '|' -f 3)
if [ "$JSON_OUTPUT" = true ]; then
[[ -n "$json_ipv4_other_inetnums" ]] && json_ipv4_other_inetnums+=","
json_ipv4_other_inetnums+="{\"prefix\":\"$prefix\",\"origin_asn\":\"$prefix_originator_asn\",\"origin_org\":\"$prefix_originator_org\", \"is_announced\":true}"
elif [ "$IS_ASN_CHILD" = true ]; then
# skip colors, they will be added along with hyperlinks later
ipv4_inetnums+=$(printf "%-18s → announced by AS%s %s" "$prefix" "${prefix_originator_asn}" "${prefix_originator_org}")
ipv4_inetnums+="\n"
else
ipv4_inetnums+=$(printf "%-18s ${dim}→ announced by ${default}${red}AS%s ${default}${green}%s" "$prefix" "${prefix_originator_asn}" "${prefix_originator_org}")
ipv4_inetnums+="\n"
fi
fi
((prefixcounter++))
done
# OLD WAY (one lookup per prefix)
# for prefix in $pwhois_unique_prefixes; do
# # found a new prefix in pWhois, check if it's announced by a different AS
# # (e.g. target AS has delegated announcements for this prefix to another AS)
# ((prefixcounter++))
# StatusbarMessage "Identifying origin AS for additional IPv4 prefix $prefixcounter/$pwhois_unique_prefixes_count"
# LookupASNAndRouteFromIP "$prefix"
# prefix_originator_asn="$found_asn"
# if [ -z "$prefix_originator_asn" ] || [ "$prefix_originator_asn" = "$1" ]; then
# # prefix originator is same as target AS, or prefix not announced
# # add this prefix to the allocated IP resources for this AS
# ipv4_inetnums+="$prefix\n"
# else
# # this prefix is allocated to target AS, but is being announced by a different AS
# prefix_originator_org=$(docurl -m5 -s "https://stat.ripe.net/data/as-overview/data.json?resource=AS${found_asn}&sourceapp=nitefood-asn" | \
# jq -r 'select (.data.holder != null) | .data.holder' | \
# awk -F' - ' '{ if ( $2 ) {print $2} else {print} }' \
# )
# if [ "$JSON_OUTPUT" = true ]; then
# [[ -n "$json_ipv4_other_inetnums" ]] && json_ipv4_other_inetnums+=","
# json_ipv4_other_inetnums+="{\"prefix\":\"$prefix\",\"origin_asn\":\"$prefix_originator_asn\",\"origin_org\":\"$prefix_originator_org\"}"
# else
# ipv4_inetnums+="$prefix (announced by AS${prefix_originator_asn} - ${prefix_originator_org})\n"
# fi
# fi
# done
fi
fi
if [ -n "$ipv4_inetnums" ]; then
ipv4_inetnums=$(echo -e "$ipv4_inetnums" | sort -iu)
if [ "$IS_ASN_CHILD" = true ] && [ "$JSON_OUTPUT" = false ]; then
# HTML output
html=""
for inetnum in $ipv4_inetnums; do
if grep -q "announced by" <<<"$inetnum"; then
# handle special case " → announced by AS"
actual_inetnum=${inetnum:0:18}
originator=$(cut -d ' ' -f 7 <<<"$inetnum")
rest_of_line=$(cut -d ' ' -f 8- <<<"$inetnum")
html+="$actual_inetnum"
html+=" → announced by "
html+="$originator ${rest_of_line}\n"
elif grep -q "not announced" <<<"$inetnum"; then
# handle special case " → not announced"
actual_inetnum=${inetnum:0:18}
html+="$actual_inetnum"
html+=" → not announced\n"
else
html+="$inetnum\n"
fi
done
ipv4_inetnums="$html"
fi
fi
if [ -n "$ipv6_inetnums" ]; then
ipv6_inetnums=$(echo -e "$ipv6_inetnums" | sort -u)
if [ "$IS_ASN_CHILD" = true ] && [ "$JSON_OUTPUT" = false ]; then
html=""
for inet6num in $ipv6_inetnums; do
html+="$inet6num\n"
done
ipv6_inetnums="$html"
fi
fi
if [ "$JSON_OUTPUT" = true ]; then
json_ipv4_aggregated_inetnums=$(jq -cM --slurp --raw-input 'split("\n") | map(select(length > 0))' <<<"$ipv4_inetnums")
json_ipv6_aggregated_inetnums=$(jq -cM --slurp --raw-input 'split("\n") | map(select(length > 0))' <<<"$ipv6_inetnums")
fi
StatusbarMessage
}
RIPESuggestASN(){
TRIM_WHITESPACES=false
input=$(tr '[:lower:]' '[:upper:]' <<<"$1")
ripe_suggest_output=""
while true; do
for input_variation in "${input}" "AS_${input}" "AS-${input}" "${input}_AS" "${input}-AS"; do
StatusbarMessage "Retrieving suggested ASNs for ${bluebg}${input_variation}${lightgreybg}"
# lookup input variation (AS_, AS-, _AS, -AS)
ripe_suggest_output+=$(docurl -s "https://stat.ripe.net/data/searchcomplete/data.json?resource=${input_variation}&sourceapp=nitefood-asn" | \
jq -r '.data.categories[] | select ( .category == "ASNs" ) | .suggestions[]')
done
StatusbarMessage
if [ -n "$ripe_suggest_output" ]; then
found_suggestions=$(jq -r '.description' <<<"$ripe_suggest_output" | sort -u)
for suggestion in $found_suggestions; do
echo -e "\n${green}$suggestion${default}"
for suggestion_asn in $(jq -r 'select (.description=="'"$suggestion"'") | .value' <<<"$ripe_suggest_output" | awk 'NR==1{print}'); do
echo -en "\t${yellow}$suggestion_asn${default} (Rank: "
GetCAIDARank "${suggestion_asn:2}"
echo -en "${caida_asrank_recap}"
echo "${default})"
done
done
echo ""
return
elif [ "$TRIM_WHITESPACES" = false ]; then
TRIM_WHITESPACES=true
oldinput="$input"
# shellcheck disable=SC2001
input=$(echo "$oldinput" | sed 's/[ \t]*//g')
if [ "$input" = "$oldinput" ]; then
echo -e "\n${redbg}No suggestions found${default}\n"
return
else
continue
fi
else
echo -e "\n${redbg}No suggestions found${default}\n"
return
fi
done
}
WhoisIP(){
# $1: (mandatory) IP to lookup
# $2: (optional) if set to anything, only perform a generic whois lookup (skip pWhois/RPKI/IXP lookups)
local WHOIS_TIMEOUT=20
[[ "$JSON_OUTPUT" = true ]] && ((json_resultcount++))
GENERIC_WHOIS_LOOKUP_ONLY=false
[[ "$#" -gt 1 ]] && GENERIC_WHOIS_LOOKUP_ONLY=true
full_whois_data=$(timeout $WHOIS_TIMEOUT whois "$1" 2>/dev/null)
network_whois_data=$(echo -e "$full_whois_data" | grep -i -E "^netname:|^orgname:|^org-name:|^owner:|^descr:|^country:")
# fetch whois inetnum and later compare to cymru prefix, in order to find smallest match (sometimes whois and cymru/pwhois prefix data diverge)
whois_inetnum=$(IpcalcDeaggregate "$(grep -E -m1 "inet[6]?num|NetRange"<<<"$full_whois_data" | awk '{print $2 $3 $4}')")
# handle problematic IPs where whois gives out wrong info
[[ "$whois_inetnum" = "192.168.1.1/32" ]] && whois_inetnum="$found_route"
ixp_data=""
ixp_geo=""
ip_type_json_output=""
# Check if input is a bogon address
if [ "$IS_BOGON" = false ]; then
ip_type_json_output+="\"is_bogon\":false"
if [ "$GENERIC_WHOIS_LOOKUP_ONLY" = false ]; then
hostname=$(RdnsLookup "$1")
[[ -z "$hostname" ]] && hostname="-"
abuse_whois_data=$(echo -e "$full_whois_data" | grep -E "^OrgAbuseEmail:|^abuse-c:|^% Abuse|^abuse-mailbox:")
abusecontacts=$(AbuseLookupForPrefix "$abuse_whois_data" "$1")
fi
if [ "$UNANNOUNCED_PREFIX" = false ] && [ "$GENERIC_WHOIS_LOOKUP_ONLY" = false ]; then
# Prefix found in the Team Cymru DB, perform pWhois lookup and CAIDA rank lookup
PwhoisLookup "$1"
GetCAIDARank "$found_asn"
else
# No data in the Team Cymru DB for this IP (unannounced prefix), or pWhois being skipped
if [ "$GENERIC_WHOIS_LOOKUP_ONLY" = false ]; then
[[ -z "$network_whois_data" ]] && PrintErrorAndExit "Error: no data found for $input"
found_asn="N/A (address not announced)"
found_asname=""
fi
IPGeoRepLookup "$1"
IPShodanLookup "$1"
# check if it's an IXP, otherwise fall back to generic whois
[[ "$GENERIC_WHOIS_LOOKUP_ONLY" = false ]] && IsIXP "$1"
if [ -n "$ixp_data" ]; then
if [ "$JSON_OUTPUT" = true ]; then
pwhois_org="$ixp_data"
else
pwhois_org="${bluebg} IXP ${default} ${blue}${ixp_data}${default}"
ip_type_data=" ${yellowbg} Internet Exchange ${default}"
fi
else
pwhois_org=$(echo -e "$network_whois_data" | grep -i -E "^orgname:|^org-name:|^owner:" | cut -d ':' -f 2 | sed 's/^[ \t]*//' | while read -r line; do echo -n "$line / "; done | sed 's/ \/ $//')
fi
[[ -z "$pwhois_org" ]] && pwhois_org="N/A"
found_route=$(echo -e "$network_whois_data" | grep -i -m2 -E "^descr:" | cut -d ':' -f 2 | sed 's/^[ \t]*//' | while read -r line; do if [ -n "$line" ]; then echo -n "$line / "; fi; done | sed 's/ \/ $//')
[[ -z "$found_route" ]] && found_route="N/A"
pwhois_net=$(echo -e "$network_whois_data" | grep -i -E "^netname:" | cut -d ':' -f 2 | sed 's/^[ \t]*//' | while read -r line; do echo -n "$line / "; done | sed 's/ \/ $//')
[[ -z "$pwhois_net" ]] && pwhois_net="N/A"
if [ -n "$ixp_geo" ]; then
pwhois_geo="$ixp_geo"
elif [ -n "$ip_geo_data" ]; then
pwhois_geo="$ip_geo_data"
else
pwhois_geo=$(echo -e "$network_whois_data" | grep -m1 -i -E "^country:" | cut -d ':' -f 2 | sed 's/^[ \t]*//')
geo_cc_json_output="$pwhois_geo"
fi
[[ -z "$pwhois_geo" ]] && pwhois_geo="N/A"
fi
[[ -n "$ixp_data" ]] && ip_type_json_output+=",\"is_ixp\":true" || ip_type_json_output+=",\"is_ixp\":false"
else
# bogon address, skip lookups
ip_type_json_output+="\"is_bogon\":true"
ip_type_json_output+=",\"bogon_type\":\"$json_bogon_type\""
hostname="-"
found_asn="-"
pwhois_org="IANA"
found_route="N/A"
abusecontacts="-"
pwhois_net=$(echo -e "$network_whois_data" | grep -i -E "^netname:" | cut -d ':' -f 2 | sed 's/^[ \t]*//' | while read -r line; do echo -n "$line / "; done | sed 's/ \/ $//')
found_asname=""
ip_type_data=" $bogon_tag"
pwhois_geo="-"
ip_rep_data="-"
fi
indent=$(( longest+4 ))
if [ -n "$found_asname" ]; then
output_asname="${green}($found_asname)"
else
output_asname=""
fi
rpki_output=""
if [ "$UNANNOUNCED_PREFIX" = false ] && [ "$GENERIC_WHOIS_LOOKUP_ONLY" = false ]; then
# we skip RPKI lookup in both cases because if SKIP_WHOIS=true then we're being
# called from TraceASPath, and RPKI lookup will be performed there subsequently
StatusbarMessage "Checking RPKI validity for ${bluebg}AS${found_asn}${lightgreybg} and prefix ${bluebg}${found_route}${lightgreybg}"
RPKILookup "$found_asn" "$found_route"
StatusbarMessage
[[ "$JSON_OUTPUT" = false ]] && echo ""
elif [ "$IS_BOGON" = false ]; then
rpki_output="${red}N/A (address not announced)${default}"
else
rpki_output="-"
fi
found_subprefix="$found_route"
whois_routename=""
# compare cymru net with whois net and pick longer (smaller), while retaining larger for route information.
# we want to identify subnets to which target IPs belong, even when they aren't
# announced directly but within a larger route.
if [ "$found_route" != "N/A" ] && [ "$found_route" != "$whois_inetnum" ]; then
foundroute_prefixlen=$(cut -d '/' -f 2 <<<"$found_route")
whois_prefixlen=$(cut -d '/' -f 2 <<<"$whois_inetnum")
if (( whois_prefixlen > foundroute_prefixlen )) 2>/dev/null; then
found_subprefix="$whois_inetnum"
# lookup route name (RIPE)
whois_routename=$(timeout $WHOIS_TIMEOUT whois "$found_route" | grep -m1 -E "^descr:" | cut -d ':' -f 2 | sed 's/^ *//g')
fi
fi
if [ "$JSON_OUTPUT" = true ]; then
# JSON output
final_json_output+="{"
final_json_output+="\"ip\":\"$1\","
grep -q ":" <<<"$1" && ipversion="6" || ipversion="4"
final_json_output+="\"ip_version\":\"$ipversion\","
[[ "$hostname" != "-" ]] && final_json_output+="\"reverse\":\"$hostname\","
final_json_output+="\"org_name\":\"$pwhois_org\","
# next field can be == $found_route or can be different (smaller) in case the IP belongs to a subnet (of $found_route) that's not routed directly
final_json_output+="\"net_range\":\"$found_subprefix\","
final_json_output+="\"net_name\":\"$pwhois_net\","
if [ -n "$abusecontacts" ] && [ "$abusecontacts" != "-" ]; then
final_json_output+="\"abuse_contacts\":$abusecontacts,"
fi
final_json_output+="\"routing\":{"
if [ -n "$found_asname" ]; then
final_json_output+="\"is_announced\":true,"
final_json_output+="\"as_number\":\"$found_asn\","
final_json_output+="\"as_name\":\"${found_asname//\"/\\\"}\","
final_json_output+="\"as_rank\":\"${caida_asrank//\"/\\\"}\","
final_json_output+="\"route\":\"$found_route\","
final_json_output+="\"route_name\":\"$whois_routename\","
final_json_output+="\"roa_count\":\"$roacount_json_output\","
final_json_output+="\"roa_validity\":\"$roavalidity_json_output\""
else
final_json_output+="\"is_announced\":false,"
if [ "$found_route" != "N/A" ]; then
final_json_output+="\"net_name\":\"$found_route ($pwhois_net)\""
else
final_json_output+="\"net_name\":\"$pwhois_net\""
fi
fi
final_json_output+="},\"type\":{$ip_type_json_output}"
if [ "$IS_BOGON" != true ]; then
final_json_output+=",\"geolocation\":{"
final_json_output+="\"city\":\"$geo_city_json_output\","
final_json_output+="\"region\":\"$geo_region_json_output\","
final_json_output+="\"country\":\"$geo_country_json_output\","
final_json_output+="\"cc\":\"$geo_cc_json_output\""
final_json_output+="}"
fi
else
# Normal output
printf "${white}%${longest}s${default} ┌${bluebg}PTR${default} %s\n" "$1" "$hostname"
if [ "$IS_ASN_CHILD" = true ] && [ -n "$found_asname" ]; then
summary_asn="$found_asn"
ipinfolink="ipinfo.io"
summary_ipinfo=" ($ipinfolink🔗)"
else
summary_asn="$found_asn"
summary_ipinfo=""
fi
printf "${white}%${indent}s${bluebg}ASN${default} ${red}%s %s${default}\n" "├" "$summary_asn" "$output_asname"
[[ "$UNANNOUNCED_PREFIX" = false ]] && printf "${white}%${indent}s${bluebg}RNK${default} ${default}%s${default}\n" "├" "$caida_asrank_recap"
printf "${white}%${indent}s${bluebg}ORG${default} ${green}%s${default}\n" "├" "$pwhois_org"
if [ "$found_subprefix" != "$found_route" ]; then
# target IP belongs to a subnet announced within a larger route
printf "${white}%${indent}s${bluebg}NET${default} ${yellow}%s (%s)${default}\n" "├" "$found_subprefix" "$pwhois_net"
[[ -n "$whois_routename" ]] && whois_routename=" ($whois_routename)"
printf "${white}%${indent}s${bluebg}ROU${default} ${yellow}%s%s%s${default}\n" "├" "$found_route" "$whois_routename" "$summary_ipinfo"
else
# target IP belongs to a subnet announced directly
printf "${white}%${indent}s${bluebg}NET${default} ${yellow}%s (%s)%s${default}\n" "├" "$found_subprefix" "$pwhois_net" "$summary_ipinfo"
fi
printf "${white}%${indent}s${bluebg}ABU${default} ${blue}%s${default}\n" "├" "$abusecontacts"
printf "${white}%${indent}s${bluebg}ROA${default} %s\n" "├" "$rpki_output"
[[ -n "$ip_type_data" ]] && printf "${white}%${indent}s${bluebg}TYP${default}%s\n" "├" "${ip_type_data}"
printf "${white}%${indent}s${bluebg}GEO${default} ${magenta}%s${default}" "├" "$pwhois_geo"
if [ "$IS_ASN_CHILD" = true ] && [ -n "$flag_icon_cc" ]; then
# signal to the parent connhandler the correct country flag to display for this IP
echo -n " #COUNTRYCODE $flag_icon_cc"
fi
printf "\n"
fi
}
IsBogon(){
bogon_tag=""
IS_BOGON=false
# Bogon regex patterns
# IPv4
localhostregex='(^127\.)' # RFC 1122 localhost
thisnetregex='(^0\.)' # RFC 1122 'this' network
privateregex='(^192\.168\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)' # RFC 1918 private space - cheers https://stackoverflow.com/a/11327345/5377165
cgnregex='(^100\.6[4-9]\.)|(^100\.[7-9][0-9]\.)|(^100\.1[0-1][0-9]\.)|(^100\.12[0-7]\.)' # RFC 6598 Carrier grade nat space
llregex='(^169\.254\.)' # RFC 3927 link local
ietfprotoregex='(^192\.0\.0\.)' # IETF protocol assignments
testnetregex='(^192\.0\.2\.)|(^198\.51\.100\.)|(^203\.0\.113\.)' # RFC 5737 TEST-NET
benchmarkregex='(^192\.1[8-9]\.)' # RFC 2544 Network interconnect device benchmark testing
sixtofouranycast='(^192\.88\.99\.)' # RFC 7526 6to4 anycast relay
multicastregex='(^22[4-9]\.)|(^23[0-9]\.)' # Multicast
reservedregex='(^24[0-9]\.)|(^25[0-5]\.)' # Reserved for future use/limited broadcast (255.255.255.255)
# IPv6
unspecifiedv6regex='(^::$)' # RFC 4291 Unspecified Address (::)
loopbackv6regex='(^::1$)' # RFC 4291 Loopback Address (::1)
ipv4mappedv6regex="^(:{1,2}|0:0:0:0:0:)ffff:((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$" # RFC 4291 IPv4-mapped IPv6 addresses (::ffff:0:0/96)
rtbhv6regex='(^100::)' # RFC 6666 Remotely triggered black hole addresses (100::/64)
orchidv6regex='(^2001:1[0-9a-f]:)' # RFC 4843 Overlay routable cryptographic hash identifiers (ORCHID) (2001:10::/28)
docv6regex='(^2001:db8:)' # RFC 3849 Documentation (2001:db8::/32)
docexpandedv6regex='(^3fff:)' # RFC 9637 Expanded IPv6 Documentation Space (3fff::/20)
ulav6regex='(^[f][cd])' # RFC 4193 Unique Local Address (fc00::/7)
llv6regex='(^fe8[0-9a-f]:)|(^fe9[0-9a-f]:)|(^fea[0-9a-f]:)|(^feb[0-9a-f]:)' # RFC 4291 Link-Local Addresses (fe80::/10)
multicastv6regex='(^ff0[0-9a-df])|(^ff[1-9a-f])|(^ff[bcdef])' # RFC 4291 Multicast Addresses (ff00::/8, except for ff0e::/16 which is defined in RFC7346 as IPv6 Multicast Global Scope and therefore should be routable)
if grep -q ':' <<<"$1"; then
# perform IPv6 bogon checks
lowercaseipv6=$(tr '[:upper:]' '[:lower:]' <<<"$1")
if [[ "$lowercaseipv6" =~ $unspecifiedv6regex ]]; then
bogon_tag="rfc4291 (IPv6 Unspecified Address)"
MTR_TRACING=false
elif [[ "$lowercaseipv6" =~ $loopbackv6regex ]]; then
bogon_tag="rfc4291 (IPv6 Loopback Address)"
MTR_TRACING=false
elif [[ "$lowercaseipv6" =~ $ipv4mappedv6regex ]]; then
bogon_tag="rfc4291 (IPv4-mapped IPv6 addresses)"
elif [[ "$lowercaseipv6" =~ $rtbhv6regex ]]; then
bogon_tag="rfc6666 (Remotely triggered black hole IPv6 addresses)"
elif [[ "$lowercaseipv6" =~ $orchidv6regex ]]; then
bogon_tag="rfc4843 (ORCHID - Overlay routable cryptographic hash identifiers)"
elif [[ "$lowercaseipv6" =~ $docv6regex ]]; then
bogon_tag="rfc3849 (IPv6 Documentation Space)"
elif [[ "$lowercaseipv6" =~ $docexpandedv6regex ]]; then
bogon_tag="rfc9637 (Expanded IPv6 Documentation Space)"
elif [[ "$lowercaseipv6" =~ $ulav6regex ]]; then
bogon_tag="rfc4193 (IPv6 Unique Local Address)"
elif [[ "$lowercaseipv6" =~ $llv6regex ]]; then
bogon_tag="rfc4291 (IPv6 Link-Local Addresses)"
elif [[ "$lowercaseipv6" =~ $multicastv6regex ]]; then
bogon_tag="rfc4291 (IPv6 Multicast Addresses)"
MTR_TRACING=false
fi
else
# perform IPv4 bogon checks
if [[ "$1" =~ $localhostregex ]]; then
bogon_tag="rfc1122 (Localhost)"
MTR_TRACING=false
elif [[ "$1" =~ $thisnetregex ]]; then
bogon_tag="rfc1122 ('this' network)"
MTR_TRACING=false
elif [[ "$1" =~ $privateregex ]]; then
bogon_tag="rfc1918 (Private Space)"
elif [[ "$1" =~ $cgnregex ]]; then
bogon_tag="rfc6598 (CGN Space)"
elif [[ "$1" =~ $llregex ]]; then
bogon_tag="rfc3927 (Link-Local)"
elif [[ "$1" =~ $ietfprotoregex ]]; then
bogon_tag="(Reserved for IETF protocol assignments)"
elif [[ "$1" =~ $testnetregex ]]; then
bogon_tag="rfc5737 (Reserved for Test Networks)"
elif [[ "$1" =~ $benchmarkregex ]]; then
bogon_tag="rfc2544 (Reserved for Network device benchmark testing)"
elif [[ "$1" =~ $sixtofouranycast ]]; then
bogon_tag="rfc7526 (6to4 anycast relay)"
elif [[ "$1" =~ $multicastregex ]]; then
bogon_tag="(Multicast Address)"
MTR_TRACING=false
elif [[ "$1" =~ $reservedregex ]]; then
bogon_tag="(Reserved Address)"
MTR_TRACING=false
fi
fi
if [ -n "$bogon_tag" ]; then
IS_BOGON=true
json_bogon_type="$bogon_tag"
bogon_tag="${yellowbg} BOGON ${default} ${bogon_tag}"
fi
}
LookupASNAndRouteFromIP(){
found_asn=""
found_route=""
found_asname=""
IsBogon "$1"
if [ "$IS_BOGON" = false ]; then
if echo "$1" | grep -q ':'; then
# whois query for IPv6 addresses
output=$(whois -h whois.cymru.com " -f -p -u $1" | sed 's/\ *|\ */|/g')
found_asn=$(echo "$output" | awk -F'[|]' 'NR==1{print $1}')
if [ "$found_asn" = "NA" ]; then
# Team Cymru has no data for this IPv6. Inform WhoisIP() that we will have to fall back to a generic whois lookup.
found_asn=""
UNANNOUNCED_PREFIX=true
else
found_asname=$(echo "$output" | awk -F'[|]' 'NR==1{print $4}')
found_route=$(echo "$output" | awk -F'[|]' 'NR==1{print $3}')
UNANNOUNCED_PREFIX=false
# lookup CAIDA rank info for the origin AS
GetCAIDARank "$found_asn"
fi
else
# Query RIPEStat for IPv4 addresses
output=$(docurl -m5 -s "https://stat.ripe.net/data/prefix-overview/data.json?resource=$1&sourceapp=nitefood-asn")
if jq -r '.data.announced' <<<"$output" | grep -q "true"; then
found_asn=$(jq -r '.data.asns[0].asn' <<<"$output")
found_asname=$(jq -r '.data.asns[0].holder' <<<"$output")
# look up the country this ASN is located in
country=$(docurl -m5 -s "https://stat.ripe.net/data/rir-stats-country/data.json?resource=AS${found_asn}" | jq -r '.data.located_resources[0].location')
[[ "$country" != "null" ]] && found_asname="${found_asname}, ${country}"
found_route=$(jq -r '.data.resource' <<<"$output")
UNANNOUNCED_PREFIX=false
else
# RIPEStat has no data for this IPv4. Fallback to Team Cymru DNS query (faster than whois)
rev=$(echo "$1" | cut -d '/' -f 1 | awk -F'.' '{printf $4 "." $3 "." $2 "." $1}')
output=$(host -t TXT "$rev.origin.asn.cymru.com" | awk -F'"' 'NR==1{print $2}' | sed 's/\ *|\ */|/g')
found_asn=$(echo "$output" | awk -F'[|]' 'NR==1{print $1}' | cut -d ' ' -f 1) # final cut gets first origin AS only if cymru has multiple
if [ -n "$found_asn" ]; then
found_asname=$(host -t TXT "AS$found_asn.asn.cymru.com" | grep -v "NXDOMAIN" | awk -F'|' 'NR==1{print substr($NF,2,length($NF)-2)}')
found_route=$(echo "$output" | awk -F'[|]' 'NR==1{print $2}')
UNANNOUNCED_PREFIX=false
else
# Team Cymru has no data for this IPv4 either. Inform WhoisIP() that we will have to fall back to a generic whois lookup.
UNANNOUNCED_PREFIX=true
fi
fi
fi
else
# bogon address, consider it unannounced
UNANNOUNCED_PREFIX=true
fi
}
ResolveHostnameToIPList(){
raw_host_output=$(host "$1" 2>/dev/null)
if echo -e "$raw_host_output" | grep -q "mail is handled"; then
host_output=$(echo "$raw_host_output" | grep -B100 -A0 -m1 "mail is handled" | sed '$d')
else
host_output="$raw_host_output"
fi
ip=$(echo "$host_output" | grep -Eo "$ipv4v6regex")
echo -e "$ip\n"
}
PrintErrorAndExit(){
if [ "$JSON_OUTPUT" = true ]; then
# json output
status_json_output="fail"
reason_json_output="${1//\"/\\\"}"
json_resultcount=0
PrintJsonOutput
elif [ "$IS_ASN_CHILD" = true ]; then
echo -e "\n${redbg}${1}${default}\n" # get the error in the html report
tput sgr0
else
# normal output
echo -e "\n${redbg}${1}${default}" >&2
tput sgr0
fi
tput cnorm # show cursor
exit 1
}
PrintUsage(){
# if an argument is passed, it will be displayed on stderr and the script will exit with error
script_name=$(basename "$0")
JSON_OUTPUT=false
BoxHeader "ASN / RPKI validity / BGP stats / IPv4v6 / Prefix / ASPath / Organization / IP reputation lookup tool" >&2
echo -e "\nVERSION:\n\n ${ASN_VERSION}" \
"\n\nUSAGE:\n\n $script_name [${green}OPTIONS${default}] [${blue}TARGET${default}]" \
"\n $script_name [${red}-v${default}] ${red}-l${default} [${red}SERVER OPTIONS${default}]" \
"\n\nOPTIONS:" \
"\n\n ${green}-t (enable trace)\n\t${default}Enable AS path trace to the ${blue}TARGET${default} (this is the default behavior)" \
"\n\n ${green}-n (no trace|no additional INETNUM lookups)\n\t${default}Disable tracing the AS path to the ${blue}TARGET${default} (for IP targets) or" \
"\n\tDisable additional (unannounced / announced by other AS) INETNUM lookups for the ${blue}TARGET${default} (for AS targets)" \
"\n\n ${green}-d (detailed)\n\t${default}Output detailed hop info during the AS path trace to the ${blue}TARGET${default}" \
"\n\tThis option also enables RPKI validation/BGP hijacking detection for every hop" \
"\n\n ${green}-a (ASN Suggest)\n\t${default}Lookup AS names and numbers matching ${blue}TARGET${default}" \
"\n\n ${green}-u (Transit/Upstream lookup)\n\t${default}Inspect BGP updates and ASPATHs for the ${blue}TARGET${default} address/prefix and identify possible transit/upstream autonomous systems" \
"\n\n ${green}-c (Country CIDR)\n\t${default}Lookup all IPv4/v6 CIDR blocks allocated to the ${blue}TARGET${default} country" \
"\n\n ${green}-g (Bulk Geolocate)\n\t${default}Geolocate all IPv4/v6 addresses passed as ${blue}TARGET${default}" \
"\n\tThis mode supports multiple targets, stdin input and IP extraction from input, e.g." \
"\n\t'asn -g < /var/log/apache2/error.log' or 'echo 1.1.1.1 2.2.2.2 | asn -g'" \
"\n\n ${green}-s (Shodan scan)\n\t${default}Query Shodan's InternetDB for CVE/CPE/Tags/Ports/Hostnames data about ${blue}TARGET${default}" \
"\n\tThis mode supports multiple targets and stdin input, e.g." \
"\n\t'asn -s < iplist' or 'echo 1.1.1.0/24 google.com | asn -s'" \
"\n\n ${green}-o (organization search)\n\t${default}Force ${blue}TARGET${default} to be treated as an Organization Name" \
"\n\n ${green}-m (monochrome output)\n\t${default}Disable colored output" \
"\n\n ${green}-v (verbose)\n\t${default}Enable (and log to \$HOME/asndebug.log) debug messages (URLs being queried and variable names being assigned)." \
"\n\tAPI call response data (i.e. the JSON output) is logged to the logfile.${default}" \
"\n\n ${green}-j (compact JSON output)\n\t${default}Set output to compact JSON mode (ideal for machine parsing)" \
"\n\n ${green}-J (pretty-printed JSON output)\n\t${default}Set output to pretty-printed JSON mode" \
"\n\n ${green}-h (help)\n\t${default}Show this help screen" \
"\n\n ${red}-l (lookup server)\n\t${default}Launch the script in server mode. See ${red}SERVER OPTIONS${default} below" \
"\n\nTARGET:" \
"\n\n ${blue}${default}\n\tLookup matching ASN and BGP announcements/neighbours data." \
"\n\t(Supports \"as123\" and \"123\" formats - case insensitive)" \
"\n\n ${blue}${default}\n\tLookup matching route(4/6), IP reputation and ASN data" \
"\n\n ${blue}${default}\n\tLookup matching ASN data" \
"\n\n ${blue}${default}\n\tLookup matching IP, route and ASN data. Supports multiple IPs - e.g. DNS RR" \
"\n\n ${blue}${default}\n\tExtract hostname/IP from the URL and lookup relative data. Supports any protocol prefix, non-standard ports and prepended credentials" \
"\n\n ${blue}${default}\n\tSearch by company name and lookup network ranges exported by (or related to) the company" \
"\n\nSERVER OPTIONS:" \
"\n\n ${red}BIND_ADDRESS${default}\n\tIP address (v4/v6) to bind the listening server to (e.g. '$script_name -l 0.0.0.0')\n\tDefault value: ${red}${DEFAULT_SERVER_BINDADDR_v4} (IPv4) or ${DEFAULT_SERVER_BINDADDR_v6} (IPv6)${default}" \
"\n\n ${red}BIND_PORT${default}\n\tTCP Port to bind the listening server to (e.g. '$script_name -l 12345')\n\tDefault value: ${red}${DEFAULT_SERVER_BINDPORT}${default}" \
"\n\n ${red}BIND_ADDRESS${default} ${red}BIND_PORT${default}\n\tIP address and port to bind the listening server to (e.g. '$script_name -l ::1 12345')" \
"\n\n ${red}-v (verbose)\n\t${default}Enable verbose output and debug messages in server mode${default}" \
"\n\n ${red}--allow host[,host,...]\n\t${default}Allow only given hosts to connect to the server${default}" \
"\n\n ${red}--allowfile file\n\t${default}A file of hosts allowed to connect to the server${default}" \
"\n\n ${red}--deny host[,host,...]\n\t${default}Deny given hosts from connecting to the server${default}" \
"\n\n ${red}--denyfile file\n\t${default}A file of hosts denied from connecting to the server${default}" \
"\n\n ${red}-m, --max-conns \n\t${default}The maximum number of simultaneous connections accepted by the server. 100 is the default.${default}" \
"\n\n\n Note: Every option in server mode (after -l) is passed directly to the ncat listener." \
"\n Refer to ${blue}man ncat${default} for more details on the available commands." \
"\n Unless specified, the default IP:PORT values of ${DEFAULT_SERVER_BINDADDR_v4}:${DEFAULT_SERVER_BINDPORT} (for IPv4) or [${DEFAULT_SERVER_BINDADDR_v6}]:${DEFAULT_SERVER_BINDPORT} (for IPv6) will be used (e.g. 'asn -l')" \
"\n\n Example server usage:" \
"\n\t${blue}asn -l${default}" \
"\n\t (starts server on default IP(v4/v6):PORT)\n" \
"\n\t${blue}asn -l 0.0.0.0 --allow 192.168.0.0/24,192.168.1.0/24,192.168.2.245${default}" \
"\n\t (binds to all availables IPv4 interfaces on the default port, allowing only connections from the three specified subnets)\n" \
"\n\t${blue}asn -l :: 2222 --allow 2001:DB8::/32${default}" \
"\n\t (binds to all availables IPv6 interfaces on port 2222, allowing only connections from the specified prefix)\n" \
"\n\t${blue}asn -v -l 0.0.0.0 --allowfile \"~/goodips.txt\" -m 5${default}" \
"\n\t (verbose mode, bind to all IPv4 interfaces, use an allowfile with allowed addresses, accept a maximum of 5 concurrent connections)\n" \
"\n Bookmarklet configuration page:" \
"\n\tplease visit ${blue}http://127.0.0.1:49200/asn_bookmarklet${default} and follow the instructions. More documentation is available on github (link below)." \
"\n\n\nProject homepage: ${yellow}https://github.com/nitefood/asn${default}\n" >&2
[[ -n "$1" ]] && PrintErrorAndExit "$1"
}
PwhoisLookup(){
StatusbarMessage "Collecting pWhois data"
pwhois_output=$(whois -h whois.pwhois.org "$1")
StatusbarMessage
if echo "$pwhois_output" | grep -vq "That IP address doesn't appear"; then
# pwhois_asn=$(echo "$pwhois_output" | grep -E "^Origin-AS" | cut -d ':' -f 2 | sed 's/^ //')
# pwhois_prefix=$(echo "$pwhois_output" | grep -E "^Prefix" | cut -d ':' -f 2 | sed 's/^ //')
pwhois_asorg=$(echo "$pwhois_output" | grep -E "^AS-Org-Name" | cut -d ':' -f 2 | sed 's/^ //')
# group all "Org-Name" fields on a single line
pwhois_org=$(echo "$pwhois_output" | grep -E "^Org-Name" | cut -d ':' -f 2 | sed 's/^[ \t]*//g' | while read -r line; do echo -n "$line / "; done | sed 's/ \/ $//')
pwhois_net=$(echo "$pwhois_output" | grep -E "^Net-Name" | cut -d ':' -f 2 | sed 's/^ //')
# if pWhois' Net-Name=Org-Name, then it's more useful to use AS-Org-Name instead of Org-Name (unless AS-Org-Name is empty)
if [ -n "$pwhois_asorg" ] && [ "$pwhois_net" = "$pwhois_org" ]; then
pwhois_org="$pwhois_asorg"
fi
IPGeoRepLookup "$1"
IPShodanLookup "$1"
pwhois_geo="$ip_geo_data"
if [ -z "$ip_geo_data" ]; then
if echo "$pwhois_output" | grep -q -E "^Geo-"; then
# use "Geo-" fields in pWhois output
cityfield="Geo-City"
regionfield="Geo-Region"
ccfield="Geo-CC"
else
cityfield="City"
regionfield="Region"
ccfield="Country-Code"
fi
pwhois_city=$(echo "$pwhois_output" | grep -m1 -E "^${cityfield}" | cut -d ':' -f 2 | sed 's/^ //')
pwhois_region=$(echo "$pwhois_output" | grep -m1 -E "^${regionfield}" | cut -d ':' -f 2 | sed 's/^ //')
pwhois_cc=$(echo "$pwhois_output" | grep -m1 -E "^${ccfield}" | cut -d ':' -f 2 | sed 's/^ //')
flag_icon_cc=$(tr '[:upper:]' '[:lower:]' <<<"$pwhois_cc")
if [ "$pwhois_city" = "NULL" ] || [ "$pwhois_region" = "NULL" ]; then
pwhois_geo="$pwhois_cc"
else
pwhois_geo="$pwhois_city, $pwhois_region ($pwhois_cc)"
fi
fi
else
pwhois_output="";
fi
}
RdnsLookup(){
# reverse DNS (PTR) lookup.
# get first lookup result only (in case of multiple PTR records) and remove trailing dot and CR (Cygwin) from hostname
rdns=$(host "$1" | awk 'NR==1{sub(/\.\r?$/, "", $NF); print $NF}')
if echo "$rdns" | grep -E -q "NXDOMAIN|SERVFAIL|REFUSED|^record$"; then rdns=""; fi
echo "$rdns"
}
AbuseLookupForPrefix(){
# $1="whois data", $2=prefix
if [ -n "$mtr_output" ] && [ "$DETAILED_TRACE" = false ]; then
# skip abuse lookup for individual trace hops in non-detailed mode
return
fi
whoisdata="$1"
prefix="$2"
abuselist=""
for abusecontact in $(echo -e "$whoisdata" | grep -E "^OrgAbuseEmail:|^abuse-c:|^% Abuse|^abuse-mailbox:" | awk '{print $NF}' | tr -d \'); do
if ! grep -q '@' <<<"$abusecontact"; then
# the currently parsed abuse contact, found in whois data, is not an email (but likely a NIC handle), fall back to RIPE API
resolvedabuse=$(docurl -m5 -s "https://stat.ripe.net/data/abuse-contact-finder/data.json?resource=$2&sourceapp=nitefood-asn" | jq -r 'select (.data.abuse_contacts != null) | .data.abuse_contacts[]')
if ! grep -q '@' <<<"$resolvedabuse"; then
# RIPE API didn't give back an email, second and last fall back to DSHIELD API
dshield_abuse_contact=$(docurl -m15 -s --user-agent "nitefood/asn" https://isc.sans.edu/api/ip/$2?json | jq -r 'select (.ip.asabusecontact != null) | .ip.asabusecontact')
if grep -q '@' <<<"$dshield_abuse_contact"; then
resolvedabuse="$dshield_abuse_contact"
fi
fi
[[ -n "$resolvedabuse" ]] && abusecontact="$resolvedabuse"
fi
[[ -n "$abuselist" ]] && abuselist+="\n"
abuselist+="$abusecontact"
done
if [ "$JSON_OUTPUT" = true ]; then
# json output
if [ -n "$abuselist" ]; then
echo -e "$abuselist" | jq -cM --slurp --raw-input 'split("\n") | map(select(length > 0)) | unique'
fi
else
# normal output
if [ -n "$abuselist" ]; then
# use jq to join contacts together and separate them with the " / " multi-character delimiter, while getting rid of spurious newlines
echo -e "$abuselist" | jq -r --slurp --raw-input 'split("\n") | map(select(length > 0)) | unique | join(" / ")'
else
echo "-"
fi
fi
}
HopPrint(){
StatusbarMessage
# shellcheck disable=SC2124
output="$@"
# create hyperlinks if running in server mode
if [ "$IS_ASN_CHILD" = true ]; then
[[ -z "$ixp_tag" ]] && htmlcolor="$htmlwhite" || htmlcolor="$htmlblue"
# extract and trim duplicate IPs from the trace output line
found_ips=$(grep -Eo "${ipv4v6regex}" <<<"$output" | sort -u)
if [ -n "$found_ips" ]; then
for ip in $found_ips; do
# replace only the last IP in the trace output, so to handle special cases
# where the trace hop PTR contains the full dotted IP, ex:
# 1.2.3.4.domain.name (1.2.3.4) -> 1.2.3.4.domain.name ()
output=$(sed "s/\(.*\)$ip/\1$ip<\/a>/" <<<"$output")
done
fi
fi
echo -e "$output"
StatusbarMessage "Analyzing collected trace output to ${bluebg}${host_to_trace}${lightgreybg}"
}
TraceASPath(){
starttime=$(date +%s)
host_to_trace="$1"
border_color="$lightblue"
# attepmt to be as responsive as possible
# ideal output width : 150 (size of headers in normal traces)
# ideal border width : 100 (used in detailed traces only)
if [ "$terminal_width" -ge 153 ]; then
output_width=150
border_width=100
elif [ "$terminal_width" -ge 103 ]; then
output_width=$((terminal_width-3))
border_width=100
else
output_width=$((terminal_width-3))
border_width=$((terminal_width-3))
fi
WhatIsMyIP
if [ "$DETAILED_TRACE" = true ]; then
headermsg="Detailed trace to $userinput"
else
headermsg="Trace to $userinput"
fi
BoxHeader "$headermsg"
# check if we're trying to trace an IPv6 from an IPv4-only box
echo "$host_to_trace" | grep -q ':' && is_ipv6=true || is_ipv6=false
if [ "$is_ipv6" = true ] && [ "$HAVE_IPV6" = false ]; then
PrintErrorAndExit "Error: cannot trace an IPv6 from this IPv4-only host!"
fi
echo ""
StatusbarMessage "Collecting trace data to ${bluebg}${host_to_trace}${lightgreybg}"
# start the mtr trace
DebugPrint "${yellow}mtr → $host_to_trace ($MTR_ROUNDS rounds)${default}"
mtr_output=$(mtr -C -n -c"$MTR_ROUNDS" "$host_to_trace" | tail -n +2)
declare -a tracehops_array
declare -a aspath_array
# initialize the aspath array with our source AS
if [ "$HAVE_IPV6" = true ]; then
found_asn=$(docurl -s "https://stat.ripe.net/data/whois/data.json?resource=$local_wanip&sourceapp=nitefood-asn" | jq -r '.data.irr_records[0] | map(select(.key | match ("origin"))) | .[].value')
WhoisASN "$found_asn"
else
LookupASNAndRouteFromIP "$local_wanip"
fi
if [ -z "$found_asn" ]; then
found_asn="XXX"
found_asname="(Unknown)"
fi
if [ "$IS_ASN_CHILD" = true ]; then
aspatharray_asnvalue="$found_asn"
else
aspatharray_asnvalue="$found_asn"
fi
# Retrieve local AS CAIDA rank
GetCAIDARank "$found_asn"
if [ -n "$caida_asrank_text" ]; then
aspath_entry=$(printf "${red}%-6s ${green}%s${default}${dim} •%s${dim}${default}" "$aspatharray_asnvalue" "$(echo "$found_asname" | cut -d ',' -f 1)" "$caida_asrank_text")
else
aspath_entry=$(printf "${red}%-6s ${green}%s${default}" "$aspatharray_asnvalue" "$(echo "$found_asname" | cut -d ',' -f 1)")
fi
aspath_array+=("$aspath_entry")
# mtr finished, analyze and output results
# print trace headers (only non-detailed trace)
if [ "$DETAILED_TRACE" = false ]; then
HopPrint "$(printf "${lightgreybg}%4s %-$((output_width-61))s %7s %13s %s ${default}" "Hop" "IP Address" "Loss%" "Ping avg" "AS Information")"
fi
LAST_HOP=false
ROUTING_LOOP=false
# parse mtr output (csv)
hop_num=0
while true; do
((hop_num++))
IFS=',' read -ra cur_hop_data <<< "$(echo "$mtr_output" | head -n "$hop_num" | tail -n 1)"
mtr_hopnum=${cur_hop_data[4]}
[[ "$hop_num" -gt "$mtr_hopnum" ]] && break # we're past the last trace hop, quit the loop
hop_ip=${cur_hop_data[5]}
hop_loss=${cur_hop_data[6]%.*}
hop_ping=$(echo "${cur_hop_data[10]}" | awk '{ printf ("%.1f\n", $1) }')
if [ "$hop_loss" -ne 0 ]; then
# color packet loss yellow if between 1% and 50%, or red if > 50%
[[ "$hop_loss" -le 50 ]] && loss_color="$lightyellow" || loss_color="$lightred"
if [ "$DETAILED_TRACE" = false ]; then
hop_loss=$(printf "${loss_color}%5s" "$hop_loss") # packet loss position in normal path trace (spacing to fit the column alignment)
else
hop_loss="${loss_color}${hop_loss}" # packet loss position in detailed path trace (no spacing)
fi
fi
tracehops_array["$hop_num"]="$hop_ip"
ixp_tag=""
hop_asn=""
if [ "$hop_ip" = "???" ]; then
# print no reply hop info
if [ "$DETAILED_TRACE" = true ]; then
trailing_line=$(printf '%.0s═' $(seq $((border_width-9-${#hop_ip}))) )
hop_output="$(printf "${border_color}╔═[${default}%3s. %s ${border_color}]%s╗${default}\n" "$hop_num" "$hop_ip" "$trailing_line")"
hop_output+="\n ├${bluebg}RTT${default} ${white}* (No reply)${default}\n"
hop_output+=" └${bluebg}LOS${default} ${hop_loss}%${default} packet loss\n"
else
hop_output="$(printf "%3s. %-$((output_width-60))s %6s${default} %13s %s" "$hop_num" "$hop_ip" "$hop_loss%" "*" "${white}(No reply)${default}")"
fi
HopPrint "${hop_output}"
continue # jump to the next hop
fi
# do a reverse DNS lookup for the IP
hostname=$(RdnsLookup "$hop_ip")
# check for routing loops, if so fail immediately
if [ "$hop_num" -ge 2 ]; then
prev_hop=${tracehops_array[$(( hop_num-1 ))]}
# Check disabled to avoid detecting routing loops in rare cases where the same hop appears twice in a row during the trace
# if [ "$hop_ip" = "$prev_hop" ]; then
# ROUTING_LOOP=true
# break
# fi
fi
if [ "$hop_num" -ge 4 ]; then
# detect routing loops
two_hops_ago=${tracehops_array[$(( hop_num-2 ))]}
three_hops_ago=${tracehops_array[$(( hop_num-3 ))]}
if [ "$hop_ip" = "$two_hops_ago" ] && [ "$prev_hop" = "$three_hops_ago" ]; then
ROUTING_LOOP=true
break
fi
fi
# AS DATA lookup
# check if it's the last hop
[[ "$hop_ip" = "$host_to_trace" ]] && LAST_HOP=true
LookupASNAndRouteFromIP "$hop_ip"
# check if IP is a bogon
if [ "$IS_BOGON" = true ]; then
asn_data="$bogon_tag"
pwhois_output=""
else
# Hop IP is not a bogon address. Proceed with lookup of hop data
if [ "$UNANNOUNCED_PREFIX" = true ]; then
# No data in the Team Cymru DB. Check to see if the ip is assigned to an IXP, or fall back to generic whois.
WhoisIP "$hop_ip" >/dev/null # don't display this, we're being parsed off-screen
hop_asn="${red}N/A (address not announced)${default}"
hop_org="$pwhois_org"
hop_net="$pwhois_net"
[[ -n "$found_route" ]] && hop_net+=" ($found_route)"
hop_typ="$ip_type_data"
hop_cpe="$ip_shodan_cpe_data"
hop_ports="$ip_shodan_ports_data"
hop_tags="$ip_shodan_tags_data"
hop_cve="$ip_shodan_cve_data"
hop_geo="$pwhois_geo"
hop_rep="$ip_rep_data"
if [ -n "$ixp_data" ]; then
# this hop is an IXP
ixp_tag="${bluebg} IXP ${default}"
asn_data="${ixp_tag} ${blue}${ixp_data}${default}"
hop_org="${ixp_tag} ${blue}${ixp_data}${default}"
aspath_entry=$(printf "%-6s ${blue}%s${default}" "$ixp_tag" "$ixp_data")
aspath_array+=("$aspath_entry")
else
# no data found and not an IXP hop, try retrieving relevant info from a generic whois lookup
hop_whois_data=$(echo -e "$full_whois_data" | grep -i -m2 -E "^netname:|^orgname:|^org-name:|^descr:" | cut -d ':' -f 2 | sed 's/^[ \t]*//' | while read -r line; do echo -n "$line / "; done | sed 's/ \/ $//')
if [ -z "$hop_whois_data" ]; then
asn_data="${yellow}(No data)${default}"
else
asn_data="${yellow}(${hop_whois_data})${default}"
fi
fi
else
# $hop_ip belongs to an announced prefix
if [ "$IS_ASN_CHILD" = true ]; then
asn_data="${red}[AS$found_asn] ${green}$found_asname${default}"
aspatharray_asnvalue="$found_asn"
else
asn_data="${red}[AS$found_asn] ${green}$found_asname${default}"
aspatharray_asnvalue="$found_asn"
fi
# Retrieve AS CAIDA rank for this hop
GetCAIDARank "$found_asn"
if [ -n "$caida_asrank_text" ]; then
aspath_entry=$(printf "${red}%-6s ${green}%s${default}${dim} •%s${dim}${default}" "$aspatharray_asnvalue" "$(echo "$found_asname" | cut -d ',' -f 1)" "$caida_asrank_text")
else
aspath_entry=$(printf "${red}%-6s ${green}%s${default}" "$aspatharray_asnvalue" "$(echo "$found_asname" | cut -d ',' -f 1)")
fi
# avoid adding the same AS multiple times in a row in the summary path
if [[ "${aspath_array[-1]}" != "$aspath_entry" ]]; then
aspath_array+=("$aspath_entry")
fi
fi
if [ "$DETAILED_TRACE" = true ] && [ "$UNANNOUNCED_PREFIX" = false ]; then
hop_asn="$found_asn"
hop_prefix="$found_route"
# run a pWhois lookup if the hop is within an announced prefix
PwhoisLookup "$hop_ip"
# in the event where Cymru has data, but pWhois doesn't, run a WhoisIP to fetch generic whois info
# specifying the "generic_whois_lookup_only" parameter in order not to run pWhois/IXP lookups again
[[ -z "$pwhois_output" ]] && WhoisIP "$hop_ip" "generic_whois_lookup_only" >/dev/null # don't display this, we're being parsed off-screen
hop_org="$pwhois_org"
hop_net="$pwhois_net"
hop_typ="$ip_type_data"
hop_cpe="$ip_shodan_cpe_data"
hop_ports="$ip_shodan_ports_data"
hop_tags="$ip_shodan_tags_data"
hop_cve="$ip_shodan_cve_data"
hop_geo="$pwhois_geo"
hop_rep="$ip_rep_data"
fi
fi
# DNS data (only used if a hostname was resolved)
if [ -n "$hostname" ] && [ ! "$hostname" = "-" ]; then
hop_ip="$hostname ($hop_ip)"
fi
# IXP coloring
[[ -z "$ixp_tag" ]] && hop_ip="${white}${hop_ip}${default}" || hop_ip="${blue}${hop_ip}${default}"
# print trace hop info
if [ "$DETAILED_TRACE" = false ]; then
hop_output="$(printf "%3s. %-$((output_width-45))s %6s${default} %10s ms %s" "$hop_num" "${hop_ip}" "$hop_loss%" "${hop_ping}" "$asn_data")"
else
trailing_line=$(printf '%.0s═' $(seq $((border_width+6-${#hop_ip}))) )
hop_output="$(printf "${border_color}╔═[${default}%3s. %s ${border_color}]%s╗${default}\n" "$hop_num" "${hop_ip}" "$trailing_line")"
hop_output+="\n ├${bluebg}RTT${default} ${white}${hop_ping} ms${default}\n"
hop_output+=" ├${bluebg}LOS${default} ${hop_loss}%${default} packet loss${default}"
# only add ASN data when not an IXP, otherwise we'll have duplicate data when ORG gets printed later
if [ -z "$ixp_tag" ]; then
# if it's a bogon address it's going to be the last branch of the displayed tree
if [ "$IS_BOGON" = true ]; then
hop_output+="\n └${bluebg}TYP${default} $asn_data${default}"
else
hop_output+="\n ├${bluebg}ASN${default} $asn_data${default}"
fi
fi
fi
HopPrint "$hop_output"
if [ "$DETAILED_TRACE" = true ] && [ -n "$hop_asn" ]; then
if [ -n "$found_asname" ]; then
# only run RPKI lookups if the prefix is announced
RPKILookup "$found_asn" "$hop_prefix"
if [ "$INVALID_ROA" = true ]; then
# notify user of possible BGP hijack/route leak
aspath_array[-1]+=" ${redbg} ─> WARNING: POSSIBLE ROUTE LEAK / BGP HIJACK ${default}"
fi
else
rpki_output="${red}N/A (hop address not announced)${default}"
fi
# compose hop detail output
# 1. hop ASN, ORG, NET, ROA
hop_details=""
[[ "$DETAILED_TRACE" = false ]] && hop_details=" ├${bluebg}ASN${default} ${red}${hop_asn}${default}\n"
[[ "$ixp_tag" = "" ]] && hop_details+=" ├${bluebg}RNK${default} ${caida_asrank_recap}${default}\n"
hop_details+=" ├${bluebg}ORG${default} ${green}${hop_org}${default}\n"
hop_details+=" ├${bluebg}NET${default} ${yellow}${hop_net}${default}\n"
hop_details+=" ├${bluebg}ROA${default} ${rpki_output}\n"
# 2. hop TYP (optional, only if hop is a particular IP type (anycast/hosting/etc))
[[ -n "$hop_typ" ]] && hop_details+=" ├${bluebg}TYP${default}${hop_typ}${default}\n"
# 3. hop CPE, PORTS, TAGS, CVE
[[ -n "$hop_cpe" ]] && hop_details+=" ├${bluebg}CPE${default}${hop_cpe}${default}\n"
[[ -n "$hop_ports" ]] && hop_details+=" ├${bluebg}POR${default}${hop_ports}${default}\n"
[[ -n "$hop_tags" ]] && hop_details+=" ├${bluebg}TAG${default}${hop_tags}${default}\n"
[[ -n "$hop_cve" ]] && hop_details+=" ├${bluebg}CVE${default}${hop_cve}${default}\n"
# 4. hop GEO, REP
hop_details+=" ├${bluebg}GEO${default} ${magenta}${hop_geo}${default}\n"
hop_details+=" └${bluebg}REP${default} ${hop_rep}${default}"
[[ "$DETAILED_TRACE" = false ]] && hop_details+="\n"
# display hop details on screen
HopPrint "$hop_details"
[[ "$DETAILED_TRACE" = true ]] && HopPrint "\n"
elif [ "$DETAILED_TRACE" = true ]; then
# PWHOIS lookups ON, but no valid hop data (e.g. no-reply hop). Just add a newline
[[ "$DETAILED_TRACE" = true ]] && HopPrint "\n"
fi
[[ "$LAST_HOP" = true ]] && break
done
# mtr output parsing complete
if [ "$LAST_HOP" = false ]; then
# last hop wasn't our target IP. Add a missing last hop to the trace.
mtr_end_msg="${redbg} no route to host"
[[ "$ROUTING_LOOP" = true ]] && mtr_end_msg+=" (routing loop detected)"
mtr_end_msg+=" ${default}"
# print hop info with final error message
if [ "$DETAILED_TRACE" = false ]; then
HopPrint "$(printf "%3s. %-$((output_width-40))s ${lightred}%11s%%${default} %13s %s" "$hop_num" "$mtr_end_msg" "100" "*" "${white}(No reply)${default}")"
else
upper_border="${border_color}╔$(printf '%.0s═' $(seq "$border_width"))╗${default}"
lower_border="${border_color}╚$(printf '%.0s═' $(seq "$border_width"))╝${default}"
HopPrint "${upper_border}\n ${mtr_end_msg}\n${lower_border}"
fi
aspath_array+=("${mtr_end_msg}${default}")
fi
endtime=$(date +%s)
runtime=$((endtime-starttime))
StatusbarMessage
if [ "$IS_ASN_CHILD" = true ]; then
# date and time is already displayed elsewhere in server mode
echo -e "\nTrace completed in $runtime seconds.\n"
else
tracetime=$(date +'%F %T %Z')
echo -e "\nTrace completed in $runtime seconds on $tracetime\n"
fi
BoxHeader "AS path to $userinput"
echo -en "\n "
for as in "${aspath_array[@]}"; do
if [ "$as" = "${aspath_array[0]}" ]; then
echo -en "${as} ${yellowbg} Local AS ${default}"
else
echo -en "${as}${default}"
fi
if [ "$as" != "${aspath_array[-1]}" ]; then
echo -en "\n ╭╯\n ╰"
fi
done
echo -e "\n"
}
SearchByOrg(){
unset orgs
declare -a orgs
echo ""
if [ "$ORG_FILTER" = false ]; then
StatusbarMessage "Searching for organizations matching ${bluebg}$1${lightgreybg}"
full_org_search_data=$(whois -h whois.pwhois.org "registry org-name=$1")
original_organizations=$(echo -e "$full_org_search_data" | grep -E "^Org-Name:" | cut -d ':' -f 2- | sed 's/^ //g' | sort -uf)
total_orgsearch_results=$(echo -e "$original_organizations" | wc -l)
organizations="$original_organizations"
else
# user chose to apply a search filter to a previous query
if [ ${#orgfilters_array[@]} -eq 0 ] && [ ${#excl_orgfilters_array[@]} -eq 0 ]; then
# user deleted all search filters. Revert to original query result
organizations="$original_organizations"
ORG_FILTER=false
else
StatusbarMessage "Applying filters"
filtered_org="$original_organizations"
# parse all inclusion filters
for filter in "${orgfilters_array[@]}"; do
apply_filter=$(echo -e "$filtered_org" | grep -i -- "$filter")
if [ -z "$apply_filter" ]; then
StatusbarMessage
echo -en "${yellow}Warning: No results found for ${bluebg}${filter}${default}"
sleep 2
# remove last filter term
unset 'orgfilters_array[${#orgfilters_array[@]}-1]'
else
filtered_org="$apply_filter"
fi
done
# parse all exclusion filters
for filter in "${excl_orgfilters_array[@]}"; do
apply_filter=$(echo -e "$filtered_org" | grep -i -v -- "$filter")
if [ -z "$apply_filter" ]; then
StatusbarMessage
echo -en "${yellow}Warning: No more results found if excluding ${bluebg}${filter}${default}"
sleep 2
# remove last filter term
unset 'excl_orgfilters_array[${#excl_orgfilters_array[@]}-1]'
else
filtered_org="$apply_filter"
fi
done
# have we removed all filters (because of no matches)? go back to unfiltered results
if [ ${#orgfilters_array[@]} -eq 0 ] && [ ${#excl_orgfilters_array[@]} -eq 0 ]; then
ORG_FILTER=false
echo ""
fi
organizations="$filtered_org"
fi
fi
for orgname in $organizations; do
orgs+=("$orgname")
done
StatusbarMessage
if [ ${#orgs[@]} -eq 0 ]; then
# company search yielded no results
PrintErrorAndExit "Error: no organizations found"
fi
# Menu showing loop
while true; do
ShowMenu
searchresults=""
[[ "$LOOKUP_ALL_RESULTS" == true ]] && orgs_to_lookup=("${orgs[@]}") || orgs_to_lookup=("$org")
for org in "${orgs_to_lookup[@]}"; do
orgids=$(echo -e "$full_org_search_data" | grep -i -E -B1 "Org-Name: $org$" | grep "Org-ID" | cut -d ':' -f 2- | sed 's/^ //g')
NO_ERROR_ON_INTERRUPT=true
for ipversion in 4 6; do
NO_RESULTS=true
searchresults+=$(BoxHeader "IPv${ipversion} networks for organization \"${org}\"")
# iterate over Org-IDs related to the company (in case of multiple Org-IDs for a single Org-Name)
for orgid in $orgids; do
StatusbarMessage "Looking up IPv${ipversion} networks for organization ${bluebg}$org${lightgreybg} (Org-ID: ${bluebg}${orgid}${lightgreybg})"
netblocks_output=""
if [ "$ipversion" = "4" ]; then
# Parse IPv4 NETBLOCKS
netblocks=$(whois -h whois.pwhois.org "netblock org-id=${orgid}" | grep -E "^\*>")
netblocks_header=" IPv4 NET RANGE | INFO"
for netblock in $netblocks; do
prefix=$(echo -e "$netblock" | cut -d '>' -f 2 | cut -d '|' -f 1)
netname=$(echo -e "$netblock" | cut -d '>' -f 2 | cut -d '|' -f 2 | tr -d ' ')
netblock_type=$(echo -e "$netblock" | cut -d '>' -f 2 | cut -d '|' -f 3 | tr -d ' ')
if [ "$netblock_type" = "unknown" ]; then
nettype=""
else
nettype=" (${yellow}$netblock_type${default})"
fi
regdate=$(echo -e "$netblock" | cut -d '>' -f 2 | cut -d '|' -f 4 | tr -d ' ')
if [ "$HAVE_IPCALC" = true ]; then
# deaggregate IPv4 netblocks into CIDR prefixes for readability
prefix_spacing=19
trimmed_prefix=$(echo "$prefix" | tr -d ' ')
prefix=$(IpcalcDeaggregate "$trimmed_prefix")
else
# no ipcalc, use direct pWhois output
prefix_spacing=41
fi
netblocks_output+=$(printf "\n${blue}%${prefix_spacing}s${default} | ${green}%-45s${default} - Registered: ${magenta}%s${default}%s" "$prefix" "$netname" "$regdate" "$nettype")
done
[[ "$HAVE_IPCALC" = true ]] && netblocks_header=" IPv4 PREFIX | INFO"
else
# Parse IPv6 NETBLOCKS
netblocks=$(whois -h whois.pwhois.org "netblock6 org-id=${orgid}" | grep -E "^Net-(Range|Name|Handle|Type)|^Register-Date" |\
cut -d ':' -f 2- |\
sed 's/^ //g' |\
awk '{if (NR%5) {ORS=""} else {ORS="\n"}{print $0"|"}}') # cheers https://stackoverflow.com/a/35315421/5377165
netblocks_header=" IPv6 NET RANGE | INFO"
for netblock in $netblocks; do
prefix=$(echo -e "$netblock" | cut -d '|' -f 1)
netname=$(echo -e "$netblock" | cut -d '|' -f 2)
nethandle=$(echo -e "$netblock" | cut -d '|' -f 3)
netname+=" (${nethandle})"
netblock_type=$(echo -e "$netblock" | cut -d '|' -f 4)
if [ "$netblock_type" = "unknown" ]; then
nettype=""
else
nettype=" (${yellow}$netblock_type${default})"
fi
regdate=$(echo -e "$netblock" | cut -d '|' -f 5)
prefix_spacing=61
netblocks_output+=$(printf "\n${blue}%${prefix_spacing}s${default} | ${green}%-45s${default} - Registered: ${magenta}%s${default}%s" "$prefix" "$netname" "$regdate" "$nettype")
done
fi
if [ -n "$netblocks_output" ]; then
# Print out netblocks
NO_RESULTS=false
searchresults+=$(echo -e "\n${red}Org-ID: ${magenta}${orgid}${red}${default}\n${netblocks_header}${netblocks_output}")
searchresults+="\n"
fi
done
[[ "$NO_RESULTS" = true ]] && searchresults+="\n\t${red}No results found${default}\n"
done
done
NO_ERROR_ON_INTERRUPT=false
StatusbarMessage
echo -e "$searchresults\n${yellow}────────────────────────────────────────────────────${default}"
# let the user choose if they want to run a quick IP lookup
while true; do
echo -e "\n- Enter any ${blue}IP/Prefix${default} to look it up or"
echo -e "- Press ${yellow}ENTER${default} to return to the menu:\n"
echo -n ">> "
read -r choice
# check if it's an IPv4/IPv6
if [ -n "${choice}" ]; then
input=$(echo "$choice" | sed 's/\/.*//g' | grep -Eo "$ipv4v6regex")
if [ -n "$input" ]; then
# valid IP
echo ""
StatusbarMessage "Looking up data for ${bluebg}${input}${lightgreybg}"
LookupASNAndRouteFromIP "$input"
(( longest=${#input}+1 ))
WhoisIP "$input"
PrintReputationAndShodanData "$input"
StatusbarMessage
continue
else
continue
fi
else
# user pressed ENTER, go back to main organizations menu
clear
break
fi
done
done
}
ShowMenu(){ # show selection menu for search-by-company results
clear
BoxHeader "Organizations matching \"$userinput\""
if [ "$ORG_FILTER" = true ]; then
num_inclusion_filters="${#orgfilters_array[@]}"
num_exclusion_filters="${#excl_orgfilters_array[@]}"
num_filters=$(( num_inclusion_filters+num_exclusion_filters ))
[[ $num_filters = 1 ]] && s="" || s="s"
ACTIVE_FILTERS_STRING=$'\n'"${bluebg}${black}${num_filters} Active filter${s}:${default}"
# recap inclusion filters
for filter in "${orgfilters_array[@]}"; do
ACTIVE_FILTERS_STRING+=" ${lightgreybg}${filter}${default}"
done
# recap exclusion filters
for filter in "${excl_orgfilters_array[@]}"; do
ACTIVE_FILTERS_STRING+=" ${lightgreybg}${red}-${filter}${default}"
done
ACTIVE_FILTERS_STRING+=$'\n'
else
ACTIVE_FILTERS_STRING=""
fi
if [ "$HAVE_IPCALC" = true ]; then
IPCALC_WARNING=""
else
IPCALC_WARNING=$'\n'"${yellow}Warning: program ${red}ipcalc${yellow} not found."$'\n'"Install it to enable netblock→CIDR"$'\n'"prefix aggregation.${default}"$'\n'
fi
PS3="${yellow}────────────────────────────────────────────────────${default}
$ACTIVE_FILTERS_STRING
${yellow}${#orgs[@]} of $total_orgsearch_results total results shown${default}
Choose an organization or enter:
- <${green}text${default}> to FILTER FOR A STRING
- <${blue}-${default}> to EXCLUDE A STRING
- <${blue}x${default}> to REMOVE ALL FILTERS
- <${blue}a${default}> to LOOKUP ALL RESULTS (max 10)
- <${blue}q${default}> to QUIT
$IPCALC_WARNING
>> "
echo -e "${yellow}────────────────────────────────────────────────────${green}"
COLUMNS=1
set -o posix
select choice in "${orgs[@]}"; do
for org in "${orgs[@]}"; do
if [[ "$org" = "$choice" ]]; then
LOOKUP_ALL_RESULTS=false
break 2
fi
done
case "$REPLY" in
"q"|"Q")
echo ""
exit 0
;;
"-")
# add an exclusion filter
echo -n "Enter a string to ${red}exclude${default}: "
read -r exclusion_string
excl_orgfilters_array+=("$exclusion_string")
ORG_FILTER=true
SearchByOrg
;;
"x"|"X")
# reset filters
if [ "$ORG_FILTER" = true ]; then
unset orgfilters_array
unset excl_orgfilters_array
declare -a orgfilters_array
declare -a excl_orgfilters_array
SearchByOrg
fi
;;
"a"|"A")
# lookup all results
if [ "${#orgs[@]}" -gt 10 ]; then
echo -en "\n${redbg}Too many results! Please add some filters!${default}\n"
sleep 2
continue
fi
LOOKUP_ALL_RESULTS=true
break
;;
*)
# apply filter to the results
orgfilters_array+=("$REPLY")
ORG_FILTER=true
SearchByOrg
;;
esac
done
set +o posix
echo ""
}
PrintReputationAndShodanData(){
# We already collected reputation and Shodan data since we called IPGeoRepLookup() and IPShodanLookup() previously
if [ "$JSON_OUTPUT" = true ]; then
# JSON output
final_json_output+=",\"reputation\":{"
[[ -n "$json_rep" ]] && final_json_output+="\"status\":\"$json_rep\""
[[ -n "$json_iqs_threat_score" ]] && final_json_output+="$json_iqs_threat_score"
[[ "$gn_noisy" = "true" ]] && final_json_output+=",\"is_noisy\":$gn_noisy"
[[ -n "$gn_json_is_knowngood" ]] && final_json_output+=",\"is_known_good\":$gn_json_is_knowngood"
[[ -n "$gn_json_is_knownbad" ]] && final_json_output+=",\"is_known_bad\":$gn_json_is_knownbad"
[[ -n "$gn_json_aka" ]] && final_json_output+=",\"known_as\":\"$gn_json_aka\""
[[ -n "$json_iqs_threat_tags" ]] && final_json_output+="$json_iqs_threat_tags"
final_json_output+="},\"fingerprinting\":{"
[[ -n "$shodan_cpes_json_output" ]] && final_json_output+="\"cpes\":$shodan_cpes_json_output,"
[[ -n "$shodan_ports_json_output" ]] && final_json_output+="\"ports\":$shodan_ports_json_output,"
[[ -n "$shodan_tags_json_output" ]] && final_json_output+="\"tags\":$shodan_tags_json_output,"
[[ -n "$shodan_vulns_json_output" ]] && final_json_output+="\"vulns\":$shodan_vulns_json_output,"
# truncate last comma
[[ ${final_json_output: -1} = "," ]] && final_json_output=${final_json_output::-1}
final_json_output+="}"
final_json_output+="}"
else
# normal output
[[ -n "$ip_shodan_cpe_data" ]] && printf "${white}%${indent}s${bluebg}CPE${default}%s\n" "├" "${ip_shodan_cpe_data}"
[[ -n "$ip_shodan_ports_data" ]] && printf "${white}%${indent}s${bluebg}POR${default}%s\n" "├" "${ip_shodan_ports_data}"
[[ -n "$ip_shodan_tags_data" ]] && printf "${white}%${indent}s${bluebg}TAG${default}%s\n" "├" "${ip_shodan_tags_data}"
[[ -n "$ip_shodan_cve_data" ]] && printf "${white}%${indent}s${bluebg}CVE${default}%s\n" "├" "${ip_shodan_cve_data}"
printf "${white}%${indent}s${bluebg}REP${default} ${magenta}%s${default}\n\n" "└" "$ip_rep_data"
fi
}
ShodanRecon(){
shodan_starttime=$(date +%s)
shodan_json_output=""
curloutput=""
urllist=""
# identify target(s) type
targetlist=$(echo -e "$userinput" | tr ' ' '\n')
target_count=$(echo -e "$targetlist" | wc -l)
if [ "$target_count" -gt 1 ]; then
# user passed multiple targets "e.g. asn -s 1.1.1.1 8.8.8.8"
userinput="multiple targets"
fi
BoxHeader "Shodan scan for $userinput"
for target in $targetlist; do
input=$(echo "$target" | sed 's/\/.*//g' | grep -Eo "$ipv4v6regex")
if [ -z "$input" ]; then
# Input is not an IP Address. See if it's a hostname (includes at least one dot)
if echo "$target" | grep -q "\."; then
target=$(awk -F[/:] '{gsub(".*//", ""); gsub(".*:.*@", ""); print $1}' <<<"$target")
target=$(ResolveHostnameToIPList "$target")
[[ -z "$target" ]] && continue # could not resolve hostname
json_target_type="hostname"
target=$(grep -v ":" <<<"$target")
numips=$(wc -l <<<"$target")
[[ $numips = 1 ]] && s="" || s="es"
else
# not an IP, not a hostname
continue
fi
else
json_target_type="ip"
fi
# prepare shodan InternetDB API URL list for every IP in the given CIDR range
for ip in $target; do
if grep -q ":" <<<"$ip"; then
# Shodan has no data for IPv6 addresses
continue
fi
[[ -n "$urllist" ]] && urllist+="\n"
urllist+=$(nmap -sL -n "$ip" 2>/dev/null | awk '/Nmap scan report/{print "https://internetdb.shodan.io/"$NF}') # cheers https://stackoverflow.com/a/31412705
done
done
[[ -z "$urllist" ]] && PrintErrorAndExit "no valid targets found"
numtargets=$(echo -e "$urllist" | wc -l)
firsturl=1
while true; do
saved_lasturl="$lasturl"
lasturl=$(( firsturl + MAX_CONCURRENT_SHODAN_REQUESTS ))
# check if concurrent Shodan requests setting is > number of IPs yet to scan
# if so, perform a batch lookup with all the urls and break. Otherwise
# perform max concurrent lookups, sleep and continue
if [ "$lasturl" -ge "$numtargets" ]; then
lasturl="$numtargets"
LAST_BATCH=true
fi
urlbatch=$(echo -e "$urllist" | awk "NR==$firsturl,NR==$lasturl")
StatusbarMessage "Collecting data for IPs ${bluebg}${firsturl}-${lasturl}${lightgreybg} of ${bluebg}$numtargets${lightgreybg} total ($MAX_CONCURRENT_SHODAN_REQUESTS threads)"
DebugPrint "${yellow}curl {$(echo -e "$urlbatch" | tr '\n' ',')}${default}"
batchoutput=$( xargs -P "$MAX_CONCURRENT_SHODAN_REQUESTS" -n 1 curl -s < <(echo -e "$urlbatch") )
if grep -q "Rate limit exceeded" <<<"$batchoutput"; then
# rate limit exceeded during current batch of queries, loop while waiting to continue
retryafter=$(docurl -s -i "https://internetdb.shodan.io/127.0.0.1" | awk '/^retry-after:/ {print $2}' | tr -d '\r\n')
while [[ ${retryafter} -gt 1 ]]; do
StatusbarMessage "Shodan rate limit hit during batch ${bluebg}${firsturl}-${lasturl}${lightgreybg}, resuming in ${redbg}${retryafter}${lightgreybg}s"
sleep 1
retryafter=$(( retryafter-1 ))
done
# retry last batch
lasturl="$saved_lasturl"
continue
else
curloutput+="$batchoutput"
[[ "$LAST_BATCH" = true ]] && break
# enable below to introduce a delay between batches of curl queries to Shodan (could help with rate limiting)
# sleep .5
firsturl=$(( lasturl++ ))
fi
done
StatusbarMessage
# convert single API output results to array, delete IPs for which Shodan had no data available
json=$(jq -cM --slurp 'del (.[] | select(.ip==null))' <<<"$curloutput")
if [ "$json" = "[]" ]; then
final_json_output="$json"
PrintErrorAndExit "Shodan has no data for $userinput"
fi
shodan_json_output="$json"
[[ "$JSON_OUTPUT" = true ]] && return
shodan_endtime=$(date +%s)
shodan_runtime=$((shodan_endtime-shodan_starttime))
StatusbarMessage "Parsing collected data"
vulnlist=""
hostnamelist=""
portlist=""
taglist=""
cpelist=""
total_hosts_with_vulns=0
total_hosts_with_ports=0
total_hosts_with_tags=0
total_hosts_with_cpe=0
total_hosts_with_hostnames=0
# find all IPs in the json having a non-null ".ip" attribute. It means Shodan has some data about that IP.
interesting_iplist=$(echo -e "$json" | jq -r '.[].ip' | sort | uniq)
# iterate over interesting IPs
for ip in $interesting_iplist; do
output+="\n${white}•${default} $ip"
attribs=$(jq -r '.[] | select(.ip=="'"$ip"'")| to_entries | .[] | .key + "=" + (.value | @sh)' <<<"$json")
numattrs=$(grep -Evc "=$|^ip=" <<<"$attribs"); # get number of populated attributes
# "attribs" contains a list of attributes for a single IP, composed like this:
#
# cpes='cpename1' 'cpename2'
# hostnames='abc.com'
# ip='1.2.3.4'
# ports=22 80 8080
# tags='vpn'
# vulns='CVE-123' 'CVE-345'
# iterate over resulting attributes.
count=0 # will be used to decide which tree symbol to use (├ or └)
# - hostnames
ip_hostnames=$(grep -E "^hostnames=" <<<"$attribs" | cut -d '=' -f 2- | tr -d "'" | uniq)
hostnamelist+=$(echo -e "\n$ip_hostnames" | tr ' ' '\n')
if [ -n "$ip_hostnames" ]; then
count=$(( count+1 ))
[[ "$count" -eq "$numattrs" ]] && treesymbol="└" || treesymbol="├"
ip_hostnames=$(echo -e "$ip_hostnames" | sed -e 's/ / • /g' -e "s/.\{136\}/&\n /g")
total_hosts_with_hostnames=$(( total_hosts_with_hostnames+1 ))
output+="\n\t${treesymbol}${bluebg} PTR ${default} ${white}$ip_hostnames${default}"
fi
# - cpes
ip_cpes=$(grep -E "^cpes=" <<<"$attribs" | cut -d '=' -f 2- | tr -d "'" | uniq)
# use more user-friendly names for some CPEs
ip_cpes=$(echo -e "$ip_cpes" | \
sed -e 's,cpe:/a:apache:http_server,Apache-HTTPD,g' \
-e 's,cpe:/o:linux:linux_kernel,[O/S]-Linux,g' \
-e 's,cpe:/o:microsoft:windows,[O/S]-Microsoft-Windows,g' \
-e 's,cpe:/o:debian:debian_linux,[O/S]-Debian-Linux,g' \
-e 's,cpe:/a:microsoft:internet_information_services,Microsoft-IIS,g' \
-e 's,cpe:/a:microsoft:exchange_server,Microsoft-Exchange,g' \
-e 's,cpe:/a:openbsd:openssh,OpenSSH,g' \
-e 's,cpe:/a:igor_sysoev:nginx,Nginx,g' \
-e 's,cpe:/a:php:php,PHP,g' \
-e 's,cpe:/a:jquery:jquery,jQuery,g' \
-e 's,cpe:/a:getbootstrap:bootstrap,Bootstrap,g' \
-e 's,cpe:/a:pureftpd:pure-ftpd,Pure-FTPd,g' \
-e 's,cpe:/a:postfix:postfix,Postfix,g' \
-e 's,cpe:/a:openssl:openssl,OpenSSL,g' \
-e 's,cpe:/a:mysql:mysql,MySQL,g' \
)
cpelist+=$(echo -e "\n$ip_cpes" | tr ' ' '\n')
if [ -n "$ip_cpes" ]; then
count=$(( count+1 ))
[[ "$count" -eq "$numattrs" ]] && treesymbol="└" || treesymbol="├"
ip_cpes=$(echo -e "$ip_cpes" | sed -e 's/ / • /g' -e "s/.\{136\}/&\n /g")
total_hosts_with_cpe=$(( total_hosts_with_cpe+1 ))
output+="\n\t${treesymbol}${bluebg} CPE ${default} ${blue}$ip_cpes${default}"
fi
# - ports
ip_ports=$(grep -E "^ports=" <<<"$attribs" | cut -d '=' -f 2- | uniq)
portlist+=$(echo -e "\n$ip_ports" | tr ' ' '\n')
if [ -n "$ip_ports" ]; then
count=$(( count+1 ))
[[ "$count" -eq "$numattrs" ]] && treesymbol="└" || treesymbol="├"
ip_ports=$(echo -e "$ip_ports" | sed -e 's/ / • /g' -e "s/.\{136\}/&\n /g")
total_hosts_with_ports=$(( total_hosts_with_ports+1 ))
output+="\n\t${treesymbol}${bluebg} POR ${default} ${green}$ip_ports${default}"
fi
# - tags
ip_tags=$(grep -E "^tags=" <<<"$attribs" | cut -d '=' -f 2- | tr -d "'" | uniq)
taglist+=$(echo -e "\n$ip_tags" | tr ' ' '\n')
if [ -n "$ip_tags" ]; then
count=$(( count+1 ))
[[ "$count" -eq "$numattrs" ]] && treesymbol="└" || treesymbol="├"
ip_tags=$(echo -e "$ip_tags" | sed -e 's/ / • /g' -e "s/.\{136\}/&\n /g")
total_hosts_with_tags=$(( total_hosts_with_tags+1 ))
output+="\n\t${treesymbol}${bluebg} TAG ${default} ${yellow}$ip_tags${default}"
fi
# - vulns
ip_vulns=$(grep -E "^vulns=" <<<"$attribs" | cut -d '=' -f 2- | tr -d "'" | uniq)
vulnlist+=$(echo -e "\n$ip_vulns" | tr ' ' '\n')
if [ -n "$ip_vulns" ]; then
count=$(( count+1 ))
[[ "$count" -eq "$numattrs" ]] && treesymbol="└" || treesymbol="├"
# ip_vulns=$(echo -e "$ip_vulns" | sed -e 's/ / • /g')
ip_vulns=$(echo -e "$ip_vulns" | sed -e 's/ / • /g' -e "s/.\{136\}/&\n /g")
total_hosts_with_vulns=$(( total_hosts_with_vulns+1 ))
output+="\n\t${treesymbol}${bluebg} CVE ${default} ${red}$ip_vulns${default}"
fi
output+="\n"
done
StatusbarMessage
echo -e "$output"
final_stats="${default}\n______________________________\n\n"
final_stats+="${green}$total_hosts_with_ports exposed${default} host"; [[ "$total_hosts_with_ports" != "1" ]] && final_stats+="s"; final_stats+=" found"
final_stats+="\n${blue}$total_hosts_with_cpe hosts${default} identified with a known CPE"
final_stats+="\n${yellow}$total_hosts_with_tags tagged${default} host"; [[ "$total_hosts_with_tags" != "1" ]] && final_stats+="s"; final_stats+=" identified"
final_stats+="\n$total_hosts_with_hostnames hostname"; [[ "$total_hosts_with_hostnames" != "1" ]] && final_stats+="s"; final_stats+=" discovered"
final_stats+="\n${red}$total_hosts_with_vulns vulnerable${default} host"; [[ "$total_hosts_with_vulns" != "1" ]] && final_stats+="s"; final_stats+=" found"
final_stats+="\n\n$numtargets host"; [[ "$numtargets" != "1" ]] && final_stats+="s"; final_stats+=" analyzed in $shodan_runtime seconds.\n"
# display final statistics (non-JSON output mode only)
BoxHeader "Statistics"
# top N ports
echo -e "\n${green}[TOP ${SHODAN_SHOW_TOP_N} Open Ports] \n"
if [ -z "$portlist" ]; then
# no ports found
echo -e "${green} Nothing found${default}\n"
else
for port in $(echo -e "$portlist" | sort | grep -Ev '^$' | uniq -c | sort -rn | head -n "${SHODAN_SHOW_TOP_N}"); do
porthits=$(awk '{print $1}' <<<"$port")
portnum=$(awk '{print $2}' <<<"$port")
portname=$(ResolveWellKnownPort "$portnum")
[[ -n "$portname" ]] && portname="(${portname})"
printf "%10s host(s) → Port %5s %s\n" "$porthits" "$portnum" "$portname"
done
echo -e "$default"
fi
# top N CPEs
echo -e "${blue}[TOP ${SHODAN_SHOW_TOP_N} CPEs] \n"
if [ -z "$cpelist" ]; then
# no CPEs found
echo -e "${blue} Nothing found${default}\n"
else
for cpe in $(echo -e "$cpelist" | sort | grep -Ev '^$' |uniq -c | sort -rn | head -n "${SHODAN_SHOW_TOP_N}"); do
cpehits=$(awk '{print $1}' <<<"$cpe")
cpefullname=$(awk '{print $2}' <<<"$cpe")
cpetype=$(echo "$cpefullname" | cut -d ':' -f 2)
cpename=$(echo "$cpefullname" | cut -d ':' -f 3-)
case "${cpetype}" in
"/a")
type="APP"
;;
"/o")
type="O/S"
;;
"/h")
type="H/W"
;;
*)
type=""
;;
esac
[[ -n "$type" ]] && cpename="[$type] $cpefullname" || cpename="$cpefullname"
printf "%10s host(s) → %s\n" "$cpehits" "$cpename"
done
echo -e "$default"
fi
# top N tags
echo -e "${yellow}[TOP ${SHODAN_SHOW_TOP_N} Tags] \n"
if [ -z "$taglist" ]; then
# no tags found
echo -e "${yellow} Nothing found${default}\n"
else
for tag in $(echo -e "$taglist" | sort | grep -Ev '^$' | uniq -c | sort -rn | head -n "${SHODAN_SHOW_TOP_N}"); do
taghits=$(awk '{print $1}' <<<"$tag")
tagname=$(awk '{print $2}' <<<"$tag")
printf "%10s host(s) → %s\n" "$taghits" "$tagname"
done
echo -e "$default"
fi
# first N hostnames discovered
echo -e "${white}[First ${SHODAN_SHOW_TOP_N} Hostnames discovered] \n"
if [ -z "$hostnamelist" ]; then
# no tags found
echo -e "${yellow} Nothing found${default}\n"
else
for hostname in $(echo -e "$hostnamelist" | grep -Ev '^$' | head -n "${SHODAN_SHOW_TOP_N}"); do
printf " %s\n" "$hostname"
done
echo -e "$default"
fi
# top N vulnerabilities
echo -e "${red}[TOP ${SHODAN_SHOW_TOP_N} Vulnerabilities by number of occurrences] \n"
StatusbarMessage "Identifying CVE score and severity for vulnerable hosts"
cvestats_text=""
if [ -z "$vulnlist" ]; then
# no vulns found
cvestats_text="${red} Nothing found${default}"
else
for cve in $(echo -e "$vulnlist" | sort | grep -Ev '^$' | uniq -c | sort -rn | head -n "${SHODAN_SHOW_TOP_N}"); do
vulnhits=$(awk '{print $1}' <<<"$cve")
cvenum=$(awk '{print $2}' <<<"$cve")
cvejsondata=$(docurl -s "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=$cvenum")
v3score=$(jq -r '.vulnerabilities[0].cve.metrics.cvssMetricV31[0].cvssData.baseScore | select(length>0)' <<<"$cvejsondata" 2>/dev/null)
v3severity=$(jq -r '.vulnerabilities[0].cve.metrics.cvssMetricV31[0].cvssData.baseSeverity | select(length>0)' <<<"$cvejsondata" 2>/dev/null)
v2score=$(jq -r '.vulnerabilities[0].cve.metrics.cvssMetricV2[0].cvssData.baseScore' <<<"$cvejsondata" 2>/dev/null)
v2severity=$(jq -r '.vulnerabilities[0].cve.metrics.cvssMetricV2[0].baseSeverity' <<<"$cvejsondata" 2>/dev/null)
cvename=$(jq -r '.vulnerabilities[0].cve.cisaVulnerabilityName | select(length>0)' <<<"$cvejsondata" 2>/dev/null)
cvedesc=$(jq -r '.vulnerabilities[0].cve.descriptions[0].value | select(length>0)' <<<"$cvejsondata" 2>/dev/null)
# apply formatting to cvedesc to fit the terminal width
cvedesc_len=${#cvedesc}
available_width=$(( terminal_width - 58 ));
formatted_cvedesc=""
if [ "$cvedesc_len" -gt "$available_width" ]; then
# iterate over cvedesc until it fits in the terminal width, inserting newlines
while [ "$cvedesc_len" -gt 0 ]; do
wordbreak_pointer="$available_width"
if [ "$cvedesc_len" -gt "$available_width" ]; then
while [ "${cvedesc:$wordbreak_pointer:1}" != " " ] && [ "$wordbreak_pointer" -gt 0 ]; do
((wordbreak_pointer--))
done
[[ "$wordbreak_pointer" -eq 0 ]] && wordbreak_pointer="$available_width"
fi
cvedesc_line=${cvedesc:0:$wordbreak_pointer}
if [ -n "$formatted_cvedesc" ]; then
formatted_cvedesc+="\n"
formatted_cvedesc+=$(printf "${default}${red}%49s ${default}${dim}%s" "┆" "$cvedesc_line")
else
formatted_cvedesc="$cvedesc_line"
fi
((wordbreak_pointer++))
cvedesc=${cvedesc:$wordbreak_pointer}
cvedesc_len=${#cvedesc}
done
else
formatted_cvedesc="$cvedesc"
fi
cvescore=""
cveseverity=""
if [ -n "$v3score" ] && [ -n "$v3severity" ]; then
cvescore="$v3score"
cveseverity="$v3severity"
elif [ -n "$v2score" ] && [ -n "$v2severity" ]; then
cvescore="$v2score"
cveseverity="$v2severity"
fi
if [ "${#cvescore}" -eq 1 ]; then
cvescore+=".0"
elif [ "${#cvescore}" -eq 2 ]; then
cvescore=" $cvescore"
fi
case "${cveseverity}" in
"LOW")
cvetext="${greenbg} ${cvescore} (LOW) ${default}${red}"
;;
"MEDIUM")
cvetext="${yellowbg} ${cvescore} (MEDIUM) ${default}${red}"
;;
"HIGH")
cvetext="${redbg} ${cvescore} (HIGH) ${default}${red}"
;;
"CRITICAL")
cvetext="${redbg} ${cvescore} (CRITICAL) ${default}${red}"
;;
*)
cvescore="N/A"
cvetext="${lightgreybg} ${cvescore} (UNKNOWN) ${default}${red}"
;;
esac
cvestats_text+=$(printf "${red}%10s host(s) → %-15s %-14s" "$vulnhits" "$cvetext" "$cvenum")
[[ -n "$cvename" ]] && cvestats_text+=" • ${dim}${cvename}${default}"
cvestats_text+="\n"
cvestats_text+=$(printf "${red}%49s Desc : ${default}${dim}%s${default}" "├" "$formatted_cvedesc")
cvestats_text+="\n"
cvestats_text+=$(printf "${red}%49s Info : ${blue}${dim}%s${default}" "└" "https://nvd.nist.gov/vuln/detail/$cvenum")
cvestats_text+="\n\n"
done
fi
StatusbarMessage
echo -e "$cvestats_text"
echo -e "$final_stats"
}
BulkGeolocate(){
# identify target(s) type
targetlist_allips=$(echo -e "$userinput" | grep -Eo "$ipv4v6regex" | sort)
top10_ipv4=$(grep -v ":" <<<"$targetlist_allips" | uniq -c | sort -rn | head -n 10)
top10_ipv6=$(grep ":" <<<"$targetlist_allips" | uniq -c | sort -rn | head -n 10)
targetlist=$(uniq <<<"$targetlist_allips")
if [ -n "$targetlist" ]; then
target_count=$(echo -e "$targetlist" | wc -l)
else
PrintErrorAndExit "no valid targets found"
fi
if [ "$target_count" -gt 1 ]; then
# user passed multiple targets "e.g. asn -g 1.1.1.1 8.8.8.8"
userinput="multiple targets"
fi
BoxHeader "Geolocation lookup for $userinput"
# TODO switch to ipinfo.io for bulk geolocation lookups when a token is present (free tier does not support batch queries)
geolocation_json_output=""
countrylist=""
firstip=1
while true; do
lastip=$(( firstip + 99 ))
if [ "$lastip" -ge "$target_count" ]; then
lastip="$target_count"
LAST_BATCH=true
fi
ipbatch=$(echo -en "$targetlist" | awk "NR==$firstip,NR==$lastip")
StatusbarMessage "Collecting geolocation data for IPs ${bluebg}${firstip}-${lastip}${lightgreybg} of ${bluebg}$target_count${lightgreybg} total"
ipmap_targets=$(echo -en "$ipbatch" | tr '\n' ',')
ipapi_targets=$(jq -c --slurp --raw-input 'split("\n") | map(select(length > 0))' <<<"$ipbatch")
ipmap_output=$(docurl -m15 -s "https://ipmap.ripe.net/api/v1/locate/all?resources=$ipmap_targets")
if [[ -n $(jq 'select (.error != null) | .error' <<<"$ipmap_output") ]]; then
ipmap_output=""
fi
# Note: the free IP-API tier only supports unencrypted HTTP, not HTTPS
ipapi_output=$(docurl -m4 -s "http://ip-api.com/batch?fields=query,status,message,country,countryCode,regionName,city" --data ''"$ipapi_targets"'')
for target in $ipbatch; do
ip_geo_countryname=""
json_geo_city=""
json_geo_region=""
json_geo_cc=""
if [ -n "$ipmap_output" ]; then
IS_ANYCAST=$(jq '.metadata.service.contributions."'"$target"'".latency.metadata.anycast' <<<"$ipmap_output")
[[ "$IS_ANYCAST" = true ]] && anycast_tag="${yellowbg} ANYCAST ${default}" || anycast_tag=""
ip_geo_countryname=$(jq -r '.data."'"$target"'" | select( .countryName != null ) | .countryName' <<<"$ipmap_output")
json_geo_city="$(jq -r '.data."'"$target"'" | select( .cityName != null ) | .cityName' <<<"$ipmap_output")"
json_geo_region="$(jq -r '.data."'"$target"'" | select( .stateName != null ) | .stateName' <<<"$ipmap_output")"
json_geo_cc="$(jq -r '.data."'"$target"'" | select( .countryCodeAlpha2 != null ) | .countryCodeAlpha2' <<<"$ipmap_output")"
fi
if [ -n "$ip_geo_countryname" ] && [ -n "$json_geo_city" ] && [ -n "$json_geo_region" ] && [ -n "$json_geo_cc" ]; then
ip_geo_data="$json_geo_city, $json_geo_region, $json_geo_cc"
else
# incomplete/no data on RIPE IPMap, fallback to IP-API
ipapi_status=$(jq -r '.[] | select(.query == "'"$target"'") | .status' <<<"$ipapi_output")
if [ "$ipapi_status" = "fail" ] && [ "$JSON_OUTPUT" = false ]; then
ip_geo_countryname="Unknown"
ip_geo_data="N/A"
else
ip_geo_countryname=$(jq -r '.[] | select(.query == "'"$target"'") | .country' <<<"$ipapi_output")
if [ -z "$ip_geo_countryname" ] && [ "$JSON_OUTPUT" = false ]; then
ip_geo_countryname="Unknown"
ip_geo_data="N/A"
else
json_geo_city="$(jq -r '.[] | select(.query == "'"$target"'") | .city' <<<"$ipapi_output")"
json_geo_region="$(jq -r '.[] | select(.query == "'"$target"'") | .regionName' <<<"$ipapi_output")"
json_geo_cc="$(jq -r '.[] | select(.query == "'"$target"'") | .countryCode' <<<"$ipapi_output")"
if [ -n "$json_geo_city" ] && [ -n "$json_geo_region" ] && [ -n "$json_geo_cc" ]; then
ip_geo_data="$json_geo_city, $json_geo_region, $json_geo_cc"
else
ip_geo_data="Unknown"
fi
fi
fi
fi
if [ "$ip_geo_countryname" = "Unknown" ]; then
namecolor="${red}"
countrycolor="${red}"
else
namecolor="${white}"
countrycolor="${green}"
fi
if [ "$JSON_OUTPUT" = true ]; then
[[ -n "$geolocation_json_output" ]] && geolocation_json_output+=","
geolocation_json_output+="{\"ip\":\"$target\""
geolocation_json_output+=",\"city\":\"$json_geo_city\""
geolocation_json_output+=",\"region\":\"$json_geo_region\""
geolocation_json_output+=",\"country\":\"$ip_geo_countryname\""
geolocation_json_output+=",\"cc\":\"$json_geo_cc\""
geolocation_json_output+=",\"hits\":$(grep -c "$target" <<<"$targetlist_allips")"
[[ "$IS_ANYCAST" = true ]] && geolocation_json_output+=",\"is_anycast\": true"
geolocation_json_output+="}"
else
final_output+=$(printf "${white}%-16s${default}: ${namecolor}%s${default} (${countrycolor}%s${default}) %s" "$target" "$ip_geo_data" "$ip_geo_countryname" "$anycast_tag\n")
countrylist+="\n$ip_geo_countryname"
fi
done
[[ "$LAST_BATCH" = true ]] && break
firstip=$(( lastip++ ))
done
StatusbarMessage
[[ "$JSON_OUTPUT" = true ]] && return
echo -en "${final_output}"
# top 10 IPs
if [ -n "$top10_ipv4" ]; then
BoxHeader "Top 10 IPv4 by number of hits"
for entry in $top10_ipv4; do
iphits=$(awk '{print $1}' <<<"$entry")
ipaddr=$(awk '{ print substr($0, index($0,$2)) }' <<<"$entry")
printf "${white}%16s appears ${magenta}%s${default} time%s\n" "$ipaddr" "$iphits" "$([[ "$iphits" != "1" ]] && echo -n "s")"
done
fi
if [ -n "$top10_ipv6" ]; then
BoxHeader "Top 10 IPv6 by number of hits"
for entry in $top10_ipv6; do
iphits=$(awk '{print $1}' <<<"$entry")
ipaddr=$(awk '{ print substr($0, index($0,$2)) }' <<<"$entry")
printf "${white}%16s appears ${magenta}%s${default} time%s\n" "$ipaddr" "$iphits" "$([[ "$iphits" != "1" ]] && echo -n "s")"
done
fi
# draw countries bar chart
BoxHeader "Country stats"
DrawChart "$countrylist" "IP"
}
DrawChart(){
# draws a bar chart for the occurrences of values from a list.
# $1 must be a list of values (not necessarily sorted)
# $2 must be the unit name of the bars (singular. A final 's' is added for bars whose value is > 1)
# [optional] $3 can be the number of items to display (i.e. TOP n)
inputlist="$1"
unitname="$2"
[[ -n "$3" ]] && topn="$3" || topn="0"
sorted_input=""
declare -A valuearray # associative array
declare -a sorted_input # simple array, to be used as an index into valuearray
for item in $(echo -e "$inputlist" | sort | grep -Ev '^$' | uniq -c | sort -rn); do
itemhits=$(awk '{print $1}' <<<"$item")
itemname=$(awk '{ print substr($0, index($0,$2)) }' <<<"$item")
valuearray["$itemname"]="$itemhits"
sorted_input+=("$itemname")
done
total_unique_names="${#sorted_input[@]}"
longest_itemname=$(echo -e "$inputlist" | wc -L)
count=0
while true; do
item=${sorted_input[$count]}
# incrementally choose color for the bar and index for the item array entry - skip 0 (black)
[[ "$MONOCHROME_MODE" = false ]] && tput setaf $(( count + 1 ))
itemhits="${valuearray[$item]}"
spacing="$itemhits"
[[ "$itemhits" -gt "$terminal_width" ]] && spacing=$(( terminal_width-(14+longest_itemname) ))
printf " %${longest_itemname}s " "$item"
printf "█%.0s" $(seq "$spacing")
printf " %s ${unitname}%s\n" "$itemhits" "$([[ "$itemhits" != "1" ]] && echo -n "s")"
(( count++ ))
if [ "$topn" != "0" ] && [ "$count" -ge "$topn" ]; then
break
elif [ "$count" -ge "$total_unique_names" ]; then
break
fi
done
echo -e "${default}"
}
PrintJsonOutput(){
DisableColors
endtime=$(date +%s)
runtime=$((endtime-starttime))
json_to_print="{\"target\":\"$userinput\","
json_to_print+="\"target_type\":\"$json_target_type\","
json_to_print+="\"result\":\"$status_json_output\","
json_to_print+="\"reason\":\"$reason_json_output\","
json_to_print+="\"version\":\"$ASN_VERSION\","
json_to_print+="\"request_time\":\"$json_request_time\","
json_to_print+="\"request_duration\":$runtime,"
json_to_print+="\"api_tokens\":{"
json_to_print+="\"ipqualityscore\":$json_IQS_TOKEN,"
json_to_print+="\"ipinfo\":$json_IPINFO_TOKEN,"
json_to_print+="\"cloudflare\":$json_CLOUDFLARE_TOKEN"
json_to_print+="},"
json_to_print+="\"result_count\":$json_resultcount,"
if [ "$RECON_MODE" = true ] || [ "$BGP_UPSTREAM_MODE" = true ]; then
# we already have an array as a final json output, append it as-is
json_to_print+="\"results\":${final_json_output}}"
else
json_to_print+="\"results\":[${final_json_output}]}"
fi
[[ "$JSON_PRETTY" = true ]] && json_to_print=$(jq -M '.' <<<"$json_to_print") || json_to_print=$(jq -c '.' <<<"$json_to_print")
echo -e "$json_to_print"
}
ResolveWellKnownPort(){
# input: port number, output: service name
[[ -n "$WELL_KNOWN_PORTS" ]] && awk "/\t$1\/(tc|ud)p/{print \$1; exit}" <<<"$WELL_KNOWN_PORTS"
}
RPKILookup(){
# $1=asn, $2=prefix
found_rpkivalidity=""
INVALID_ROA=false
rpki_apioutput=$(docurl -s "https://stat.ripe.net/data/rpki-validation/data.json?resource=$1&prefix=$2&sourceapp=nitefood-asn")
is_valid_json=$(jq type <<<"$rpki_apioutput" 2>/dev/null)
if [ -n "$is_valid_json" ]; then
found_rpkivalidity=$(jq -r '.data.status' <<<"$rpki_apioutput" | tr '[:lower:]' '[:upper:]')
found_rpkiroacount=$(jq '.data.validating_roas | length' <<<"$rpki_apioutput")
roacount_json_output="$found_rpkiroacount"
found_rpkiprefix=$(jq -r '.data.validating_roas[0].prefix' <<<"$rpki_apioutput")
found_rpkiorigins=$(jq -r '.data.validating_roas[] | select (.prefix=="'"$found_rpkiprefix"'") | .origin' <<<"$rpki_apioutput")
found_rpkiorigin="["
origin_count=0
for origin in $found_rpkiorigins; do
[[ "$origin_count" -gt 0 ]] && found_rpkiorigin+=", "
found_rpkiorigin+="AS$origin"
(( origin_count++ ))
done
found_rpkiorigin+="]"
found_rpkimaxlength=$(jq -r '.data.validating_roas[0].max_length' <<<"$rpki_apioutput")
# TODO: iterate over ROAs (.data.validatingRoas[]) to give deeper source RIR insight
#found_rpkisource=$(echo "$rpki_apioutput" | jq -r '.data.validatingRoas[0].source')
case "$found_rpkivalidity" in
"VALID")
[[ "${found_rpkiroacount}" -gt 1 ]] && s="s" || s=""
rpki_output="${green}✓ VALID (${found_rpkiroacount} ROA$s found)${default}"
roavalidity_json_output="valid"
;;
"UNKNOWN")
rpki_output="${yellow}✓ UNKNOWN (no ROAs found)${default}"
roavalidity_json_output="unknown"
;;
"INVALID_ASN")
INVALID_ROA=true
roavalidity_json_output="invalid"
if [ "$found_rpkiorigin" = "0" ]; then
rpki_output="${red}❌ ${found_rpkivalidity} (no Origin authorized to announce Prefix '${found_rpkiprefix}' with Max-Length=${found_rpkimaxlength})${default}"
else
rpki_output="${red}❌ ${found_rpkivalidity} (expected Origin(s): ${found_rpkiorigin} for Prefix '${found_rpkiprefix}' with Max-Length=${found_rpkimaxlength})${default}"
fi
;;
"INVALID_LENGTH")
INVALID_ROA=true
roavalidity_json_output="invalid"
rpki_output="${red}❌ ${found_rpkivalidity} (expected Max-Length=${found_rpkimaxlength} for Prefix '${found_rpkiprefix}')${default}"
;;
esac
else
rpki_output="${yellow}? (WRONG RPKI DATA or problem accessing RIPEStat API)${default}"
roacount_json_output="-1"
roavalidity_json_output="unknown (API error)"
fi
}
IsIXP() {
# input ($1) is an IPv4/v6.
ixp_full_ix_data=""
ixp_data=""
ixp_geo=""
input_is_ipv6=false
if [ "$IXP_DETECTION" = true ]; then
echo -e "$1" | grep -q ':' && input_is_ipv6=true
# Update IXP prefixes from PeeringDB if necessary
# use the appropriate (v4/v6) IXP prefix dataset
if [ "$input_is_ipv6" = false ]; then
# if input is an IPv4, speedup lookups further by grabbing only IXP prefixes starting with the same two octets
# we can afford filtering based on the first two octets since the largest individual IXP prefix is around /20
first_octets=$(echo "${1}." | cut -d '.' -f 1,2)
# see if the PEERINGDB_CACHED_DATASETS array already an entry for this prefix's first two octets.
# If not, fetch the relevant IXP prefix dataset from PeeringDB and store it in the PEERINGDB_CACHED_DATASETS
if [ -n "${PEERINGDB_CACHED_DATASETS[$first_octets]}" ]; then
peeringdb_dataset="${PEERINGDB_CACHED_DATASETS[$first_octets]}"
else
peeringdb_ipv4_dataset=$(docurl -s "https://www.peeringdb.com/api/ixpfx?prefix__startswith=$first_octets&protocol__in=IPv4")
peeringdb_dataset="$peeringdb_ipv4_dataset"
# store the IXP dataset in the PEERINGDB_CACHED_DATASETS array
PEERINGDB_CACHED_DATASETS[$first_octets]="$peeringdb_dataset"
fi
else
# only fetch IPv6 dataset once, since we don't filter it for prefixes
if [ -z "$peeringdb_ipv6_dataset" ]; then
peeringdb_ipv6_dataset=$(docurl -s "https://www.peeringdb.com/api/ixpfx?protocol__in=IPv6")
fi
peeringdb_dataset="$peeringdb_ipv6_dataset"
fi
ixp_prefixes=$(jq -r '.data[].prefix' <<<"$peeringdb_dataset" 2>/dev/null)
# search for input prefix through PeeringDB IXP prefix list
for prefix in $ixp_prefixes; do
if echo "$1" | grepcidr -f <(echo "$prefix") &>/dev/null; then
# the IP is part of an IXP prefix
ixlan_id=$(jq -r '.data[] | select(.prefix == "'"$prefix"'") | .ixlan_id' <<<"$peeringdb_dataset")
# see if the PEERINGDB_CACHED_IXP_DATA array already has an entry with the IXP details for this ixlan_id.
# If not, fetch the full IXP data from PeeringDB and store it in the PEERINGDB_CACHED_IXP_DATA
if [ -n "${PEERINGDB_CACHED_IXP_DATA[$ixlan_id]}" ]; then
ixp_full_ix_data="${PEERINGDB_CACHED_IXP_DATA[$ixlan_id]}"
else
# Query PeeringDB to match an IXP for that prefix.
ixp_full_ix_data=$(docurl -s "https://www.peeringdb.com/api/ix/$ixlan_id")
# store the IXP data in the PEERINGDB_CACHED_IXP_DATA array
PEERINGDB_CACHED_IXP_DATA[$ixlan_id]="$ixp_full_ix_data"
fi
ixp_data=$(jq -r '.data[0].name, .data[0].name_long' <<<"$ixp_full_ix_data" | paste -sd '|' - | awk -F'|' '{print $1 " (" $2 ")"}')
ixp_geo=$(jq -r '.data[0].org.city' <<<"$ixp_full_ix_data")
ixp_state=$(jq -r '.data[0].org.state' <<<"$ixp_full_ix_data")
ixp_country=$(jq -r '.data[0].org.country' <<<"$ixp_full_ix_data")
ip_type_data=" ${lightgreybg} IXP ${default}"
[[ -n "$ixp_state" ]] && ixp_geo+=", $ixp_state"
[[ -n "$ixp_country" ]] && ixp_geo+=" ($ixp_country)"
[[ "$IS_ASN_CHILD" = true ]] && ixp_data="$ixp_data"
break
fi
done
fi
}
GetIXPresence(){
asn="$1"
ixps=""
outputix=""
json_ixps=""
netlist=$(docurl -s "https://www.peeringdb.com/api/net?asn__in=$asn" | jq -r '.data[].id')
if [ -n "$netlist" ]; then
for net in $netlist; do
if [ "$IS_ASN_CHILD" = true ] && [ "$JSON_OUTPUT" = false ]; then
ixps+=$(docurl -s "https://www.peeringdb.com/api/net/$net" | jq -r '.data[].netixlan_set[] | "\(.name)"')
else
ixps+=$(docurl -s "https://www.peeringdb.com/api/net/$net" | jq -r '.data[].netixlan_set[].name')
fi
done
if [ -n "$ixps" ]; then
for ix in $(echo "$ixps" | sort -u); do
[[ -n "$outputix" ]] && outputix+=" • "
outputix+="${blue}${ix}${default}"
done
else
outputix="${redbg} NONE ${default}"
fi
else
outputix="${redbg} NONE ${default}"
fi
if [ "$JSON_OUTPUT" = true ]; then
# json output
json_ixps=$(jq -c --slurp --raw-input 'split("\n") | map(select(length > 0))' <<<"$ixps")
else
# normal output
echo -e "$outputix"
fi
}
GetCAIDARank(){
# see if we already have CAIDA rank cached for this AS
asn="$1"
if [ -n "${CAIDARANK_CACHED_AS_DATA[$asn]}" ]; then
caida_data="${CAIDARANK_CACHED_AS_DATA[$asn]}"
else
caida_data=$(docurl -s "https://api.asrank.caida.org/dev/restful/asns/${asn}")
# store the rank in the CAIDARANK_CACHED_AS_DATA array
CAIDARANK_CACHED_AS_DATA[$asn]="$caida_data"
fi
caida_asrank_text=""
caida_asrank=$(jq -r '.data.asn.rank' <<<"$caida_data")
if [ "$caida_asrank" != "null" ]; then
caida_rir=$(jq -r '.data.asn.source' <<<"$caida_data")
case "${caida_rir}" in
"AFRINIC")
caida_rir="AFRINIC ${dim}(Africa)${default}"
;;
"APNIC")
caida_rir="APNIC ${dim}(Asia Pacific)${default}"
;;
"ARIN")
caida_rir="ARIN ${dim}(USA, Canada, many Caribbean and North Atlantic islands)${default}"
;;
"LACNIC")
caida_rir="LACNIC ${dim}(Latin America and the Caribbean)${default}"
;;
"RIPE")
caida_rir="RIPE ${dim}(Europe, the Middle East and parts of Central Asia)${default}"
;;
esac
caida_customercone=$(jq -r '.data.asn.cone.numberAsns' <<<"$caida_data")
caida_degree_total=$(jq -r '.data.asn.asnDegree.total' <<<"$caida_data")
caida_degree_customer=$(jq -r '.data.asn.asnDegree.customer' <<<"$caida_data")
caida_degree_peer=$(jq -r '.data.asn.asnDegree.peer' <<<"$caida_data")
caida_degree_provider=$(jq -r '.data.asn.asnDegree.provider' <<<"$caida_data")
if (( caida_asrank <= 10 )); then
caida_asrank_text=" ${lightgreybg} TOP 10 AS ${default}"
elif (( caida_asrank <= 35 )); then
caida_asrank_text=" ${lightgreybg} TOP 35 AS ${default}"
elif (( caida_asrank <= 100 )); then
caida_asrank_text=" ${lightgreybg} TOP 100 AS ${default}"
elif (( caida_asrank <= 500)); then
caida_asrank_text=" ${lightgreybg} TOP 500 AS ${default}"
elif (( caida_asrank <= 1000)); then
caida_asrank_text=" ${lightgreybg} TOP 1000 AS ${default}"
fi
caida_asrank_recap="${dim}#${white}${caida_asrank}${caida_asrank_text}"
else
caida_asrank="N/A"
caida_customercone="N/A"
caida_rir="N/A"
caida_customercone="N/A"
caida_degree_total="N/A"
caida_degree_provider="N/A"
caida_degree_peer="N/A"
caida_degree_customer="N/A"
caida_asrank_recap="${dim}N/A"
fi
}
GetCloudflareHijacksAndLeaks(){
# query Cloudflare Radar API for BGP hijacks or route leaks involving the target ASN in the past 12 months
asn="$1"
cf_hijacks_text="${dim}${red}N/A${default}${dim} [Cloudflare query timed out or API error]"
cf_leaks_text="${dim}${red}N/A${default}${dim} [Cloudflare query timed out or API error]"
cf_hijack_query_success=false
cf_leak_query_success=false
if [ -z "$CLOUDFLARE_TOKEN" ]; then
cf_hijacks_text="${dim}${red}N/A${default}${dim} [Cloudflare API token missing]${default}"
cf_leaks_text="${dim}${red}N/A${default}${dim} [Cloudflare API token missing]${default}"
return
fi
StatusbarMessage "Retrieving BGP hijacks and leaks history for AS${asn} (${target_asname})"
cf_hijacks_json_output=$(docurl -m 10 -s -H "Authorization: Bearer $CLOUDFLARE_TOKEN" "https://api.cloudflare.com/client/v4/radar/bgp/hijacks/events?dateRange=52w&involvedAsn=$asn")
cf_leaks_json_output=$(docurl -m 10 -s -H "Authorization: Bearer $CLOUDFLARE_TOKEN" "https://api.cloudflare.com/client/v4/radar/bgp/leaks/events?dateRange=52w&involvedAsn=$asn")
if [ -n "$cf_hijacks_json_output" ]; then
cf_hijacks_count=$(jq -r '.result_info.total_count' <<<"$cf_hijacks_json_output" 2>/dev/null)
if [ -z "$cf_hijacks_count" ]; then
StatusbarMessage
return
fi
cf_hijacks_as_hijacker_count=$(jq -r ".result.events | map(select (.hijacker_asn == $asn)) | length" <<<"$cf_hijacks_json_output" 2>/dev/null)
if [ -z "$cf_hijacks_as_hijacker_count" ]; then
StatusbarMessage
return
fi
cf_hijacks_as_victim_count=$((cf_hijacks_count - cf_hijacks_as_hijacker_count))
if [ "$cf_hijacks_count" -gt 0 ]; then
# at least one hijack incident involving this AS
[[ "$cf_hijacks_count" -gt 1 ]] && s="s" || s=""
cf_hijacks_text="${white}Involved in ${magenta}$cf_hijacks_count${white} BGP hijack incident${s}"
if [ "$cf_hijacks_as_hijacker_count" -gt 0 ] && [ "$cf_hijacks_as_victim_count" -gt 0 ]; then
# mixed situations involving this AS (both hijacker and victim)
cf_hijacks_text="${cf_hijacks_text} (of which ${red}$cf_hijacks_as_hijacker_count${white} as a hijacker and ${green}$cf_hijacks_as_victim_count${white} as a victim)${default}"
elif [ "$cf_hijacks_as_hijacker_count" -gt 0 ]; then
# this AS was always a hijacker
cf_hijacks_text="${cf_hijacks_text} ${red}(always as a hijacker)${default}"
else
# this AS was always a victim
cf_hijacks_text="${cf_hijacks_text} ${green}(always as a victim)${default}"
fi
else
# no hijack incidents involving this AS
cf_hijacks_text="${green}None${default}"
fi
fi
cf_hijack_query_success=true
if [ -n "$cf_leaks_json_output" ]; then
cf_leaks_count=$(jq -r '.result_info.total_count' <<<"$cf_leaks_json_output" 2>/dev/null)
if [ -z "$cf_leaks_count" ]; then
StatusbarMessage
return
fi
if [ "$cf_leaks_count" -gt 0 ]; then
# at least one route leak incident involving this AS
[[ "$cf_leaks_count" -gt 1 ]] && s="s" || s=""
cf_leaks_text="${white}Involved in ${yellow}$cf_leaks_count${white} BGP route leak incident${s}${default}"
else
# no route leak incidents involving this AS
cf_leaks_text="${green}None${default}"
fi
fi
cf_leak_query_success=true
StatusbarMessage
}
IPGeoRepLookup(){
if [ -n "$mtr_output" ] && [ "$DETAILED_TRACE" = false ]; then
# skip geolocation and reputation lookups for individual trace hops in non-detailed mode
return
fi
ip_geo_data=""
ip_geo_countryname=""
geo_city_json_output=""
geo_region_json_output=""
geo_country_json_output=""
geo_cc_json_output=""
ipinfo_city=""
flag_icon_cc="" # will be used in HTML reports served over HTTP
IS_ANYCAST=""
# local ipmap_city
# local ipmap_region
# local ipmap_cc
# local ipmap_country_name
# local ipmap_location_data
local ipinfo_city
local ipinfo_region
local ipinfo_cc
local ipinfo_countryname
local ipapi_city
local ipapi_region
local ipapi_cc
local ipapi_countryname
local ipapi_output
local ipapi_status
local ipapi_errmsg
StatusbarMessage "Collecting geolocation and classification data"
# fetch preferred geolocation and anycast data from ipinfo.io (pass token regardless of whether IPINFO_TOKEN is set, ipinfo API does not complain for an empty token)
ipinfo_output=$(docurl -m4 -s "https://ipinfo.io/$1?token=${IPINFO_TOKEN}")
# fetch fallback geolocation and ip type (is mobile?) data from ip-api.com
# Note: the free IP-API tier only supports unencrypted HTTP, not HTTPS
ipapi_output=$(docurl -m4 -s "http://ip-api.com/json/$1?fields=status,message,country,countryCode,regionName,city,mobile,proxy,hosting")
ipapi_status=$(jq -r '.status' <<<"$ipapi_output")
if [ "$ipapi_status" = "fail" ]; then
ipapi_errmsg=$(jq -r '.message' <<<"$ipapi_output")
ip_rep_data="${red}IP-API ERROR:${default} ${ipapi_errmsg}"
else
IS_MOBILE=$(jq -r '.mobile' <<<"$ipapi_output")
IS_PROXY=$(jq -r '.proxy' <<<"$ipapi_output")
IS_HOSTING=$(jq -r '.hosting' <<<"$ipapi_output")
fi
# ipmap_location_data=$(jq 'select (.location != null) | .location' <<<"$ipmap_output")
# if [ -n "$ipmap_location_data" ]; then
# # RIPE IPmap has (at least some) geo data about this address, check city/region
# ipmap_city=$(jq -r 'select (.cityNameAscii != null) | .cityNameAscii' <<<"$ipmap_location_data")
# ipmap_region=$(jq -r 'select (.stateName != null) | .stateName' <<<"$ipmap_location_data")
# fi
# check if ipinfo.io returned valid JSON
if jq type <<<"$ipinfo_output" &>/dev/null; then
if jq '.anycast' <<<"$ipinfo_output" | grep -q true; then
IS_ANYCAST=true
fi
ipinfo_city=$(jq -r 'select (.city != null) | .city' <<<"$ipinfo_output")
ipinfo_region=$(jq -r 'select (.region != null) | .region' <<<"$ipinfo_output")
ipinfo_cc=$(jq -r 'select (.country != null) | .country' <<<"$ipinfo_output")
ipinfo_countryname=$(jq -r ".${ipinfo_cc}" <<<"$COUNTRY_MAP_CACHE")
fi
if [ -n "$ipinfo_city" ]; then
# populate json output fields from ipinfo output
geo_city_json_output="$ipinfo_city"
geo_region_json_output="$ipinfo_region"
geo_country_json_output="$ipinfo_countryname"
geo_cc_json_output="$ipinfo_cc"
ip_geo_data="$ipinfo_city, $ipinfo_region ($ipinfo_cc)"
flag_icon_cc=$(tr '[:upper:]' '[:lower:]' <<<"$ipinfo_cc")
ip_geo_countryname="$ipinfo_countryname"
elif [ "$ipapi_status" = "success" ]; then
# ipinfo has no data about this address or returned an invalid response, fallback to ip-api.com
ipapi_city=$(jq -r '.city' <<<"$ipapi_output")
ipapi_region=$(jq -r '.regionName' <<<"$ipapi_output")
ipapi_cc=$(jq -r '.countryCode' <<<"$ipapi_output")
ipapi_countryname=$(jq -r '.country' <<<"$ipapi_output")
# populate json output fields from ip-api output
geo_city_json_output="$ipapi_city"
geo_region_json_output="$ipapi_region"
geo_country_json_output="$ipapi_countryname"
geo_cc_json_output="$ipapi_cc"
if [ -n "$ipapi_city" ] && [ -n "$ipapi_region" ] && [ -n "$ipapi_cc" ]; then
ip_geo_data="$ipapi_city, $ipapi_region ($ipapi_cc)"
ip_geo_countryname="$ipapi_countryname"
flag_icon_cc=$(tr '[:upper:]' '[:lower:]' <<<"$ipapi_cc")
elif [ -n "$ipapi_region" ] && [ -n "$ipapi_countryname" ]; then
ip_geo_data="$ipapi_region ($ipapi_countryname)"
ip_geo_countryname="$ipapi_countryname"
elif [ -n "$ipapi_countryname" ]; then
ip_geo_data="$ipapi_countryname"
ip_geo_countryname="$ipapi_countryname"
fi
fi
# IP type identification
ip_type_data=""
if [ "$IS_ANYCAST" = true ]; then
ip_type_data+=" ${yellowbg} Anycast IP ${default}"
ip_type_json_output+=",\"is_anycast\":true"
else
ip_type_json_output+=",\"is_anycast\":false"
fi
if [ "$IS_MOBILE" = true ]; then
ip_type_data+=" ${yellowbg} Mobile network IP ${default}"
ip_type_json_output+=",\"is_mobile\":true"
else
ip_type_json_output+=",\"is_mobile\":false"
fi
if [ "$IS_PROXY" = true ]; then
ip_type_data+=" ${yellowbg} Proxy host ${default}"
ip_type_json_output+=",\"is_proxy\":true"
else
ip_type_json_output+=",\"is_proxy\":false"
fi
# fetch detailed DC information (incolumitas.com Datacenter IP Address API)
incolumitas_dcdata=$(docurl -m2 -s "https://api.incolumitas.com/datacenter?ip=$1")
dcname=$(jq -r 'select (.datacenter.datacenter != null) | .datacenter.datacenter' <<<"$incolumitas_dcdata" 2>/dev/null)
dcregion=$(jq -r 'select (.datacenter.region != null) | .datacenter.region' <<<"$incolumitas_dcdata" 2>/dev/null)
if [ -n "$dcname" ]; then
# incolumitas.com has details regarding this DC
ip_type_data+=" ${yellowbg} DC ${default}${yellow} $dcname"
ip_type_json_output+=",\"is_dc\":true"
ip_type_json_output+=",\"dc_details\":{\"dc_name\":\"$dcname\""
if [ -n "$dcregion" ]; then
ip_type_data+=" ($dcregion)"
ip_type_json_output+=",\"dc_region\":\"$dcregion\""
fi
ip_type_json_output+="}"
ip_type_data+="${default}"
elif [ "$IS_HOSTING" = true ]; then
# fallback to IP-API DC detection
ip_type_data+=" ${yellowbg} Hosting/DC ${default}"
ip_type_json_output+=",\"is_dc\":true"
else
ip_type_json_output+=",\"is_dc\":false"
fi
# Reputation lookup (stopforumspam.org), noise classification (greynoise.io) and threat analysis (ipqualityscore.com)
ip_rep_data="${green}✓ NONE${default}"
json_rep="none"
json_iqs_threat_score=""
json_iqs_threat_tags=""
is_blacklisted=$(docurl -m4 -s "https://api.stopforumspam.org/api?json&ip=$1" | jq -r '.ip.appears')
if [ "$is_blacklisted" = "1" ]; then
# IP is blacklisted by StopForumSpam
json_rep="bad"
ip_rep_data="${red}❌ BAD (on stopforumspam.org)${default}"
fi
if [ -n "$IQS_TOKEN" ]; then
if [ "$is_blacklisted" = "1" ] || [ "$IQS_ALWAYS_QUERY" = true ]; then
# Lookup detailed reputation data on IPQualityScore
iqs_query_url="https://ipqualityscore.com/api/json/ip/$IQS_TOKEN/$1"
[[ -n "$IQS_CUSTOM_SETTINGS" ]] && iqs_query_url+="?$IQS_CUSTOM_SETTINGS"
iqs_output=$(docurl -m4 -s "$iqs_query_url")
iqs_success=$(jq -r '.success' <<<"$iqs_output")
if [ "$iqs_success" = true ]; then
iqs_score=$(jq -r '.fraud_score' <<<"$iqs_output")
iqs_proxy=$(jq -r '.proxy' <<<"$iqs_output")
iqs_vpn=$(jq -r '.active_vpn' <<<"$iqs_output")
iqs_tor=$(jq -r '.active_tor' <<<"$iqs_output")
iqs_recentabuse=$(jq -r '.recent_abuse' <<<"$iqs_output")
iqs_bot=$(jq -r '.bot_status' <<<"$iqs_output")
iqs_crawler=$(jq -r '.is_crawler' <<<"$iqs_output")
json_iqs_threat_score+=",\"threat_score\":\"$iqs_score\""
if [ "$iqs_score" -lt 40 ]; then
ip_rep_data="✓ GOOD"
ip_rep_color="$green"
json_rep="good"
elif [ "$iqs_score" -lt 75 ]; then
ip_rep_data="✓ AVERAGE"
ip_rep_color="$green"
json_rep="average"
elif [ "$iqs_score" -lt 85 ]; then
ip_rep_data="! SUSPICIOUS"
ip_rep_color="$yellow"
json_rep="suspicious"
else
ip_rep_data="❌ BAD"
ip_rep_color="$red"
json_rep="bad"
fi
ip_rep_data="${ip_rep_color}${ip_rep_data} (Threat Score ${iqs_score}%)${default}"
[[ "$iqs_recentabuse" = true ]] && ip_rep_data+=" ${redbg} RECENT ABUSER ${default}"; json_iqs_threat_tags+=",\"is_recent_abuser\": true"
[[ "$iqs_bot" = true ]] && ip_rep_data+=" ${redbg} BOT ${default}"; json_iqs_threat_tags+=",\"is_bot\": true"
[[ "$iqs_proxy" = true ]] && ip_rep_data+=" ${redbg} PROXY ${default}"; json_iqs_threat_tags+=",\"is_proxy\": true"
[[ "$iqs_vpn" = true ]] && ip_rep_data+=" ${redbg} VPN ${default}"; json_iqs_threat_tags+=",\"is_vpn\": true"
[[ "$iqs_tor" = true ]] && ip_rep_data+=" ${redbg} TOR EXIT NODE ${default}"; json_iqs_threat_tags+=",\"is_tor\": true"
[[ "$iqs_crawler" = true ]] && ip_rep_data+=" ${redbg} CRAWLER ${default}"; json_iqs_threat_tags+=",\"is_crawler\": true"
else
iqs_errmsg=$(jq -r '.message' <<<"$iqs_output")
ip_rep_data+=" ${redbg} ERR ${default} (IpQualityScore API said: $iqs_errmsg)"
fi
fi
fi
# GreyNoise lookup
greynoise_data=$(docurl -m5 -s "https://api.greynoise.io/v3/community/$1")
gn_noisy=false
gn_riot=""
gn_classification=""
gn_name=""
gn_json_is_knowngood=""
gn_json_is_knownbad=""
gn_json_aka=""
if [ -n "$greynoise_data" ]; then
gn_noisy=$(jq -r 'select (.noise != null) | .noise' <<<"$greynoise_data")
gn_riot=$(jq -r 'select (.riot != null) | .riot' <<<"$greynoise_data")
gn_classification=$(jq -r 'select (.classification != null) | .classification' <<<"$greynoise_data")
gn_name=$(jq -r 'select (.name != null) | .name' <<<"$greynoise_data")
if [ "$gn_name" = "unknown" ]; then
gn_name="${default}"
else
gn_json_aka="$gn_name"
gn_name="as \"$gn_name\" ${default}"
fi
if [ "$gn_riot" = "true" ] || [ "$gn_classification" = "benign" ]; then
# GreyNoise known-good
ip_rep_data="${green}✓${default} ${greenbg} KNOWN GOOD $gn_name"
json_rep="good"
gn_json_is_knowngood=true
[[ "$gn_noisy" = "true" ]] && ip_rep_data+=" ${yellowbg} SEEN SCANNING ${default}"
elif [ "$gn_classification" = "malicious" ]; then
# GreyNoise known-bad
[[ "$is_blacklisted" != "1" ]] && ip_rep_data="${red}❌ BAD${default}" # tag the IP with a bad REP, it wasn't caught by StopForumSpam earlier
ip_rep_data+=" ${redbg} SCANNER $gn_name"
json_rep="bad"
gn_json_is_knownbad=true
else
# GreyNoise not listed, or tagged as "noisy" but not explicitly good or bad
[[ "$gn_noisy" = "true" ]] && ip_rep_data+=" ${yellowbg} SEEN SCANNING ${default}"
fi
fi
StatusbarMessage
}
IPShodanLookup(){
# Shodan InternetDB lookup
if [ -n "$mtr_output" ] && [ "$DETAILED_TRACE" = false ]; then
# skip Shodan lookup for individual trace hops in non-detailed mode
return
fi
ip_shodan_cpe_data=""
ip_shodan_ports_data=""
ip_shodan_tags_data=""
ip_shodan_cve_data=""
shodan_cpes_json_output=""
shodan_ports_json_output=""
shodan_tags_json_output=""
shodan_vulns_json_output=""
StatusbarMessage "Collecting open ports, CPE and CVE data"
shodan_data=$(docurl -m5 -s "https://internetdb.shodan.io/$1" | grep -v "No information available")
if [ -n "$shodan_data" ]; then
shodan_cpes=$(jq -r '.cpes[]' <<<"$shodan_data" 2>/dev/null)
shodan_cpes_json_output=$(jq -cM 'select (.cpes | length > 0) | .cpes' <<<"$shodan_data" 2>/dev/null)
shodan_ports=$(jq -r '.ports[]' <<<"$shodan_data" 2>/dev/null)
shodan_ports_json_output=$(jq -cM 'select (.ports | length > 0) | .ports' <<<"$shodan_data" 2>/dev/null)
shodan_tags=$(jq -r '.tags[]' <<<"$shodan_data" 2>/dev/null)
shodan_tags_json_output=$(jq -cM 'select (.tags | length > 0) | .tags' <<<"$shodan_data" 2>/dev/null)
shodan_cve_count=$(jq '.vulns | length' <<<"$shodan_data" 2>/dev/null)
shodan_vulns_json_output=$(jq -cM 'select (.vulns | length > 0) | .vulns' <<<"$shodan_data" 2>/dev/null)
# fetch CPE types, possible values:
# a for Applications, h for Hardware, o for Operating Systems
for cpe in $shodan_cpes; do
cpetype=$(echo "$cpe" | cut -d ':' -f 2)
cpename=$(echo "$cpe" | cut -d ':' -f 3-)
case "${cpetype}" in
"/a")
type="APP"
;;
"/o")
type="O/S"
;;
"/h")
type="H/W"
;;
*)
type="UNK"
;;
esac
ip_shodan_cpe_data+="${white} [${blue}$type: ${white}$cpename]${default}"
done
# fetch open ports
ip_shodan_ports_data=""
for port in $shodan_ports; do
[[ -n "$ip_shodan_ports_data" ]] && ip_shodan_ports_data+="," || ip_shodan_ports_data=" ${green}Open ports:"
ip_shodan_ports_data+=" $port"
done
[[ -n "$ip_shodan_ports_data" ]] && ip_shodan_ports_data+="${default}"
# fetch Shodan tags
ip_shodan_tags_data=""
for tag in $shodan_tags; do
ip_shodan_tags_data+=" ${lightgreybg} $tag ${default}"
done
# fetch Shodan CVE count
ip_shodan_cve_data=""
if [ -n "$shodan_cve_count" ] && [ "$shodan_cve_count" != "0" ]; then
[[ "$shodan_cve_count" != "1" ]] && vulntext="VULNERABILITIES" || vulntext="VULNERABILITY"
ip_shodan_cve_data+=" ${redbg} $shodan_cve_count $vulntext FOUND ${default} (check "
[[ "$IS_ASN_CHILD" = true ]] && ip_shodan_cve_data+="Shodan)" || ip_shodan_cve_data+="https://internetdb.shodan.io/$1)"
fi
fi
StatusbarMessage
}
PwhoisListPrefixesForOrg(){
# list all IPv4 prefixes on pWhois based on an Org-Name
# $1 = Org-Name handle
[[ "$HAVE_IPCALC" = false ]] && return
org="$1"
full_org_search_data=$(whois -h whois.pwhois.org "registry org-name=$org")
orgids=$(echo -e "$full_org_search_data" | grep -i -E -B1 "Org-Name: $org$" | grep "Org-ID" | cut -d ':' -f 2- | sed 's/^ //g')
# assemble bulk pWhois query
pwhois_bulk_query="begin\n"
for orgid in $orgids; do
pwhois_bulk_query+="netblock org-id=${orgid}\n"
done
pwhois_bulk_query+="end"
# query pWhois
for prefix in $(echo -e "$pwhois_bulk_query" | ncat whois.pwhois.org 43 | grep -E "^\*>"); do
iprange=$(echo -e "$prefix" | cut -d '>' -f 2 | cut -d '|' -f 1 | tr -d ' ')
IpcalcDeaggregate "$iprange"
done
}
BGPUpstreamLookup(){
# retrieve likely upstream/transit autonomous system(s) for this IP address
final_json_output="[]"
if [ "$IS_BOGON" = true ]; then
PrintErrorAndExit "Error: IP $1 is a bogon address, cannot continue"
elif [ -z "$found_asname" ]; then
PrintErrorAndExit "Error: IP $1 is not part of an announced prefix, cannot continue"
fi
BoxHeader "Recently observed upstream/transit AS for $1"
# fetch all observed ASPATHs involving this IP address
full_json_data=$(docurl -s "https://stat.ripe.net/data/bgp-updates/data.json?resource=$1&sourceapp=nitefood-asn")
found_prefix=$(jq -r '.data.resource' <<<"$full_json_data")
updates=$(jq '.data.updates[].attrs.path' <<<"$full_json_data")
GetCAIDARank "$found_asn"
asnlist=$(jq -r '. as $aspaths | $aspaths[first((range(1;length) | select($aspaths[.] == '"$found_asn"')) - 1)] // empty' <<<"$updates" | sort | uniq -c | sort -rn)
tot=0
for line in $asnlist; do
count=$(awk '{print $1}' <<<"$line")
tot=$(( tot+count ))
done
[ -z "$asnlist" ] && total_upstreams_count=0 || total_upstreams_count=$(wc -l <<<"$asnlist")
red_upstreams_count=0
green_upstreams_count=0
yellow_upstreams_count=0
white_upstreams_count=0
dim_upstreams_count=0
[[ "$caida_asrank" = "N/A" ]] && caida_asrank=99999
if (( caida_asrank < 100 )) || (( total_upstreams_count > 50 )); then
# origin AS is in the top 100, therefore less likely to need transits (given its large customer cone),
# or the prefix is widely announced (likely also by well-connected IXP RS peers).
# Raise the thresholds to infer transit relationship status (need a higher percentage of BGP updates)
green_probability=85
yellow_probability=75
white_probability=65
else
green_probability=66
yellow_probability=33
white_probability=25
fi
# create a static list of the largest known transit ASNs.
# The list includes most of the top 35 CAIDA ranked ASNs (https://asrank.caida.org/?page_number=1&page_size=40&sort=rank)
# Note: the list excludes Hurricane Electric (AS6939) because HE is known to largely reannounce most prefixes it receives
# (even from IXP RS/SFI peers who do not subscribe a transit relationship with them)
static_transits_array=( 3356 1299 174 6762 2914 6461 6453 3257 3491 9002 1273 5511 4637 12956 7473 1239 3320 701 7018 6830 7922 )
legend="\t${red}██ most likely transit\t( very large / Tier 1 upstream AS )${default}"
legend+="\n\t${green}██ very likely transit\t( >= ${green_probability}% BGP updates from this AS )${default}"
legend+="\n\t${yellow}██ likely transit\t( >= ${yellow_probability}% BGP updates from this AS )${default}"
legend+="\n\t██ potentially transit\t( >= ${white_probability}% BGP updates from this AS )${default}"
legend+="\n\t${dim}██ unlikely transit\t( < ${white_probability}% BGP updates from this AS )${default}"
if [ "$JSON_OUTPUT" = false ]; then
echo -e "\nLegend:\n$legend\n"
echo -e "${bluebg} Target : ${default} ${blue}$1${default} ${dim}(matching prefix: ${white}$found_prefix${dim})${default}"
echo -e "${bluebg} Origin AS : ${default} ${red}[AS$found_asn] ${green}$found_asname${default}"
echo -e "${bluebg} CAIDA AS rank: ${default} ${caida_asrank_recap}${default}\n"
else
final_json_output=$(jq '. += [{ "prefix": "'"$found_prefix"'", "origin_as": "'"$found_asn"'", "origin_as_name": "'"$found_asname"'", "origin_as_rank": '"$caida_asrank"' }]' <<<"$final_json_output")
final_json_output=$(jq '.[0] += {"upstreams_count": '"$total_upstreams_count"',"upstreams":[]}' <<<"$final_json_output")
fi
for line in $asnlist; do
is_tier1=false
count=$(awk '{print $1}' <<<"$line")
asn=$(awk '{print $2}' <<<"$line")
# TODO use bulk Team Cymru query to lookup asnames
asname=$(host -t TXT "AS${asn}.asn.cymru.com" | grep -v "NXDOMAIN" | awk -F'|' 'NR==1{print substr($NF,2,length($NF)-2)}')
integer_probability=$(( count*100/tot ))
probability=$(jq -n "$count*100/$tot")
if grep -qw "$asn" <<<"${static_transits_array[@]}" && (( caida_asrank > 35 )); then
# apply static transit list to this AS only if it's not among the top 35 ASNs (per CAIDA rankings).
# Larger networks (ranked < 35) are more likely to arrange SFI relationships with other Tier-1 networks,
# rather than pay for transit
is_tier1=true
probability_color="${red}"
(( red_upstreams_count++ ))
elif (( integer_probability >= "$green_probability" )); then
probability_color="${green}"
(( green_upstreams_count++ ))
elif (( integer_probability >= "$yellow_probability" )); then
probability_color="${yellow}"
(( yellow_upstreams_count++ ))
elif (( integer_probability >= "$white_probability" )); then
probability_color="${default}"
(( white_upstreams_count++ ))
else
probability_color="${dim}"
(( dim_upstreams_count++ ))
fi
if [ "$JSON_OUTPUT" = true ]; then
final_json_output=$(jq '.[0].upstreams += [{"asn":"'"$asn"'", "asname":"'"$asname"'", "probability":'"$(printf "%.2f" "$probability")"', "is_tier1":'"$is_tier1"'}]' <<<"$final_json_output")
else
printf "${probability_color}██\tAS%-6s (%6.2f%%) - %s${default}\n" "$asn" "$probability" "$asname"
fi
done
# count each type of upstreams and try to determine if this prefix is multihomed
# echo "dbg green: $green_upstreams_count, yellow: $yellow_upstreams_count, white: $white_upstreams_count, dim: $dim_upstreams_count"
MULTIPLE_UPSTREAMS=false
multiple_string="multiple"
if (( total_upstreams_count >= 20 )); then
MULTIPLE_UPSTREAMS=true
multiple_string="several"
elif (( green_upstreams_count >= 2 )) || (( yellow_upstreams_count >= 2 )) || (( white_upstreams_count >= 2 )) || (( red_upstreams_count >= 2 )); then
MULTIPLE_UPSTREAMS=true
fi
if [ "$JSON_OUTPUT" = false ]; then
if [ "$MULTIPLE_UPSTREAMS" = true ]; then
echo -e "\n${bluebg} INFO ${default}${blue} This prefix seems to be reannounced by ${underline}$multiple_string ($total_upstreams_count) upstream ASNs${default}${blue}. Possible reasons include:\n\t- ${underline}BGP multihoming${default}${blue} (if few upstreams announce this prefix)\n\t- ${underline}Tier 1 origin AS${default}${blue} or highly reachable prefix${default}${blue} (if many upstreams announce this prefix)\n\t- ${underline}Anycast prefix${default}\n"
fi
else
json_resultcount="1"
final_json_output=$(jq '.[0] += {"multiple_upstreams": '"$MULTIPLE_UPSTREAMS"'}' <<<"$final_json_output")
fi
if (( total_upstreams_count == 0 )); then
echo -e "\n${lightgreybg} Prefix has not been observed in the DFZ recently ${default}\n"
fi
}
AsnServerListener(){
DisableColors
BoxHeader "ASN Lookup Server v$ASN_VERSION on $HOSTNAME"
if [ "$ASN_DEBUG" = true ]; then
server_user_uid=$(id -u)
server_user_name=$(id -n -u "$server_user_uid")
echo -e "\n- ${yellow}[DBG]${default} Server UID : ${blue}${server_user_uid} (${server_user_name})${default}" >&2
echo -en "- ${yellow}[DBG]${default} Server BIND_ADDR :" >&2
[[ -z "$ASN_SRV_BINDADDR" ]] && echo -en " ${green}not specified (default v4/v6)${default}" || echo -en " $ASN_SRV_BINDADDR" >&2
echo -en "\n- ${yellow}[DBG]${default} Server BIND_PORT : $ASN_SRV_BINDPORT" >&2
[[ "$ASN_SRV_BINDPORT" = "$DEFAULT_SERVER_BINDPORT" ]] && echo -en " ${green}(default)${default}" >&2
echo -e "\n- ${yellow}[DBG]${default} Ncat options : '${blue}${userinput}${default}'\n" >&2
fi
# fetch external IP and ASN to include in the HTML reports
StatusbarMessage "Detecting host external IP and ASN"
WhatIsMyIP
if [ "$HAVE_IPV6" = true ]; then
found_asn=$(docurl -s "https://stat.ripe.net/data/whois/data.json?resource=$local_wanip&sourceapp=nitefood-asn" | jq -r '.data.irr_records[0] | map(select(.key | match ("origin"))) | .[].value')
WhoisASN "$found_asn"
[[ -z "$ASN_SRV_BINDADDR" ]] && ASN_SRV_BINDADDR="$DEFAULT_SERVER_BINDADDR_v6"
else
LookupASNAndRouteFromIP "$local_wanip"
[[ -z "$ASN_SRV_BINDADDR" ]] && ASN_SRV_BINDADDR="$DEFAULT_SERVER_BINDADDR_v4"
fi
if [ -z "$found_asn" ]; then
found_asn="N/A"
found_asname="(Unknown)"
fi
server_country=$(echo "${found_asname##*,}" | tr -d ' ')
[[ -z "$server_country" ]] && server_country="(Unknown)"
BOOKMARKLET_URL=""
INTERNAL_ASNSERVER_ADDRESS=""
DISPLAY_ASN_SRV_BINDADDR=""
CLOUD_SHELL_MARK="${red}❌${default}"
# detect if we're running in Google Cloud Shell environment
if [ "$GOOGLE_CLOUD_SHELL" = true ] && [ -n "$WEB_HOST" ]; then
# on Google Cloud Shell, the $WEB_HOST environment variable contains the external hostname to reach the server
# the format is https://- (cheers https://stackoverflow.com/a/70255668)
INTERNAL_ASNSERVER_ADDRESS="${ASN_SRV_BINDPORT}-${WEB_HOST}"
BOOKMARKLET_URL="https://${INTERNAL_ASNSERVER_ADDRESS}/asn_bookmarklet"
CLOUD_SHELL_MARK="${green}✓${default}"
else
# Handle all non-GCP cases
# prepare the bookmarklet URL and the appropriate notation for the server's bind address:port
if [ "$ASN_SRV_BINDADDR" = "0.0.0.0" ]; then
# discover our IPv4 WAN IP if we already determined we're on an IPv6-capable host but the user wants to bind to 0.0.0.0
[[ "$HAVE_IPV6" = true ]] && WhatIsMyIP "true"
if grep -q ':' <<<"$local_wanip"; then
INTERNAL_ASNSERVER_ADDRESS="[$local_wanip]:$ASN_SRV_BINDPORT"
else
INTERNAL_ASNSERVER_ADDRESS="$local_wanip:$ASN_SRV_BINDPORT"
fi
elif [ "$ASN_SRV_BINDADDR" = "::" ]; then
if [ "$HAVE_IPV6" = true ]; then
INTERNAL_ASNSERVER_ADDRESS="[$local_wanip]:$ASN_SRV_BINDPORT"
DISPLAY_ASN_SRV_BINDADDR="[${ASN_SRV_BINDADDR}]"
else
# we're on an IPv4-only host, but the user wants to bind to [::] - fall back to 0.0.0.0 (INADDR_ANY)
# this is necessary to handle hosts where v6 sockets (and thus [::]) are only permitted to handle IPv6 traffic
# and do not map back to IPv4 (see https://sysctl-explorer.net/net/ipv6/bindv6only/).
# This also applies to default Docker container behavior (which will try to listen on [::] regardless of IPv6 availability)
ASN_SRV_BINDADDR="0.0.0.0"
INTERNAL_ASNSERVER_ADDRESS="$local_wanip:$ASN_SRV_BINDPORT"
fi
elif grep -q ':' <<<"$ASN_SRV_BINDADDR"; then
INTERNAL_ASNSERVER_ADDRESS="[$ASN_SRV_BINDADDR]:$ASN_SRV_BINDPORT"
DISPLAY_ASN_SRV_BINDADDR="[${ASN_SRV_BINDADDR}]"
else
INTERNAL_ASNSERVER_ADDRESS="$ASN_SRV_BINDADDR:$ASN_SRV_BINDPORT"
fi
fi
[[ -z "$BOOKMARKLET_URL" ]] && BOOKMARKLET_URL="http://${INTERNAL_ASNSERVER_ADDRESS}/asn_bookmarklet"
[[ -z "$DISPLAY_ASN_SRV_BINDADDR" ]] && DISPLAY_ASN_SRV_BINDADDR="$ASN_SRV_BINDADDR"
StatusbarMessage
if [ "$HAVE_IPV6" = true ]; then
[[ "$IS_HEADLESS" = true ]] && ipv6_mark="YES" || ipv6_mark="${green}✓${default}"
else
[[ "$IS_HEADLESS" = true ]] && ipv6_mark="NO" || ipv6_mark="${red}❌${default}"
fi
# prepare API tokens status line
[[ "$json_IQS_TOKEN" = true ]] && API_TOKENS_STATUS="${green}✓ IQS${default}" || API_TOKENS_STATUS="${red}❌ IQS${default}"
[[ "$json_IPINFO_TOKEN" = true ]] && API_TOKENS_STATUS+=" • ${green}✓ IPINFO${default}" || API_TOKENS_STATUS+=" • ${red}❌ IPINFO${default}"
[[ "$json_CLOUDFLARE_TOKEN" = true ]] && API_TOKENS_STATUS+=" • ${green}✓ CLOUDFLARE${default}" || API_TOKENS_STATUS+=" • ${red}❌ CLOUDFLARE${default}"
echo -e "\n- Server ext. IP : ${blue}${local_wanip}${default}" \
"\n- Server Country : ${blue}${server_country}${default}" \
"\n- Server ASN : ${red}[AS${found_asn}]${default} ${green}$found_asname${default}" \
"\n- Server has IPv6 : ${ipv6_mark}" \
"\n- Running on GCP : ${CLOUD_SHELL_MARK}" \
"\n- API Tokens : ${API_TOKENS_STATUS}" \
"\n- Bookmarklet URL : ${BOOKMARKLET_URL}" \
"\n\n[$(date +"%F %T")] ${bluebg} INFO ${default} ASN Lookup Server listening on ${white}${DISPLAY_ASN_SRV_BINDADDR}:${ASN_SRV_BINDPORT}${default}"
server_country="$(echo -e "$server_country" | tr '[:upper:]' '[:lower:]')"
trap '
[[ "$IS_HEADLESS" = false ]] && echo -en "\r" >&2
echo -e "[$(date +"%F %T")] ${bluebg} INFO ${default} ASN Lookup Server requested shutdown, terminating..." >&2
exit 0
' INT TERM
#* Assemble the ncat listener command. Also pass on MONOCHROME/DEBUG preferences
#* to the listener child (which will output to stderr on server's console)
#* and print information about client requests (1 child spawned per client connected)
read -r -d '' ncat_cmd <<- END_OF_NCAT_CMD
ncat -k -l $ASN_SRV_BINDADDR $ASN_SRV_BINDPORT $userinput --sh-exec "
export NCAT_REMOTE_ADDR; \
found_asn=\"$found_asn\" \
found_asname=\"$found_asname\" \
server_country=\"$server_country\" \
INTERNAL_CONNHANDLER_CHILD=false \
INTERNAL_ASNSERVER_CONNHANDLER=true \
INTERNAL_ASNSERVER_ADDRESS="$INTERNAL_ASNSERVER_ADDRESS" \
MONOCHROME_MODE=\"$MONOCHROME_MODE\" \
ASN_DEBUG=\"$ASN_DEBUG\" \
\"$0\"
"
END_OF_NCAT_CMD
#! Start the ncat listener and serve each client request
#! by respawning the script with $INTERNAL_ASNSERVER_CONNHANDLER => true
#! for every incoming connection
eval "$ncat_cmd"
PrintErrorAndExit "ERROR: The ncat server crashed or couldn't start. Try passing the '-v' option to see precisely what is being passed to it."
}
Ctrl_C() {
if [ "$NO_ERROR_ON_INTERRUPT" = true ]; then
StatusbarMessage
tput sgr0
tput cnorm # show cursor
ShowMenu
else
PrintErrorAndExit "Interrupted"
fi
}
BoxHeader() { # cheers https://unix.stackexchange.com/a/70616
# no output if in json mode
[[ "$JSON_OUTPUT" = true ]] && return
local message="$*"
if [ "$IS_ASN_CHILD" = true ]; then
echo "BOXHEADER $message" # the BOXHEADER tag will be transformed by the ASN server into an html
element
else
if [ "$IS_HEADLESS" = true ]; then
echo "// $message //"
else
echo -e "\n${white}╭─${message//?/─}─╮\n│ ${yellow}${message}${white} │\n╰─${message//?/─}─╯"
tput sgr 0
fi
fi
}
StatusbarMessage() { # invoke without parameters to delete the status bar message
# suppress status bar displaying for headless, json or debug runs
if [ "$IS_HEADLESS" = true ] || [ "$JSON_OUTPUT" = true ] || [ "$ASN_DEBUG" = true ]; then
return
fi
if [ -n "$statusbar_message" ]; then
# delete previous status bar message
blank_line=$(printf "%.0s " $(seq "$terminal_width"))
printf "\r%s\r" "$blank_line" >&2
tput cnorm # show cursor
fi
if [ -n "$1" ]; then
statusbar_message="$1"
max_msg_size=$((terminal_width-23))
if [ "${#statusbar_message}" -gt "${max_msg_size}" ]; then
statusbar_message="${lightgreybg}${statusbar_message:0:$max_msg_size}${lightgreybg}..."
else
statusbar_message="${lightgreybg}${statusbar_message}"
fi
statusbar_message+="${lightgreybg} (press CTRL-C to cancel)...${default}"
echo -en "$statusbar_message" >&2
tput civis # hide cursor
fi
}
WhatIsMyIP() {
# if an argument was passed, force IPv4 lookup (and skip "only check once" check)
if [ -n "$1" ]; then
local_wanip=$(docurl -s -4 "https://ifconfig.co")
else
# only lookup local WAN IP once
[[ -n "$local_wanip" ]] && return
# retrieve local WAN IP (v6 takes precedence) from ifconfig.co
local_wanip=$(docurl -s "https://ifconfig.co")
fi
# handle ifconfig.co serving a captcha-type redirect page (e.g. when coming from some AWS networks)
grep -qE '^' <<<"$local_wanip" && local_wanip=$(docurl -s "https://api64.ipify.org")
# check if we default to an IPv6 internet connection (if we're forcing IPv4 on a dual-stack host, HAVE_IPV6 will be left untouched)
if echo "$local_wanip" | grep -q ':'; then
HAVE_IPV6=true
fi
}
DisableColors() {
# avoid colors for headless server runs (e.g. systemd logs/status),
# for listener runs, json output and monochrome mode
if [ "$IS_HEADLESS" = true ] || [ "$JSON_OUTPUT" = true ]; then
# disable all colors for headless/json
green=""
magenta=""
yellow=""
white=""
blue=""
red=""
black=""
lightyellow=""
lightred=""
lightblue=""
lightgreybg=""
bluebg=""
redbg=""
greenbg=""
yellowbg=""
dim=""
default=""
elif [ "$MONOCHROME_MODE" = true ]; then
# set all colors to white, leave black/white/dim/default intact
green="$white"
magenta="$white"
yellow="$white"
blue="$white"
red="$white"
lightyellow="$white"
lightred="$white"
lightblue="$white"
bluebg="$lightgreybg"
redbg="$lightgreybg"
greenbg="$lightgreybg"
yellowbg="$lightgreybg"
fi
}
CoreutilsFixup() {
# check for GNU coreutils alternatives (improve command predictability on FreeBSD/MacOS systems)
if [ -x "$(command -v gdate)" ]; then
date() { gdate "$@"; }
export -f date
fi
if [ -x "$(command -v gsed)" ]; then
sed() { gsed "$@"; }
export -f sed
fi
if [ -x "$(command -v gawk)" ]; then
awk() { gawk "$@"; }
export -f awk
fi
if [ -x "$(command -v gbase64)" ]; then
base64() { gbase64 "$@"; }
export -f base64
fi
if [ -x "$(command -v gwc)" ]; then
wc() { gwc "$@"; }
export -f wc
fi
if [ "$IS_ASN_CHILD" = true ] || [ "$IS_ASN_CONNHANDLER" = true ]; then
# suppress the tput command during headless runs
tput() { :; }
export -f tput
fi
}
IpcalcVersionCheck() {
# check for ipcalc version to accommodate both v1.0.0+ (CentOS/RHEL/Rocky 9) and v0.41+ (Debian derivatives)
IPCALC_FLAG=""
if [ "$HAVE_IPCALC" = true ]; then
ipcalc_version=$(ipcalc -v | sed 's/ipcalc //')
ipcalc_major=$(echo "$ipcalc_version" | cut -d '.' -f 1)
case "${ipcalc_major}" in
"0")
ipcalc_minor=$(echo "$ipcalc_version" | cut -d '.' -f 2)
if [ "$ipcalc_minor" -ge 5 ]; then
IPCALC_FLAG="-r"
else
HAVE_IPCALC=false
missing_tools+="\n - ipcalc"
disabled_features+="\n - CIDR deaggregation (due to incompatible ipcalc version - v0.5+ for Debian-based or v1.0.0+ for RHEL-based required, but you have v$ipcalc_version)"
fi
;;
"1")
IPCALC_FLAG="-d"
;;
esac
fi
}
IpcalcDeaggregate() {
ipcalc_parm=$(tr -d ' ' <<<"$1")
if grep -q "/" <<<"$1"; then
# consider input to be already a CIDR block, return it unchanged
echo "$1"
else
ipcalc ${IPCALC_FLAG} "$ipcalc_parm" 2>/dev/null | grep -iv "deaggregate" | awk '{print $NF}' | tail -n1
fi
}
CheckPrerequisites() {
saveIFS="$IFS"
IFS=' '
prerequisite_tools="jq whois host curl" # mandatory tools
optional_tools="nmap mtr ipcalc grepcidr ncat aha" # optional tools
missing_tools=""
disabled_features=""
HARD_FAIL=false
HAVE_IPCALC=true
HAVE_NMAP=true
IXP_DETECTION=true
UNABLE_TO_SERVE=false
# BASH version check
bash_major=$(echo "${BASH_VERSION}" | cut -d '.' -f 1)
bash_minor=$(echo "${BASH_VERSION}" | cut -d '.' -f 2)
bash_version_too_low=false
if [ "$bash_major" -lt 4 ]; then
bash_version_too_low=true
elif [ "$bash_major" -eq 4 ] && [ "$bash_minor" -lt 2 ]; then
bash_version_too_low=true
fi
[[ "$bash_version_too_low" = true ]] && PrintErrorAndExit "Error: BASH version must be >= 4.2 (you are running v${BASH_VERSION})"
# Mandatory tools checking (hard fail if not found)
for tool in $prerequisite_tools; do
if [ -z "$(command -v "$tool")" ]; then
missing_tools+="\n - $tool"
HARD_FAIL=true
fi
done
# Optional tools checking (no hard fail if not found, but some features disabled)
for tool in $optional_tools; do
if [ -z "$(command -v "$tool")" ]; then
missing_tools+="\n - $tool"
case "$tool" in
"mtr")
disabled_feat="AS path tracing"
MTR_TRACING=false
;;
"nmap")
disabled_feat="Recon mode (Shodan scanning)"
HAVE_NMAP=false
;;
"ipcalc")
disabled_feat="CIDR deaggregation"
HAVE_IPCALC=false
;;
"grepcidr")
disabled_feat="IXP prefix detection"
IXP_DETECTION=false
;;
"ncat"|"aha")
disabled_feat="ASN Lookup Server"
UNABLE_TO_SERVE=true
;;
esac
disabled_features+="\n - ${disabled_feat}"
fi
done
IpcalcVersionCheck
if [ -n "$missing_tools" ]; then
if [ "$JSON_OUTPUT" = false ]; then
BoxHeader "! WARNING !"
echo -e "\nThe following tools were not found on this system:" \
"${red} ${missing_tools}${default}" >&2
if [ -n "$disabled_features" ]; then
echo -e "\nThe following features will be disabled:" \
"${yellow}${disabled_features}${default}" >&2
fi
echo -e "\nPlease install the necessary prerequisite packages\nfor your system by following these instructions:" \
"\n\n>> ${blue}https://github.com/nitefood/asn#prerequisite-packages${default} <<\n" >&2
[[ "$HARD_FAIL" = true ]] && PrintErrorAndExit "Can not continue without (at least) the following tools: ${prerequisite_tools// /, }"
read -srp "${lightgreybg}Press ENTER to continue...${default}" >&2
else
[[ "$HARD_FAIL" = true ]] && PrintErrorAndExit "Can not continue without (at least) the following tools: ${prerequisite_tools// /, }"
fi
fi
IQS_TOKEN="" ; json_IQS_TOKEN="false"
IPINFO_TOKEN="" ; json_IPINFO_TOKEN="false"
CLOUDFLARE_TOKEN="" ; json_CLOUDFLARE_TOKEN="false"
IFS=$'\n'
# Read tokens token from possible config files on disk
for asn_config_file in $(tr ':' '\n' <<<"$IQS_TOKEN_FILES"); do
if [ -r "$asn_config_file" ]; then
IQS_TOKEN=$(tr -d ' \n\r\t' < "$asn_config_file")
json_IQS_TOKEN="true"
break
fi
done
for asn_config_file in $(tr ':' '\n' <<<"$IPINFO_TOKEN_FILES"); do
if [ -r "$asn_config_file" ]; then
IPINFO_TOKEN=$(tr -d ' \n\r\t' < "$asn_config_file")
json_IPINFO_TOKEN="true"
break
fi
done
for asn_config_file in $(tr ':' '\n' <<<"$CLOUDFLARE_TOKEN_FILES"); do
if [ -r "$asn_config_file" ]; then
CLOUDFLARE_TOKEN=$(tr -d ' \n\r\t' < "$asn_config_file")
json_CLOUDFLARE_TOKEN="true"
break
fi
done
if [ "$JSON_OUTPUT" = false ]; then
if [ -z "$IQS_TOKEN" ] || [ -z "$IPINFO_TOKEN" ] || [ -z "$CLOUDFLARE_TOKEN" ]; then
# warn the user about the absence of external API token(s)
if [ "$IS_HEADLESS" = true ]; then
line="------------------------------------------------------------"
echo -e "\n${line}\nWARNING: At least one external (free) API token is missing." \
"\nCheck https://github.com/nitefood/asn#api-tokens for\ninstructions on how to enable them." \
"\n${line}" >&2
else
line="────────────────────────────────────────────────────"
echo -e "\n${dim}${yellow}${line}" \
"\nWarning: ${dim}${yellow}At least one external API token is missing." \
"\nTo enable full script functionalities, please visit" \
"\n${blue}https://github.com/nitefood/asn#api-tokens" >&2
echo -e "${yellow}${line}${default}" >&2
fi
fi
if [ "$ASN_DEBUG" = true ]; then
echo ""
for token in "IQS_TOKEN" "IPINFO_TOKEN" "CLOUDFLARE_TOKEN"; do
if [ -z "${!token}" ]; then
DebugPrint "${dim}${white}$token: ${red}❌ NOT FOUND${white}"
else
DebugPrint "${dim}${white}$token: ${green}✓ OK${white}"
fi
done
fi
fi
CoreutilsFixup
IFS="$saveIFS"
}
DebugPrint(){
# Debug print helper function. will display debug string in terminal mode, or full client debug info in headless mode. Will append output to $ASN_LOGFILE logfile
if [ "$ASN_DEBUG" = true ]; then
# strip CRLFs
dbgstring=$(echo -e "$1" | tr -d '\r\n')
if [ "$IS_HEADLESS" = false ]; then
# command line tool mode
echo -e "${default}[$(date +'%F %T')] ${lightgreybg} DEBUG ${default} $dbgstring" >&2
echo -e "\n────────────────────────────────────────────────────────────────────────────────────────────────────────" \
"\n>>>> ${default}[$(date +'%F %T')] ${lightgreybg} DEBUG ${default} $dbgstring" >> "$ASN_LOGFILE"
else
# server mode
if [ -z "$host" ]; then
target='N/A'
else
target="$host"
fi
if [ -z "$reqid" ]; then
requestid='N/A'
else
requestid="$reqid"
fi
echo -e "${default}[$(date +'%F %T')] ${lightgreybg} DEBUG ${default} $dbgstring [CLIENT: ${yellow}$NCAT_REMOTE_ADDR${default}, " \
"TARGET: ${magenta}$target${default}, REQID: ${blue}$requestid${default}]" >&2 \
echo -e "${default}[$(date +'%F %T')] ${lightgreybg} DEBUG ${default} $dbgstring [CLIENT: ${yellow}$NCAT_REMOTE_ADDR${default}, " \
"TARGET: ${magenta}$target${default}, REQID: ${blue}$requestid${default}]" >> "$ASN_LOGFILE"
fi
fi
}
StripAnsi() {
# portable ANSI colors strip helper fn (for termbin sharing) - cheers https://unix.stackexchange.com/a/140255
# shellcheck disable=SC2001
echo -e "$1" | sed "s,$(printf '\033')\\[[0-9;]*[a-zA-Z],,g"
}
HandleNcatClientConnection() {
# ╭─────────────────────────────────────╮
# │ MAIN NCAT CLIENT CONNECTION HANDLER │
# ╰─────────────────────────────────────╯
DisableColors
# HTTP response headers
http_ok='HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n'
http_ok_json='HTTP/1.0 200 OK\r\nContent-Type: application/json\r\n'
http_ko='HTTP/1.0 400\r\nContent-Type: text/html\r\n\r\n
400 Bad Request
\nYour request could not be processed.'
http_ko_json='HTTP/1.0 400\r\nContent-Type: application/json\r\n'
# Javascript bookmarklet
js_bookmarklet='javascript:(function(){var asnserver="'"${INTERNAL_ASNSERVER_ADDRESS}"'",target=window.location.hostname,'
js_bookmarklet+='width=screen.width-screen.width/7,height=screen.height-screen.height/4,left=window.innerWidth/2-width/2,top=window.innerHeight/2-height/2;'
js_bookmarklet+='window.open("http://"+asnserver+"/asn_lookup&"+target,"newWindow","width="+width+",height="+height+",top="+top+",left="+left)})();'
# HTML bookmarklet page
read -r -d '' html_bookmarklet_page <<- END_OF_BOOKMARKLET_HTML
[ASN] Server configuration (client-side)
- ASN Server // Browser Integration -1) Drag and drop the yellow link below to your bookmarks toolbar:ASN LOOKUP2) Close this page and click the new bookmark while viewing any website.
A lookup and trace for that host should start in a pop-up window. ASN Lookup Server v${ASN_VERSION} running on $HOSTNAME
'
# HTML closing tags
html_footer='
'
CoreutilsFixup
# a client just connected
echo -e "[$(date +'%F %T')] ${bluebg} INFO ${default} Incoming connection by client ${yellow}$NCAT_REMOTE_ADDR${default}" >&2
# read input from client (URL being accessed through the client browser)
read -r line
DebugPrint "RECEIVED new client request: '$line'"
# handle 'asn_bookmarklet' command. This will show a web page for easy dragging&dropping of the bookmarklet to the favorites toolbar
if (echo -e "$line" | grep -Eq "^GET /asn_bookmarklet[?& ]"); then
DebugPrint "SERVING bookmarklet page to client"
echo -e "${http_ok}${html_bookmarklet_page}"
# handle 'termbin_share' command. This will decode the input and send it to termbin, returning a html link to the client
elif (echo -e "$line" | grep -Eq "^GET /termbin_share&"); then
# send HTTP 200 OK header to client
echo -e "${http_ok}"
# decode input data
input_data="$(echo -e "$line" | cut -d '&' -f 2- | cut -d ' ' -f 1 | base64 -d | gunzip)"
termbin_url=""
if [ -n "$input_data" ]; then
# share on termbin.com and output the url to the client
termbin_url="$(echo -e "$input_data" | timeout 10 ncat termbin.com 9999 | tr -d '\r\n\0')" # sanitize trailing characters in termbin output link
fi
# analyze the sharing attempt result
if [ -n "$termbin_url" ]; then
# sharing successful, output the link
echo -e "${termbin_url}\n"
# log the successful sharing attempt
echo -e "[$(date +'%F %T')] ${greenbg} SHAREOK ${default} Successfully shared termbin link ${blue}${termbin_url}${default} with client ${yellow}$NCAT_REMOTE_ADDR${default}" >&2
exit 0
elif [ -z "$input_data" ]; then
sharefailreason="invalid input data"
elif [ -z "$termbin_url" ]; then
sharefailreason="termbin.com error"
else
sharefailreason="unknown reason"
fi
# log the failed sharing attempt
echo -e "[$(date +'%F %T')] ${redbg} SHAREKO ${default} Error sharing termbin link with client ${yellow}$NCAT_REMOTE_ADDR${default} ($sharefailreason)" >&2
# close the client connection without any output
exit 1
elif (grep -Eq "^GET /asn_lookup(_json[p]?)?&" <<<"$line"); then
OUTPUT_TYPE=""
starttime=$(date +'%s')
startdatetime=$(date +'%F %T')
reportdatetime=$(date +'%F %T (%Z)')
# detect output method requested by the user
method=$(echo -e "$line" | cut -d '&' -f 1)
if grep -q "jsonp" <<<"$method"; then
# user requested pretty-print json output from the server
OUTPUT_TYPE="jsonp"
elif grep -q "json" <<<"$method"; then
# user requested compact json output from the server
OUTPUT_TYPE="json"
fi
# handle 'asn_lookup' command. This will start a lookup for the target.
# 1) trim down target length to at most 100 characters
# 2) extract target from client request (cut on ampersand first, space next, e.g.: 'GET /asn_lookup&google.com HTTP/1.1' should return google.com)
# 3) convert any '%3A' to ':' (for IPv6 searches)
# 4) convert any '%2F' to '/' (for URL searches)
# 5) strip leading slash (/google.com -> google.com)
# 6) sanitize by stripping any character except letters, numbers, and dot/colon/dash [.:-])
target=$(echo -e "$line" | cut -d '&' -f 2- | awk -F' ' '{print substr($1,0,100)}')
host=$(echo -e "$target" | sed -e 's/%3[aA]/:/g' -e 's/%2[fF]/\//g' -e 's/^\///' -e 's/[^0-9a-zA-Z/\.:-]//g')
# validate target by checking if it's an:
# 1) IP address
# 2) hostname (beginning with any alphanumeric character and including at least one dot '.')
# 3) AS number (with or without case-insensitive 'AS' prefix)
is_valid_target=$(echo -e "$host" | grep -Eo "$ipv4v6regex|^([0-9a-zA-Z])+.*\.|^([aA][sS])?[0-9]{1,6}$")
# avoid invalid/malformed targets
if [ -n "$is_valid_target" ]; then
# this is a valid client request, set up request identifier
reqid=$(date +'%N')
echo -e "[$startdatetime] ${yellowbg} STARTED ${default} Lookup request by client ${yellow}$NCAT_REMOTE_ADDR${default} for target ${magenta}$host${default} (Request ID: ${blue}$reqid${default})" >&2
if [ "$OUTPUT_TYPE" = "json" ] || [ "$OUTPUT_TYPE" = "jsonp" ]; then
# send HTTP 200 OK response with JSON Content-Type
echo -e "${http_ok_json}"
else
# send HTTP 200 OK response and display HTML headers
echo -e "${http_ok}${html_header}"
fi
countryname=$(docurl -m2 -s "https://restcountries.com/v3.1/alpha/$server_country" | jq -r '.[].name.common')
if [ -n "$countryname" ] && [ "$countryname" != "null" ]; then
title_tag="title='$countryname'"
else
title_tag=""
fi
if [ -z "$OUTPUT_TYPE" ]; then
# default HTML mode, display ASN server info table
echo -e "
END_OF_MODAL_HTML
outputline=$(echo -e "$outputline" | sed -e 's/#PERFORMWHOIS.*//')
tracedata+="${outputline}\n"
outputline+="($whoisbutton•$hostio_link )"
printf "%s\n" "$outputline" | aha -b -n | sed -e 's/<//g' -e 's/"/"/g' -e 's/&/&/g'
else
tracedata+="$outputline\n"
printf "%s\n" "$outputline" | aha -b -n
fi
printf "" # cheers https://stackoverflow.com/a/55471426
done
#* Child asn instance finished, finish up the html
# append text footer to tracedata (will only be displayed when sharing results on termbin)
footermsg="Report generated by ASN Lookup Server v${ASN_VERSION} (https://github.com/nitefood/asn) on $reportdatetime"
footermsglen=${#footermsg}
footerhr=$(printf "%150s" "" | tr " " "_")
tracedata+="$(printf "%s\n\n%*s\n" "$footerhr" $(((footermsglen+150)/2)) "$footermsg")"
# print closing headers
endtime=$(date +%s)
runtime=$((endtime-starttime))
echo -e "
" \
"" \
"" \
"Generated by ASN Lookup Server v${ASN_VERSION} running on $HOSTNAME in ${runtime}s (request ID: $reqid)" \
"" \
"
" \
"
" \
"What is that?" \
"\nThis report will be shared on Termbin (a Pastebin-like sharing service for terminal data).\nTermbin pastes have a lifespan of 1 week." \
"
" \
""
# create a hidden div with the gzipped+b64 encoded trace output for optional termbin sharing
tracedata_encoded="$(StripAnsi "$tracedata" | gzip | base64 -w0)"
echo -e "
${tracedata_encoded}
"
echo -e "$html_footer"
fi
}
# end the ncat client connection for this request ID
childretval="$?"
if [ "$childretval" -eq 0 ]; then
DebugPrint "Child process completed successfully"
[[ -z "$OUTPUT_TYPE" ]] && echo -e ""
# log lookup request completed on server side
echo -e "[$(date +"%F %T")] ${greenbg}COMPLETED${default} Lookup request by client ${yellow}$NCAT_REMOTE_ADDR${default} for target ${magenta}$host${default} (Request ID: ${blue}$reqid${default})" >&2
else
DebugPrint "Child process failed with exit code $childretval (SIG$(kill -l $childretval))"
echo -e "[$(date +"%F %T")] ${redbg} FAILED ${default} Lookup request by client ${yellow}$NCAT_REMOTE_ADDR${default} for target ${magenta}$host${default} failed with exit code $childretval (SIG$(kill -l $childretval)) (Request ID: ${blue}$reqid${default})" >&2
fi
if [ "$childretval" -eq 141 ]; then
DebugPrint "Client has gone away?"
fi
else
# send HTTP code 400 Bad Request to client
if [ "$JSON_OUTPUT" = false ]; then
echo -e "$http_ko"
else
echo -e "$http_ko_json"
userinput="$host"
json_resultcount=0
final_json_output=""
PrintErrorAndExit "bad request"
fi
# log lookup request ignored on server side
echo -e "[$(date +"%F %T")] ${redbg} IGNORED ${default} Malformed data in request by client ${yellow}$NCAT_REMOTE_ADDR${default} for target ${magenta}$host${default}" >&2
fi
else
# ignore spurious browser requests (e.g. 'GET /favicon.ico') or scans.
# Log the bad request on server side, sleep for 1 second, and drop the connection
reqbytes=$(echo -e "$line" | awk -F$'\r\n' '{print substr($1,0,30)}')
echo -e "[$(date +"%F %T")] ${redbg} IGNORED ${default} Ignored request by client ${yellow}$NCAT_REMOTE_ADDR${default}. Request data (first 30 bytes): ${red}${reqbytes}${default}" >&2
sleep 1
fi
}
#! ╭───────────────────────╮
#! │ Main asn script start │
#! ╰───────────────────────╯
IFS=$'\n\t'
# Color scheme
green=$'\e[38;5;035m'
magenta=$'\e[38;5;207m'
yellow=$'\e[38;5;142m'
white=$'\e[38;5;007m'
blue=$'\e[38;5;038m'
red=$'\e[38;5;203m'
black=$'\e[38;5;016m'
lightyellow=$'\e[38;5;220m'
lightred=$'\e[38;5;167m'
lightblue=$'\e[38;5;109m'
lightgreybg=$'\e[48;5;252m'${black}
bluebg=$'\e[48;5;038m'${black}
redbg=$'\e[48;5;210m'${black}
greenbg=$'\e[48;5;035m'${black}
yellowbg=$'\e[48;5;142m'${black}
dim=$'\e[2m'
underline=$'\e[4m'
default=$'\e[0m'
# HTML color codes matching ANSI colors used by the script
htmlwhite="#cccccc"
htmlblack="#1e1e1e"
htmllightgray="#d5d5d5"
htmlred="#ff5f5f"
htmldarkred="#b74d4d"
htmlblue="#00afd7"
htmlyellow="#afaf00"
htmlgreen="#00af5f"
htmldarkgreen="#058505"
htmlmagenta="#ff5fff"
[[ "$TERM" = "dumb" ]] && IS_HEADLESS=true || IS_HEADLESS=false
ipv4v6regex='[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|'\
'([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|'\
'([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|'\
':((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|'\
'(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|'\
'1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))' # cheers https://stackoverflow.com/a/17871737
# Get terminal width. If running headless in a child instance (or in server mode) set it to a "sane" value to display appropriate HTML spacing in reports
if [ "$IS_HEADLESS" = true ]; then
terminal_width=233
else
terminal_width=$(tput cols)
trap 'terminal_width=$(tput cols)' SIGWINCH
fi
# Check if this script instance was launched by the ASN server connection handler
if [ "$INTERNAL_CONNHANDLER_CHILD" = true ]; then
IS_ASN_CHILD=true
else
IS_ASN_CHILD=false
fi
# Check if this script instance was spawned by the ncat listener
if [ "$INTERNAL_ASNSERVER_CONNHANDLER" = true ]; then
IS_ASN_CONNHANDLER=true
HandleNcatClientConnection
exit 0
else
IS_ASN_CONNHANDLER=false
fi
# External API tokens for ipqualityscore.com (IP reputation & threat analysis lookup),
# ipinfo.io (IP geolocation lookup) and Cloudflare Radar (BGP hijacks and route leaks historical data)
# Files will be parsed in the order they are declared (first path found takes precedence)
IQS_TOKEN_FILES="$HOME/.asn/iqs_token:/etc/asn/iqs_token"
IPINFO_TOKEN_FILES="$HOME/.asn/ipinfo_token:/etc/asn/ipinfo_token"
CLOUDFLARE_TOKEN_FILES="$HOME/.asn/cloudflare_token:/etc/asn/cloudflare_token"
# SIGINT trapping
NO_ERROR_ON_INTERRUPT=false
trap Ctrl_C INT
# PeeringDB list of IXP prefixes
peeringdb_dataset=""
peeringdb_ipv6_dataset=""
# Well known ports list
[[ -r "/etc/services" ]] && WELL_KNOWN_PORTS=$(cat /etc/services) || WELL_KNOWN_PORTS=""
# default options (configurable via $HOME/.asnrc)
MTR_TRACING=true
ADDITIONAL_INETNUM_LOOKUP=true
DETAILED_TRACE=false
MTR_ROUNDS=5
MAX_CONCURRENT_SHODAN_REQUESTS=10
SHODAN_SHOW_TOP_N=5
MONOCHROME_MODE=false
ASN_DEBUG=false
JSON_OUTPUT=false
JSON_PRETTY=false
DEFAULT_SERVER_BINDADDR_v4="127.0.0.1"
DEFAULT_SERVER_BINDADDR_v6="::1"
DEFAULT_SERVER_BINDPORT="49200"
IQS_ALWAYS_QUERY=false
IQS_CUSTOM_SETTINGS="" # e.g. "strictness=1&allow_public_access_points=false" - see https://www.ipqualityscore.com/documentation/proxy-detection/overview -> "Note About Front End IP Lookups"
declare -A PEERINGDB_CACHED_DATASETS
declare -A PEERINGDB_CACHED_IXP_DATA
declare -A CAIDARANK_CACHED_AS_DATA
# to build the CC->CountryName mapping cache:
# COUNTRY_MAP_CACHE=$(docurl -m4 -s "https://restcountries.com/v3.1/all?fields=name,cca2" | jq -c 'map({(.cca2): .name.common}) | add | to_entries | sort_by(.key) | from_entries')
COUNTRY_MAP_CACHE='{"AD":"Andorra","AE":"United Arab Emirates","AF":"Afghanistan","AG":"Antigua and Barbuda","AI":"Anguilla","AL":"Albania","AM":"Armenia","AO":"Angola",
"AQ":"Antarctica","AR":"Argentina","AS":"American Samoa","AT":"Austria","AU":"Australia","AW":"Aruba","AX":"Åland Islands","AZ":"Azerbaijan","BA":"Bosnia and Herzegovina",
"BB":"Barbados","BD":"Bangladesh","BE":"Belgium","BF":"Burkina Faso","BG":"Bulgaria","BH":"Bahrain","BI":"Burundi","BJ":"Benin","BL":"Saint Barthélemy","BM":"Bermuda",
"BN":"Brunei","BO":"Bolivia","BQ":"Caribbean Netherlands","BR":"Brazil","BS":"Bahamas","BT":"Bhutan","BV":"Bouvet Island","BW":"Botswana","BY":"Belarus","BZ":"Belize","CA":"Canada",
"CC":"Cocos (Keeling) Islands","CD":"DR Congo","CF":"Central African Republic","CG":"Republic of the Congo","CH":"Switzerland","CI":"Ivory Coast","CK":"Cook Islands","CL":"Chile",
"CM":"Cameroon","CN":"China","CO":"Colombia","CR":"Costa Rica","CU":"Cuba","CV":"Cape Verde","CW":"Curaçao","CX":"Christmas Island","CY":"Cyprus","CZ":"Czechia","DE":"Germany",
"DJ":"Djibouti","DK":"Denmark","DM":"Dominica","DO":"Dominican Republic","DZ":"Algeria","EC":"Ecuador","EE":"Estonia","EG":"Egypt","EH":"Western Sahara","ER":"Eritrea","ES":"Spain",
"ET":"Ethiopia","FI":"Finland","FJ":"Fiji","FK":"Falkland Islands","FM":"Micronesia","FO":"Faroe Islands","FR":"France","GA":"Gabon","GB":"United Kingdom","GD":"Grenada",
"GE":"Georgia","GF":"French Guiana","GG":"Guernsey","GH":"Ghana","GI":"Gibraltar","GL":"Greenland","GM":"Gambia","GN":"Guinea","GP":"Guadeloupe","GQ":"Equatorial Guinea",
"GR":"Greece","GS":"South Georgia","GT":"Guatemala","GU":"Guam","GW":"Guinea-Bissau","GY":"Guyana","HK":"Hong Kong","HM":"Heard Island and McDonald Islands","HN":"Honduras",
"HR":"Croatia","HT":"Haiti","HU":"Hungary","ID":"Indonesia","IE":"Ireland","IL":"Israel","IM":"Isle of Man","IN":"India","IO":"British Indian Ocean Territory","IQ":"Iraq",
"IR":"Iran","IS":"Iceland","IT":"Italy","JE":"Jersey","JM":"Jamaica","JO":"Jordan","JP":"Japan","KE":"Kenya","KG":"Kyrgyzstan","KH":"Cambodia","KI":"Kiribati","KM":"Comoros",
"KN":"Saint Kitts and Nevis","KP":"North Korea","KR":"South Korea","KW":"Kuwait","KY":"Cayman Islands","KZ":"Kazakhstan","LA":"Laos","LB":"Lebanon","LC":"Saint Lucia",
"LI":"Liechtenstein","LK":"Sri Lanka","LR":"Liberia","LS":"Lesotho","LT":"Lithuania","LU":"Luxembourg","LV":"Latvia","LY":"Libya","MA":"Morocco","MC":"Monaco","MD":"Moldova",
"ME":"Montenegro","MF":"Saint Martin","MG":"Madagascar","MH":"Marshall Islands","MK":"North Macedonia","ML":"Mali","MM":"Myanmar","MN":"Mongolia","MO":"Macau",
"MP":"Northern Mariana Islands","MQ":"Martinique","MR":"Mauritania","MS":"Montserrat","MT":"Malta","MU":"Mauritius","MV":"Maldives","MW":"Malawi","MX":"Mexico","MY":"Malaysia",
"MZ":"Mozambique","NA":"Namibia","NC":"New Caledonia","NE":"Niger","NF":"Norfolk Island","NG":"Nigeria","NI":"Nicaragua","NL":"Netherlands","NO":"Norway","NP":"Nepal","NR":"Nauru",
"NU":"Niue","NZ":"New Zealand","OM":"Oman","PA":"Panama","PE":"Peru","PF":"French Polynesia","PG":"Papua New Guinea","PH":"Philippines","PK":"Pakistan","PL":"Poland",
"PM":"Saint Pierre and Miquelon","PN":"Pitcairn Islands","PR":"Puerto Rico","PS":"Palestine","PT":"Portugal","PW":"Palau","PY":"Paraguay","QA":"Qatar","RE":"Réunion","RO":"Romania",
"RS":"Serbia","RU":"Russia","RW":"Rwanda","SA":"Saudi Arabia","SB":"Solomon Islands","SC":"Seychelles","SD":"Sudan","SE":"Sweden","SG":"Singapore",
"SH":"Saint Helena, Ascension and Tristan da Cunha","SI":"Slovenia","SJ":"Svalbard and Jan Mayen","SK":"Slovakia","SL":"Sierra Leone","SM":"San Marino","SN":"Senegal","SO":"Somalia",
"SR":"Suriname","SS":"South Sudan","ST":"São Tomé and Príncipe","SV":"El Salvador","SX":"Sint Maarten","SY":"Syria","SZ":"Eswatini","TC":"Turks and Caicos Islands","TD":"Chad",
"TF":"French Southern and Antarctic Lands","TG":"Togo","TH":"Thailand","TJ":"Tajikistan","TK":"Tokelau","TL":"Timor-Leste","TM":"Turkmenistan","TN":"Tunisia","TO":"Tonga",
"TR":"Turkey","TT":"Trinidad and Tobago","TV":"Tuvalu","TW":"Taiwan","TZ":"Tanzania","UA":"Ukraine","UG":"Uganda","UM":"United States Minor Outlying Islands","US":"United States",
"UY":"Uruguay","UZ":"Uzbekistan","VA":"Vatican City","VC":"Saint Vincent and the Grenadines","VE":"Venezuela","VG":"British Virgin Islands","VI":"United States Virgin Islands",
"VN":"Vietnam","VU":"Vanuatu","WF":"Wallis and Futuna","WS":"Samoa","XK":"Kosovo","YE":"Yemen","YT":"Mayotte","ZA":"South Africa","ZM":"Zambia","ZW":"Zimbabwe"}'
#*
#* Parse command line options
#*
if [[ $# -lt 1 ]]; then
PrintUsage
exit 1
fi
# read optional preferences from "$HOME/.asnrc" (only for non-headless runs)
if [ "$IS_HEADLESS" = false ]; then
rcfile="/${HOME}/.asnrc"
if [ -r "$rcfile" ]; then
# shellcheck disable=SC1090
. "$rcfile"
fi
fi
status_json_output="ok"
reason_json_output="success"
json_request_time=$(date "+%Y-%m-%dT%H:%M:%S")
starttime="$(date +%s)"
final_json_output=""
json_target_type="unknown"
json_resultcount=0
# optspec contains:
# - options followed by a colon: parameter is mandatory
# - first colon: disable getopts' own error reporting
# in this mode, getopts sets optchar to:
# '?' -> unknown option
# ':' -> missing mandatory parameter to the option
optspec=":hvmljJsgn:t:d:o:a:c:u:"
FORCE_ORGSEARCH=false
SUGGEST_SEARCH=false
SERVER_MODE=false
RECON_MODE=false
COUNTRY_BLOCK_MODE=false
GEOLOCATE_ONLY_MODE=false
BGP_UPSTREAM_MODE=false
OPTIONS_PRESENT=false # will be set to true if getopts enters the loop (detects an option being passed)
while getopts "$optspec" optchar; do {
GetFullParamsFromCurrentPosition(){
#
# Helper function that retrieves all the command line
# parameters starting from position $OPTIND (current
# option's argument as being parsed by getopts)
#
# 1) first param is set to current option's param (space-separated)
# 2) then append (if any exist) the following command line params.
#
# this allows for invocations such as 'asn -o NAME WITH SPACES'
# without having to quote NAME WITH SPACES
# The function requires passing the original $@ as parameter
# so as to not confuse it with the function's own $@.
#
# in the above example, $OPTARG="NAME", $OPTIND="3", ${@:$OPTIND}=array("WITH" "SPACES")
#
userinput="$OPTARG"
for option in "${@:$OPTIND}"; do
userinput+=" $option"
done
}
userinput=""
OPTIONS_PRESENT=true
case "${optchar}" in
"n")
MTR_TRACING=false
ADDITIONAL_INETNUM_LOOKUP=false
GetFullParamsFromCurrentPosition "$@"
;;
"t")
MTR_TRACING=true
GetFullParamsFromCurrentPosition "$@"
;;
"d")
MTR_TRACING=true
DETAILED_TRACE=true
GetFullParamsFromCurrentPosition "$@"
;;
"o")
FORCE_ORGSEARCH=true
GetFullParamsFromCurrentPosition "$@"
;;
"a")
SUGGEST_SEARCH=true
GetFullParamsFromCurrentPosition "$@"
;;
"s")
MTR_TRACING=false
RECON_MODE=true
GetFullParamsFromCurrentPosition "$@"
;;
"c")
COUNTRY_BLOCK_MODE=true
GetFullParamsFromCurrentPosition "$@"
;;
"g")
GEOLOCATE_ONLY_MODE=true
GetFullParamsFromCurrentPosition "$@"
;;
"u")
BGP_UPSTREAM_MODE=true
GetFullParamsFromCurrentPosition "$@"
;;
"m")
MONOCHROME_MODE=true
;;
"j")
MTR_TRACING=false # json output for mtr traces not implemented yet
JSON_OUTPUT=true
# GetFullParamsFromCurrentPosition "$@"
;;
"J")
MTR_TRACING=false # json output for mtr traces not implemented yet
JSON_OUTPUT=true
JSON_PRETTY=true
# GetFullParamsFromCurrentPosition "$@"
;;
"l")
SERVER_MODE=true
argcount=1
for option in "${@:$OPTIND}"; do
# one or more parameters were passed to -l, check if
# is an IP, PORT or "IP PORT" pair.
# pass the rest of the command line as direct
# ncat arguments.
# Not passing everything directly (-l has the same
# functionality in ncat) because asn should have more
# sensible defaults (i.e. bind to 127.0.0.1 and not
# 0.0.0.0 by default).
# handle special case (if invocation was 'asn -l -v ' instead of 'asn -v -l ')
[[ "$option" = "-v" ]] && { ASN_DEBUG=true; continue; }
if [ "$argcount" -eq 1 ]; then
listen_opt1="$option"
[[ "${listen_opt1:0:1}" = "-" ]] && { listen_opt1=""; userinput+=" $option"; argcount=99999; } # next lines will be treated as ncat options}
elif [ "$argcount" -eq 2 ]; then
listen_opt2="$option"
[[ "${listen_opt2:0:1}" = "-" ]] && { listen_opt2=""; userinput+=" $option"; argcount=99999; } # next lines will be treated as ncat options}
else
userinput+=" $option"
fi
(( argcount++ ))
done
# analyze the args
ASN_SRV_BINDADDR=""
ASN_SRV_BINDPORT=""
for passedarg in $listen_opt1 $listen_opt2; do
if grep -Eq ":|\." <<<"$passedarg"; then
# it's an IP address
ASN_SRV_BINDADDR="$passedarg"
else
# it's a port
ASN_SRV_BINDPORT="$passedarg"
fi
done
# fallback to default port if none was passed.
# The rest is already in $userinput, ncat will use it for its own args
[[ -z "$ASN_SRV_BINDPORT" ]] && ASN_SRV_BINDPORT="$DEFAULT_SERVER_BINDPORT"
# trim the leading whitespace from ncat options
userinput="${userinput#' '}"
# '-l' must be the last option (since it includes optional args), bail the getopts loop
break
;;
"h")
PrintUsage
exit 0
;;
"v")
ASN_DEBUG=true
rm -f "$ASN_LOGFILE" 2>/dev/null
;;
*)
if [ "$OPTERR" = 1 ] && [ -t 0 ]; then
[[ "$optchar" = "?" ]] && PrintUsage "Error: unknown option '-$OPTARG'"
[[ "$optchar" = ":" ]] && PrintUsage "Error: option '-$OPTARG' requires an argument"
exit 1
fi
;;
esac
}
done
if [ -t 0 ] || [ "$IS_HEADLESS" = true ] || [ -n "$PARALLEL_SEQ" ] || [ -n "$PARALLEL_PID" ]; then
# input is from the terminal (or this is a headless instance)
if [ "$OPTIONS_PRESENT" = false ]; then
# shellcheck disable=SC2124
userinput="$@"
elif [ -z "$userinput" ] && [ "$SERVER_MODE" = false ]; then
# an option was passed, but it was not one that requires a param (e.g. -m for monochrome mode)
# fetch the actual target
GetFullParamsFromCurrentPosition "$@"
fi
else
# script was invoked with input from stdin (e.g. "asn -s < iplist")
# read IP list from stdin and trim blanks and comments
userinput=$(cat | grep -Ev '^$|^[ \t]*#')
fi
# trim leading whitespace from userinput
userinput=$(awk '{ sub(/^[ \t]+/, ""); print }' <<<"$userinput")
# trim trailing newline from userinput
userinput=$(echo -en "$userinput")
# check if we still don't have a target
if [ -z "$userinput" ] && [ "$SERVER_MODE" = false ]; then
PrintUsage "Error: no target specified"
fi
[[ "$MONOCHROME_MODE" = true ]] && DisableColors
# options consistency check:
# enable JSON_OUTPUT if the user has JSON_PRETTY=true in the preferences file
# also explicitly disable tracing if the user forgot to pass "-n"
[[ "$JSON_PRETTY" = true ]] && JSON_OUTPUT=true
[[ "$JSON_OUTPUT" = true ]] && MTR_TRACING=false
#* Check prerequisite and optional tools
CheckPrerequisites
local_wanip=""
HAVE_IPV6=false
if [ "$SERVER_MODE" = true ]; then
# user passed the "-l" switch
[[ "$UNABLE_TO_SERVE" = true ]] && PrintErrorAndExit "ERROR: Can not start the listening server. Please install the necessary tools."
AsnServerListener
fi
# handle output type for child instances
# supported output types: json, jsonp
if [ -n "$INTERNAL_ASNSERVER_OUTPUT_TYPE" ]; then
MTR_TRACING=false
case "${INTERNAL_ASNSERVER_OUTPUT_TYPE}" in
"json")
JSON_OUTPUT=true
;;
"jsonp")
JSON_OUTPUT=true
JSON_PRETTY=true
;;
esac
fi
if [ "$RECON_MODE" = true ]; then
# user passed the "-s" switch
final_json_output="[]"
[[ "$HAVE_NMAP" = false ]] && PrintErrorAndExit "Nmap is required to use -s, but not available on this system"
# launch Shodan scan
ShodanRecon
if [ "$JSON_OUTPUT" = true ]; then
json_resultcount=$(jq '. | length' <<<"$shodan_json_output")
final_json_output="$shodan_json_output"
PrintJsonOutput
else
tput sgr0
fi
exit 0
fi
if [ "$GEOLOCATE_ONLY_MODE" = true ]; then
# user passed the "-g" switch
final_json_output="[]"
# launch bulk geolocation lookup
BulkGeolocate
if [ "$JSON_OUTPUT" = true ]; then
json_resultcount=$(jq '. | length' <<<"[$geolocation_json_output]")
final_json_output="$geolocation_json_output"
PrintJsonOutput
else
tput sgr0
fi
exit 0
fi
if [ "$COUNTRY_BLOCK_MODE" = true ]; then
# user passed the "-c" switch
# check if user passed a country code (.xx)
if [ "${userinput::1}" == "." ]; then
cc=$(echo "$userinput" | cut -d '.' -f 2-)
restcountries_output=$(docurl -s "https://restcountries.com/v3.1/alpha/$cc")
else
# Urlencode the spaces in user's input
countrystring=${userinput// /%20}
# Look up the country code
restcountries_output=$(docurl -s "https://restcountries.com/v3.1/name/${countrystring}")
fi
have_results=$(jq 'if type=="array" then true else false end' <<<"$restcountries_output")
[[ "$have_results" = false ]] && PrintErrorAndExit "No countries matching '$userinput'"
matches=$(jq -r '. | length' <<<"$restcountries_output")
if [ "$matches" -gt 1 ]; then
[[ "$JSON_OUTPUT" = true ]] && PrintErrorAndExit "Multiple countries found, please specify a valid country code"
echo -e "\n${yellow}Multiple countries found, please specify a valid country code:${default}\n"
for (( i=0; i<"$matches"; i++ )); do
formalname=$(jq -r ".[$i] | (.name.official)" <<<"$restcountries_output")
commonname=$(jq -r ".[$i] | (.name.common)" <<<"$restcountries_output")
cc=$(jq -r ".[$i] | (.cca2)" <<<"$restcountries_output")
printf "%-55s : ${green}%s${default} (aka: %s)\n" "$formalname" "$cc" "$commonname"
done
echo ""
tput sgr0
exit 0
fi
cc=$(jq -r 'select(.[].cca2 != null) | .[].cca2' <<<"$restcountries_output" | tr '[:upper:]' '[:lower:]')
[[ -z "$cc" ]] && PrintErrorAndExit "No country matching '$userinput' found"
countryname=$(jq -r '.[].name.common' <<<"$restcountries_output")
population=$(jq -r '.[].population' <<<"$restcountries_output")
country_ipv4_blocks=$(docurl -s "https://raw.githubusercontent.com/herrbischoff/country-ip-blocks/master/ipv4/$cc.cidr")
country_ipv6_blocks=$(docurl -s "https://raw.githubusercontent.com/herrbischoff/country-ip-blocks/master/ipv6/$cc.cidr")
country_ipv4_blocks_count=$(wc -l <<<"$country_ipv4_blocks")
country_ipv6_blocks_count=$(wc -l <<<"$country_ipv6_blocks")
if [ "$country_ipv4_blocks" = "404: Not Found" ]; then
country_ipv4_blocks=""
country_ipv4_blocks_count=0
fi
if [ "$country_ipv6_blocks" = "404: Not Found" ]; then
country_ipv6_blocks=""
country_ipv6_blocks_count=0
fi
# calculate total number of IPs allocated to the country
json_country_ipv4_ip_count=0
cidrlist=$(echo -e "$country_ipv4_blocks" | sed -e 's/^.*\///g' | sort | uniq -c | sort -nr)
for cidr in $cidrlist; do
cidr_total_ips=$(awk '{printf "%d", (2**(32-$2) * $1)}' <<<"$cidr")
json_country_ipv4_ip_count=$(( json_country_ipv4_ip_count + cidr_total_ips ))
done
ips_per_capita=$(awk "BEGIN {printf \"%.2g\", $json_country_ipv4_ip_count/$population}")
country_ipv4_ip_count=$(printf "%'d" "$json_country_ipv4_ip_count")
population=$(printf "%'d" "$population")
if [ "$JSON_OUTPUT" = true ]; then
json_country_ipv4_blocks=$(jq -c --slurp --raw-input 'split("\n") | map(select(length > 0))' <<<"$country_ipv4_blocks")
json_country_ipv6_blocks=$(jq -c --slurp --raw-input 'split("\n") | map(select(length > 0))' <<<"$country_ipv6_blocks")
json_resultcount="1"
json_target_type="country"
final_json_output="{\"country_name\":\"$countryname\""
final_json_output+=",\"country_code\":\"$cc\""
final_json_output+=",\"ipv4_blocks\":$country_ipv4_blocks_count"
final_json_output+=",\"ipv4_total_ips\":$json_country_ipv4_ip_count"
final_json_output+=",\"ipv4_per_capita\":$ips_per_capita"
final_json_output+=",\"ipv4\":${json_country_ipv4_blocks}"
final_json_output+=",\"ipv6_blocks\":$country_ipv6_blocks_count"
final_json_output+=",\"ipv6\":${json_country_ipv6_blocks}"
final_json_output+="}"
PrintJsonOutput
else
BoxHeader "CIDR blocks allocated to $countryname"
BoxHeader "IPv4"
echo -e "$country_ipv4_blocks" | pr -4 -T -W$((terminal_width/2))
BoxHeader "IPv6"
echo -e "$country_ipv6_blocks" | pr -4 -T -W$((terminal_width/2))
BoxHeader "IP Statistics for $countryname (.$cc)"
echo -e "\n- ${green}$country_ipv4_blocks_count${default} IPv4 blocks found\n- ${yellow}$country_ipv6_blocks_count${default} IPv6 blocks found"
echo -e "- Population: ${population}\n- Total IPv4 addresses: ${blue}$country_ipv4_ip_count${default}${default} (~${blue}$ips_per_capita${default} IPs per person)"
BoxHeader "CIDR distribution (IPv4)"
for cidr in $cidrlist; do
cidrcount=$(awk '{print $1}' <<<"$cidr")
cidrsize=$(awk '{print $2}' <<<"$cidr")
[[ "$cidrcount" = "1" ]] && s="" || s="s"
printf "%10s x ${magenta}/%s${default} %s\n" "$cidrcount" "$cidrsize" "block$s"
done
echo ""
fi
exit 0
fi
BoxHeader "ASN lookup for $userinput"
DebugPrint "ASN v$ASN_VERSION started with target: '$userinput'"
if [ "$FORCE_ORGSEARCH" = true ]; then
# user passed the "-o" switch
ORG_FILTER=false
declare -a orgfilters_array
declare -a excl_orgfilters_array
SearchByOrg "$userinput"
exit 0
fi
if [ "$SUGGEST_SEARCH" = true ]; then
# user passed the "-s" switch
RIPESuggestASN "$userinput"
exit 0
fi
input=$(echo "$userinput" | sed 's/\/.*//g' | grep -Eo "$ipv4v6regex")
if [ -z "$input" ]; then
# Input is not an IP Address
if [ "$BGP_UPSTREAM_MODE" = true ]; then
echo -e "\n${red} Error: the ${blue}'-u'${red} option requires an IPv4 or IPv6 address as input${default}\n"
exit 1
fi
# Check if it is a number (ASN)
asn=$(echo "$userinput" | sed 's/[a|A][s|S]//g' | grep -E "^[0-9]*$")
if [ -z "$asn" ]; then
# Input is not an ASN either. See if it's a hostname (includes at least one dot)
if echo "$userinput" | grep -q "\."; then
# filter the input in case it's a URL, extracting relevant hostname/IP data
userinput=$(awk -F[/:] '{gsub(".*//", ""); gsub(".*:.*@", ""); print $1}' <<<"$userinput")
# run the IP regex again in case input is an IP(v6) URL (e.g. https://1.2.3.4/) and skip resolution if an IP is found
ip=$(echo "$userinput" | grep -Eo "$ipv4v6regex")
if [ -z "$ip" ]; then
if [ "$IS_ASN_CHILD" = true ] && [ "$JSON_OUTPUT" = false ]; then
# this instance is an ASN server child (not in JSON mode)
targetdomain=$(rev <<<"$userinput" | cut -d '.' -f 1,2 | rev)
echo -e "\n${blue}- Resolving \"$userinput\"... #PERFORMWHOIS $targetdomain\n"
elif [[ "$JSON_OUTPUT" = false ]]; then
# normal output
echo -e -n "\n${blue}- Resolving \"$userinput\"... "
fi
json_target_type="hostname"
ip=$(ResolveHostnameToIPList "$userinput")
if [ -z "$ip" ]; then
resolver_error="${red}Error: unable to resolve hostname${default}"
if [ "$IS_ASN_CHILD" = true ] && [ "$JSON_OUTPUT" = false ]; then
echo -e "$resolver_error\n"
elif [ "$IS_ASN_CHILD" = false ] && [ "$JSON_OUTPUT" = false ]; then
# normal output
echo -e "${resolver_error}\n\n(Hint: if you wanted to search by organization, try the ${blue}'-o'${default} switch)\n" >&2
else
# json output (both ASN child and normal instance)
PrintErrorAndExit "unable to resolve hostname"
fi
exit 1
fi
fi
numips=$(echo "$ip" | wc -l)
[[ $numips = 1 ]] && s="" || s="es"
[[ "$JSON_OUTPUT" = false ]] && echo -e "${blue}$numips IP address$s found:${default}\n"
# grab the longest IP to properly size output padding
longest=0
for singleip in $ip; do
[[ ${#singleip} -gt $longest ]] && longest=${#singleip}
done
(( longest++ ))
# output actual results
ip_to_trace=""
WhatIsMyIP
if [ "$IS_ASN_CHILD" = true ] && [ "$numips" -gt 1 ] && [ "$JSON_OUTPUT" = false ]; then
# Running headless as an ASN server connhandler child, not in JSON mode, and more than 1 IP detected.
# Speed up the operation and only process the first appropriate IP (v4/v6)
if [ "$HAVE_IPV6" = true ] && grep -q ':' <<<"$ip"; then
ip_to_trace=$(echo "$ip" | grep -m1 ':')
else
ip_to_trace=$(echo "$ip" | grep -v -m1 ':')
fi
LookupASNAndRouteFromIP "$ip_to_trace"
WhoisIP "$ip_to_trace"
PrintReputationAndShodanData "$ip_to_trace"
echo -e "\n${bluebg} INFO ${default} multiple IP support is ${red}disabled${default} for remote traces. The remaining $((numips-1)) IPs have been ignored:"
count=1
for singleip in $(echo "$ip" | grep -v "$ip_to_trace"); do
ignoredip="$singleip"
if [[ count -lt $((numips-1)) ]]; then
echo -e " ├ ${white}${ignoredip}${default}"
else
echo -e " └ ${white}${ignoredip}${default}"
fi
((count++))
done
echo ""
TraceASPath "$ip_to_trace"
else
for singleip in $ip; do
if [ -n "$ip_to_trace" ] && [ "$JSON_OUTPUT" = true ]; then
# add a comma to separate json output in case we hit multiple dns lookup results
final_json_output+=","
fi
LookupASNAndRouteFromIP "$singleip"
WhoisIP "$singleip"
PrintReputationAndShodanData "$singleip"
# save the first IP from the dns lookup result
[[ -z "$ip_to_trace" ]] && ip_to_trace="$singleip"
done
[[ "$JSON_OUTPUT" = true ]] && PrintJsonOutput
# Check if AS path tracing is requested
if [ "$MTR_TRACING" = true ]; then
# In case of multiple IPs (DNS RR), trace the first one.
# Additionally, if we're on an IPv6 connection, default to
# tracing to the first resolved IPv6 address (if any)
if [ "$HAVE_IPV6" = true ]; then
first_ipv6=$(echo "$ip" | grep -m1 ':')
[[ -n "$first_ipv6" ]] && ip_to_trace="$first_ipv6"
fi
TraceASPath "$ip_to_trace"
fi
fi
if [ "$JSON_OUTPUT" = false ]; then
tput sgr0
echo ""
fi
exit 0
else
# not an IP, not an ASN, not a hostname. Consider it an Organization name
ORG_FILTER=false
declare -a orgfilters_array
declare -a excl_orgfilters_array
SearchByOrg "$userinput"
fi
else
# Input is an ASN
json_target_type="asn"
WhoisASN "$asn"
if [ -z "$found_asname" ]; then
PrintErrorAndExit "Error: no data found for AS${asn}"
fi
GetCAIDARank "$asn"
target_asname="$found_asname"
if [ "$JSON_OUTPUT" = true ]; then
# JSON output
GetIXPresence "$asn"
QueryRipestat "$asn"
GetCloudflareHijacksAndLeaks "$asn"
final_json_output+="{"
final_json_output+="\"asn\":\"${asn}\""
final_json_output+=",\"asname\":\"${target_asname//\"/\\\"}\""
final_json_output+=",\"asrank\":${caida_asrank//\"/\\\"}"
final_json_output+=",\"org\":\"${found_org//\"/\\\"}\""
final_json_output+=",\"holder\":\"${found_holder//\"/\\\"}\""
final_json_output+=",\"abuse_contacts\":${json_abuse_contacts}"
final_json_output+=",\"registration_date\":\"${found_createdate}\""
final_json_output+=",\"ixp_presence\":$json_ixps"
# peer count
if [ -n "$ripestat_routing_data" ]; then
final_json_output+=",\"prefix_count_v4\":${ripestat_ipv4}"
final_json_output+=",\"prefix_count_v6\":${ripestat_ipv6}"
final_json_output+=",\"bgp_peer_count\":${ripestat_bgp}"
fi
# BGP incident (hijacks/leaks) summary
if [ "$cf_hijack_query_success" = true ]; then
final_json_output+=",\"bgp_hijack_incidents\":{\"total\":${cf_hijacks_count}"
final_json_output+=",\"as_hijacker\":${cf_hijacks_as_hijacker_count}"
final_json_output+=",\"as_victim\":${cf_hijacks_as_victim_count}"
final_json_output+="}"
fi
if [ "$cf_leak_query_success" = true ]; then
final_json_output+=",\"bgp_leak_incidents\":{\"total\":${cf_leaks_count}"
final_json_output+="}"
fi
# peer list
if [ -n "$ripestat_neighbours_data" ]; then
final_json_output+=",\"bgp_peers\":{"
final_json_output+="\"upstream\":$json_upstream_peers"
final_json_output+=",\"downstream\":$json_downstream_peers"
final_json_output+=",\"uncertain\":$json_uncertain_peers"
final_json_output+="}"
fi
# announced prefixes
final_json_output+=",\"announced_prefixes\":$json_ripe_prefixes"
final_json_output+=",\"inetnums\":{"
final_json_output+="\"v4\":$json_ipv4_aggregated_inetnums"
final_json_output+=",\"v6\":$json_ipv6_aggregated_inetnums}"
final_json_output+=",\"inetnums_announced_by_other_as\":{"
final_json_output+="\"v4\":[$json_ipv4_other_inetnums]"
final_json_output+=",\"v6\":[$json_ipv6_other_inetnums]}"
final_json_output+="}"
PrintJsonOutput
else
# normal output
echo -en "\n${bluebg} AS Number ──>${default} ${red}${asn}"
if [ "$IS_ASN_CHILD" = true ]; then
ripestat_link="RIPEStat"
he_link="HE.NET"
bgpview_link="BGPView"
bgptools_link="BGPTools"
peeringdb_link="PeeringDB"
echo -en " ($ripestat_link🔗 • $he_link🔗 • $bgpview_link🔗 • $bgptools_link🔗 • $peeringdb_link🔗)"
fi
echo ""
echo -en "${bluebg} AS Name ──>${default} ${green}${target_asname}"
if [ "$IS_ASN_CHILD" = true ]; then
# signal to the parent connhandler the correct country flag to display for this ASN
flag_icon_cc=$(echo "${target_asname##*,}" | tr -d ' ' | tr '[:upper:]' '[:lower:]')
[[ -n "$flag_icon_cc" ]] && echo -n " #COUNTRYCODE $flag_icon_cc"
fi
printf "\n"
echo -e "${bluebg} Organization ──>${default} ${yellow}${found_holder} (${found_org})"
echo -e "${bluebg} CAIDA AS Rank ──>${default} ${caida_asrank_recap}${default}"
echo -e "${bluebg} Abuse contact ──>${default} ${blue}${found_abuse_contact}"
echo -e "${bluebg} AS Reg. date ──>${default} ${white}${found_createdate}"
echo -e "${bluebg} RIR (Region) ──>${default} ${white}${caida_rir}"
echo -en "${bluebg} Peering @IXPs ──>${default} "
GetIXPresence "$asn"
echo ""
BoxHeader "BGP information for AS${asn} (${target_asname})"
GetCloudflareHijacksAndLeaks "$asn"
echo ""
echo -e "${bluebg} BGP Neighbors ────>${default} ${green}${caida_degree_total}${default} ${dim}(${default}${caida_degree_provider}${dim} Transits • ${default}${caida_degree_peer}${dim} Peers • ${default}${caida_degree_customer}${dim} Customers)${default}"
echo -e "${bluebg} Customer cone ────>${default} ${green}${caida_customercone} ${default}${dim}(# of ASNs observed in the customer cone for this AS)${default}"
echo -e "${bluebg} BGP Hijacks (past 1y) ────>${default} $cf_hijacks_text"
echo -e "${bluebg} BGP Route leaks (past 1y) ──>${default} $cf_leaks_text"
radar_href="https://radar.cloudflare.com/routing/as$asn?dateRange=52w"
[[ "$IS_ASN_CHILD" = true ]] && radar_href="View on Cloudflare Radar🔗"
echo -e "${bluebg} In-depth BGP incident info ─>${default} ${dim}${blue}➜ ${radar_href}${default}"
echo ""
BoxHeader "Prefix information for AS${asn} (${target_asname})"
echo ""
QueryRipestat "${asn}"
if [ -n "$ripestat_routing_data" ]; then
echo -e "${bluebg} IPv4 Prefixes ────>${default} ${green}${ripestat_ipv4}"
echo -e "${bluebg} IPv6 Prefixes ────>${default} ${yellow}${ripestat_ipv6}"
fi
if [ -n "$ripestat_neighbours_data" ]; then
[[ -n "$upstream_peers" ]] && upstream_peers=$(echo -e "$upstream_peers") || upstream_peers="${redbg} NONE ${default}"
[[ -n "$downstream_peers" ]] && downstream_peers=$(echo -e "$downstream_peers") || downstream_peers="${redbg} NONE ${default}"
[[ -n "$uncertain_peers" ]] && uncertain_peers=$(echo -e "$uncertain_peers") || uncertain_peers="${redbg} NONE ${default}"
echo ""
BoxHeader "Peering information for AS${asn} (${target_asname})"
echo -e "\n${green}──────────────── Upstream Peers ────────────────${default}\n\n${upstream_peers}"
echo -e "\n${yellow}─────────────── Downstream Peers ───────────────${default}\n\n${downstream_peers}"
echo -e "\n${white}─────────────── Uncertain Peers ───────────────${default}\n\n${uncertain_peers}"
fi
echo ""
BoxHeader "Aggregated IP resources for AS${asn} (${target_asname})"
echo -e "\n${green}───── IPv4 ─────${default}"
[[ -n "$ipv4_inetnums" ]] && echo -e "${green}${ipv4_inetnums}${default}" || echo -e "\n${redbg} NONE ${default}"
echo -e "\n${yellow}───── IPv6 ─────${default}"
[[ -n "$ipv6_inetnums" ]] && echo -e "${yellow}${ipv6_inetnums}${default}" || echo -e "\n${redbg} NONE ${default}"
tput sgr0
echo ""
fi
exit 0
fi
else
# Input is an IP address
grep -q ":" <<<"$input" && json_target_type="ipv6" || json_target_type="ipv4"
# Perform IP lookup
LookupASNAndRouteFromIP "$input"
(( longest=${#input}+1 ))
if [ "$BGP_UPSTREAM_MODE" = true ]; then
BGPUpstreamLookup "$input"
[[ "$JSON_OUTPUT" = true ]] && PrintJsonOutput
exit 0
fi
WhoisIP "$input"
PrintReputationAndShodanData "$input"
[[ "$JSON_OUTPUT" = true ]] && PrintJsonOutput
# Perform AS path tracing if requested
[[ "$MTR_TRACING" = true ]] && TraceASPath "$input"
if [ "$JSON_OUTPUT" = false ]; then
tput sgr0
echo ""
fi
exit 0
fi