From 4efe54d689c016deb18e422b5f3ab735b208f6c0 Mon Sep 17 00:00:00 2001
From: nitefood
Date: Tue, 20 Aug 2024 15:39:40 +0200
Subject: [PATCH] BGP hijack/route leak historical incidents reporting, abuse
lookup, full API token support for Docker and GCP, JSON mode improvements -
added historical (past year) BGP incident (hijacks/route leaks) reporting for
AS targets using Cloudflare Radar API. Requires a free API token from
Cloudflare - see
https://github.com/nitefood/asn#bgp-hijack-and-route-leak-incidents-cloudflare-radar
[terminal, server and JSON modes] - abuse contact lookup improvements using
DSHIELD API as fallback if RIPEStat has no match - enhanced JSON output and
server terminal dashboard with API token presence - suppressed statusbar
displaying for JSON mode (could interfere with output parsing by third party
tools not expecting data on stderr) - added ipinfo.io and Cloudflare API
tokens support for Docker container (as ENV vars) and for Google Cloud Shell
(in the GCP bootstrap script) - updated base Docker container image to Alpine
3.20.2 - minor Dockerfile optimizations
---
Dockerfile | 18 ++--
README.md | 177 +++++++++++++++++++++++++++++-----------
asn | 173 +++++++++++++++++++++++++++++----------
cloudshell_bootstrap.sh | 26 +++++-
4 files changed, 295 insertions(+), 99 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 890cb61..668ebc1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,18 +1,22 @@
-FROM alpine:3.18.5
+FROM alpine:3.20.2
ENV IQS_TOKEN ""
+ENV IPINFO_TOKEN ""
+ENV CLOUDFLARE_TOKEN ""
# - Prepare the config directory
-# - Create the entrypoint script that writes the IQS token to the config file
+# - Create the entrypoint script that writes the API tokens to the config files
# - Install prerequisite packages
RUN mkdir -p /etc/asn && \
- touch /etc/asn/iqs_token && \
- chown nobody:nobody /etc/asn/iqs_token && \
- echo -e "#!/bin/sh\nif [ -n \"\$IQS_TOKEN\" ]; then echo \"\$IQS_TOKEN\" > /etc/asn/iqs_token; fi\nexec \"\$@\"" > /entrypoint.sh && \
+ chown nobody:nobody /etc/asn/ && \
+ printf '%s\n' '#!/usr/bin/env bash' \
+ '[[ -n "$IQS_TOKEN" ]] && echo "$IQS_TOKEN" > /etc/asn/iqs_token' \
+ '[[ -n "$IPINFO_TOKEN" ]] && echo "$IPINFO_TOKEN" > /etc/asn/ipinfo_token' \
+ '[[ -n "$CLOUDFLARE_TOKEN" ]] && echo "$CLOUDFLARE_TOKEN" > /etc/asn/cloudflare_token' \
+ 'exec "$@"' > /entrypoint.sh && \
chmod +x /entrypoint.sh && \
apk update && \
- apk add -X https://dl-cdn.alpinelinux.org/alpine/v3.19/community grepcidr3 && \
- apk add --no-cache aha bash bind-tools coreutils curl ipcalc jq mtr ncurses nmap nmap-ncat whois
+ apk add --no-cache aha bash bind-tools coreutils curl grepcidr3 ipcalc jq mtr ncurses nmap nmap-ncat whois
COPY asn /bin/asn
RUN chmod 0755 /bin/asn
diff --git a/README.md b/README.md
index b0ae439..804d938 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,7 @@ Furthermore, it can serve as a self-hosted lookup **API endpoint** and output JS
* **IXP Presence** (*Internet Exchange facilities where the AS is present*)
* **Global AS rank** (*derived from the size of its customer cone, number of peering relationships and more*)
* **BGP statistics** (*neighbours count, originated v4/v6 prefix count*)
+ * **BGP incident history** (number of *BGP hijacks* and *route leaks* involving the target AS in the past 12 months, as a **victim** or a **hijacker**)
* **Peering relationships** separated by type (*upstream/downstream/uncertain*), and sorted by observed *path count*, to give more reliable results (so for instance, the first few upstream peers are most likely to be transits). Furthermore, a recap of *transits/peers/customers* amount (per latest CAIDA data) is displayed.
* **Announced prefixes** aggregated to the most relevant less-specific `INET(6)NUM` object (actual [LIR allocation](https://www.ripe.net/manage-ips-and-asns/db/support/documentation/ripe-database-documentation/rpsl-object-types/4-2-descriptions-of-primary-objects/4-2-4-description-of-the-inetnum-object)).
@@ -115,6 +116,8 @@ The script uses the following services for data retrieval:
* [ip-api](https://ip-api.com/)
* [StopForumSpam](https://www.stopforumspam.com/)
* [IP Quality Score](https://www.ipqualityscore.com)
+* [Cloudflare Radar](https://radar.cloudflare.com/)
+* [ISC DSHIELD](https://isc.sans.edu/)
* [GreyNoise](https://greynoise.io)
* [Shodan](https://www.shodan.io/)
* [NIST National Vulnerability Database](https://nvd.nist.gov/)
@@ -129,6 +132,7 @@ It also provides hyperlinks (in [server](#running-lookups-from-the-browser) mode
* [BGPTools](https://bgp.tools)
* [ipinfo.io](https://ipinfo.io)
* [Host.io](https://host.io)
+* [Cloudflare Radar](https://radar.cloudflare.com/)
Requires Bash v4.2+. Tested on:
@@ -159,9 +163,9 @@ Requires Bash v4.2+. Tested on:
![ipv6lookup](https://user-images.githubusercontent.com/24555810/159185780-44a1af6e-7aa9-4f52-b04c-55a314b2a5e3.png)
-* *Autonomous system number lookup with AS ranking, operational region, BGP stats, peering and prefix informations*
+* *Autonomous system number lookup with AS ranking, operational region, BGP stats and incident history, peering and prefix informations*
- ![asnlookup](https://github.com/nitefood/asn/assets/24555810/758890d8-7103-41f3-978e-ba5799213af6)
+ ![asnlookup](https://github.com/user-attachments/assets/6afdd2cf-a454-4607-ac17-b62fe78ba816)
* *Hostname/URL lookup*
@@ -234,12 +238,13 @@ To run the script without installing it locally, you have the following options:
* **Docker** _(thanks [Gianni Stubbe](https://github.com/33Fraise33), [anarcat](https://github.com/anarcat), [Francesco Colista](https://github.com/fcolista), [arbal](https://github.com/arbal))_
- _Note: the Docker image runs by default in server mode, if no parameters are given. This is equivalent to running the tool as `asn -l 0.0.0.0` (run server, bind to all IPv4 interfaces - this is necessary to expose the server port to the host machine). You can run the server with different [options](#syntax) by explicitly passing `-l [options]`. It's also possible to pass an [IpQualityScore token](#ip-reputation-api-token) (both client and server runs) by setting the `IQS_TOKEN` environment variable (example below) in the container._
+ _Note: the Docker image runs by default in server mode, if no parameters are given. This is equivalent to running the tool as `asn -l 0.0.0.0` (run server, bind to all IPv4 interfaces - this is necessary to expose the server port to the host machine). You can run the server with different [options](#syntax) by explicitly passing `-l [options]`. It's also possible to pass an [IpQualityScore](#ip-reputation-api-token-ipqualityscore), [ipinfo.io](#geolocation-api-token-ipinfoio) and/or [Cloudflare](#bgp-hijack-and-route-leak-incidents-cloudflare-radar) API token (both client and server runs) by setting, respectively, the `IQS_TOKEN`, `IPINFO_TOKEN` and `CLOUDFLARE_TOKEN` environment variables (examples below) in the container._
Usage examples:
- Start server: `docker run -it -p 49200:49200 nitefood/asn`
- Client mode: `docker run -it nitefood/asn 1.1.1.1`
- - Supply an IQS token: `docker run -it -e IQS_TOKEN="" nitefood/asn [...]`
+ - Supply an IQS token: `docker run -it -e IQS_TOKEN="xxx" nitefood/asn [...]`
+ - Supply multiple tokens: `docker run -it -e IQS_TOKEN="xxx" -e IPINFO_TOKEN="yyy" -e CLOUDFLARE_TOKEN="zzz" nitefood/asn [...]`
* **Google Cloud Shell**
@@ -251,7 +256,7 @@ To run the script without installing it locally, you have the following options:
**2.** Prepare the GCP environment by launching `./cloudshell_bootstrap.sh`
- **3.** _(OPTIONAL)_ Input your [IpQualityScore token](#ip-reputation-api-token) when requested to enable in-depth threat analisys and scoring
+ **3.** _(OPTIONAL)_ Input your [API tokens](#api-tokens) when requested to enable full script features
- - -
@@ -436,7 +441,7 @@ The script can be configured to make use of your API tokens to enhance its funct
The currently supported API tokens are:
-### Geolocation API token
+### Geolocation API token (ipinfo.io)
Geolocation API token details
@@ -465,7 +470,7 @@ Either way, `asn` will pick up your token on the next run (no need to restart th
-### IP reputation API token
+### IP reputation API token (IPQualityScore)
IP reputation API token details
@@ -497,6 +502,44 @@ Either way, `asn` will pick up your token on the next run (no need to restart th
+### BGP hijack and route leak incidents (Cloudflare Radar)
+
+Cloudflare token details
+
+When this token is available, an additional lookup will be enabled for **autonomous system** targets, in order to enumerate the BGP incidents (both **BGP hijacks** and **BGP route leaks**) involving the target ASN.
+
+The script will use the [Cloudfare Radar](https://radar.cloudflare.com/) API to retrieve the amount of incidents involving the target ASN in the past 12 months. Additionally, it will report how many incidents saw the target ASN as a **hijacker** or as a **victim**.
+
+The Cloudflare Radar API is **free** to use, but requires a registration. The steps are:
+
+1. [Sign up](https://dash.cloudflare.com/sign-up) for a free Cloudflare account and **validate your email**
+2. From the [Cloudflare dashboard](https://dash.cloudflare.com/profile/api-tokens/), go to **My Profile > API Tokens**.
+3. Select **Create Token**
+4. Choose the "*Read Cloudflare Radar data*" template
+5. Click **Continue to summary** (the default values are fine)
+6. Click **Create token**
+
+Once obtained, the api token should be written to one of the following files (parsed in that order):
+
+`$HOME/.asn/cloudflare_token` or
+`/etc/asn/cloudflare_token`
+
+The `/etc`-based file should be used when running asn in **server mode**. The `$HOME`-based file takes precedence if both files exist, and is ideal for **user mode** (that is, running `asn` interactively from the command line).
+
+In order to do so, you can use the following command:
+
+***User mode:***
+
+`TOKEN=""; mkdir "$HOME/.asn/" && echo "$TOKEN" > "$HOME/.asn/cloudflare_token" && chmod -R 600 "$HOME/.asn/"`
+
+***Server mode:***
+
+`TOKEN=""; mkdir "/etc/asn/" && echo "$TOKEN" > "/etc/asn/cloudflare_token" && chmod -R 700 "/etc/asn/" && chown -R nobody /etc/asn/`
+
+Either way, `asn` will pick up your token on the next run (no need to restart the service if running in server mode), and use it to query the Cloudflare Radar API.
+
+
+
- - -
## Usage
@@ -948,9 +991,14 @@ The tool can be instructed to output lookup results in JSON mode by using the `-
"target_type": "ipv4",
"result": "ok",
"reason": "success",
- "version": "0.72.1",
- "request_time": "2022-03-28T22:42:34",
- "request_duration": 3,
+ "version": "0.78.0",
+ "request_time": "2024-08-20T02:50:28",
+ "request_duration": 5,
+ "api_tokens": {
+ "ipqualityscore": true,
+ "ipinfo": true,
+ "cloudflare": true
+ },
"result_count": 1,
"results": [
{
@@ -958,16 +1006,18 @@ The tool can be instructed to output lookup results in JSON mode by using the `-
"ip_version": "4",
"reverse": "dns.google",
"org_name": "Google LLC",
+ "net_range": "8.8.8.0/24",
+ "net_name": "GOGL",
"abuse_contacts": [
- "abuse@level3.com",
"network-abuse@google.com"
],
"routing": {
"is_announced": true,
"as_number": "15169",
"as_name": "GOOGLE, US",
- "net_range": "8.8.8.0/24",
- "net_name": "LVLT-GOGL-8-8-8",
+ "as_rank": "1788",
+ "route": "8.8.8.0/24",
+ "route_name": "",
"roa_count": "1",
"roa_validity": "valid"
},
@@ -978,13 +1028,13 @@ The tool can be instructed to output lookup results in JSON mode by using the `-
"is_proxy": false,
"is_dc": true,
"dc_details": {
- "dc_name": "Google Cloud"
+ "dc_name": "Google LLC"
},
"is_ixp": false
},
"geolocation": {
- "city": "Washington, D.C.",
- "region": "Washington, D.C.",
+ "city": "Mountain View",
+ "region": "California",
"country": "United States",
"cc": "US"
},
@@ -1018,15 +1068,20 @@ The tool can be instructed to output lookup results in JSON mode by using the `-
"target_type": "asn",
"result": "ok",
"reason": "success",
- "version": "0.76.0",
- "request_time": "2024-02-22T00:11:41",
- "request_duration": 10,
+ "version": "0.78.0",
+ "request_time": "2024-08-20T02:50:46",
+ "request_duration": 17,
+ "api_tokens": {
+ "ipqualityscore": true,
+ "ipinfo": true,
+ "cloudflare": true
+ },
"result_count": 1,
"results": [
{
"asn": "5505",
"asname": "VADAVO, ES",
- "asrank": 3779,
+ "asrank": 4448,
"org": "VDV-VLC-RED06 VDV-VLC-RED06 - CLIENTES TELECOM",
"holder": "VADAVO SOLUCIONES SL",
"abuse_contacts": [
@@ -1035,31 +1090,41 @@ The tool can be instructed to output lookup results in JSON mode by using the `-
"registration_date": "2016-12-13T08:28:07",
"ixp_presence": [
"DE-CIX Madrid: DE-CIX Madrid Peering LAN",
- "ESPANIX Madrid Lower LAN"
+ "ESpanix Madrid Lower LAN"
],
"prefix_count_v4": 8,
"prefix_count_v6": 1,
"bgp_peer_count": 36,
+ "bgp_hijack_incidents": {
+ "total": 0,
+ "as_hijacker": 0,
+ "as_victim": 0
+ },
+ "bgp_leak_incidents": {
+ "total": 0
+ },
"bgp_peers": {
"upstream": [
"1299",
"6939",
"59432",
"174",
+ "34549",
"25091",
+ "35625",
"33891",
- "8218",
- "41327",
"48348",
- "35280",
- "35625",
- "4455",
"13030",
- "202766",
+ "8218",
+ "41327",
"3303",
+ "4455",
+ "6424",
"6057",
- "137409",
- "15830"
+ "34927",
+ "9498",
+ "35280",
+ "1239"
],
"downstream": [
"48952",
@@ -1068,32 +1133,30 @@ The tool can be instructed to output lookup results in JSON mode by using the `-
"202054"
],
"uncertain": [
- "47787",
- "39384",
- "37721",
- "36236",
- "25160",
"24482",
"51185",
- "49544",
"41047",
"29680",
- "29049",
"212483",
+ "198150",
"14840",
- "34927"
+ "49544",
+ "39384",
+ "37721",
+ "36236",
+ "25160"
]
},
"announced_prefixes": {
"v4": [
- "185.123.204.0/24",
- "185.123.207.0/24",
+ "185.210.225.0/24",
"188.130.247.0/24",
- "185.210.226.0/24",
"185.210.227.0/24",
"185.123.205.0/24",
- "185.210.225.0/24",
- "185.123.206.0/24"
+ "185.123.207.0/24",
+ "185.210.226.0/24",
+ "185.123.206.0/24",
+ "185.123.204.0/24"
],
"v6": [
"2a03:9320::/32"
@@ -1169,16 +1232,21 @@ The tool can be instructed to output lookup results in JSON mode by using the `-
"target_type": "ipv4",
"result": "ok",
"reason": "success",
- "version": "0.76.0",
- "request_time": "2024-02-22T00:15:25",
- "request_duration": 3,
+ "version": "0.78.0",
+ "request_time": "2024-08-20T02:54:03",
+ "request_duration": 4,
+ "api_tokens": {
+ "ipqualityscore": true,
+ "ipinfo": true,
+ "cloudflare": true
+ },
"result_count": 1,
"results": [
{
"prefix": "72.17.0.0/17",
"origin_as": "33363",
"origin_as_name": "BHN-33363, US",
- "origin_as_rank": 435,
+ "origin_as_rank": 441,
"upstreams_count": 1,
"upstreams": [
{
@@ -1207,6 +1275,23 @@ The tool can be instructed to output lookup results in JSON mode by using the `-
188.130.254.0/24
```
+
+Example 7 - enumerating the amount of BGP hijacking incidents involving a given AS
+
+##### Command:
+
+`asn -j AS8860 | jq '.results[].bgp_hijack_incidents'`
+
+##### Output:
+
+```
+{
+ "total": 18,
+ "as_hijacker": 11,
+ "as_victim": 7
+}
+```
+
#### Remotely (API endpoint)
diff --git a/asn b/asn
index 715293f..c1a7990 100755
--- a/asn
+++ b/asn
@@ -12,7 +12,7 @@
# │ (Launch the script without parameters or visit the project's homepage for usage info)│
# ╰──────────────────────────────────────────────────────────────────────────────────────╯
-ASN_VERSION="0.77.0"
+ASN_VERSION="0.78.0"
ASN_LOGFILE="$HOME/asndebug.log"
# ╭──────────────────╮
@@ -897,9 +897,16 @@ AbuseLookupForPrefix(){
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"
@@ -2132,6 +2139,11 @@ PrintJsonOutput(){
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
@@ -2361,6 +2373,76 @@ GetCAIDARank(){
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
@@ -2883,11 +2965,16 @@ AsnServerListener(){
else
DISPLAY_ASN_SRV_BINDADDR="${ASN_SRV_BINDADDR}"
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}"
@@ -2954,12 +3041,8 @@ BoxHeader() { # cheers https://unix.stackexchange.com/a/70616
}
StatusbarMessage() { # invoke without parameters to delete the status bar message
- # suppress output for headless runs
- [[ "$IS_HEADLESS" = true ]] && return
-
- if [ "$ASN_DEBUG" = true ]; then
- # [[ -n "$1" ]] && statusdbgstring="$1" || statusdbgstring="[remove last statusbar message]"
- # echo -e "${default}[$(date +'%F %T')] ${lightgreybg} STATUS ${default} ${statusdbgstring}${default}"
+ # suppress status bar displaying for headless, json or debug runs
+ if [ "$IS_HEADLESS" = true ] || [ "$JSON_OUTPUT" = true ] || [ "$ASN_DEBUG" = true ]; then
return
fi
@@ -3184,26 +3267,36 @@ CheckPrerequisites() {
fi
fi
- IQS_TOKEN=""
- IPINFO_TOKEN=""
+ 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" ]; 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="------------------------------------------------------------"
@@ -3221,40 +3314,15 @@ CheckPrerequisites() {
fi
if [ "$ASN_DEBUG" = true ]; then
echo ""
- for token in "IQS_TOKEN" "IPINFO_TOKEN"; do
+ for token in "IQS_TOKEN" "IPINFO_TOKEN" "CLOUDFLARE_TOKEN"; do
if [ -z "${!token}" ]; then
- DebugPrint "${dim}${white}[$token: ${red}❌ NOT FOUND${white}]"
+ DebugPrint "${dim}${white}$token: ${red}❌ NOT FOUND${white}"
else
- DebugPrint "${dim}${white}[$token: ${green}✓ OK${white}]"
+ DebugPrint "${dim}${white}$token: ${green}✓ OK${white}"
fi
done
fi
fi
- # if [ -z "$IQS_TOKEN" ] && [ "$JSON_OUTPUT" = false ]; then
- # # warn the user about the absence of in-depth IP reputation API token
- # if [ "$IS_HEADLESS" = true ]; then
- # line="------------------------------------------------------------"
- # echo -e "\n${line}\nWARNING: No IpQualityScore token found, disabling in-depth\nthreat analysis. Check" \
- # "\nhttps://github.com/nitefood/asn#ip-reputation-api-token for\ninstructions on how to enable it." \
- # "\n${line}" >&2
- # else
- # line="────────────────────────────────────────────────────────────"
- # echo -en "\n${yellow}${line}\n\t\t\tWARNING${default}" \
- # "\n\n${white}No IPQualityScore token found, so disabling in-depth threat" \
- # "\nanalysis and IP reputation lookups. Please visit" \
- # "\n${blue}https://github.com/nitefood/asn#ip-reputation-api-token${white}" \
- # "\nfor instructions on how to enable it." \
- # "\n${yellow}${line}${default}\n" >&2
- # fi
- # fi
-
- # # API tokens presence check
- # if [ "$IS_HEADLESS" = false ] && [ -z "$IPINFO_TOKEN" ]; then
- # echo ""
- # echo -e "${dim}${blue}[INFO]${white} No IQS token found, disabling in-depth threat analysis (${underline}https://github.com/nitefood/asn#ip-reputation-api-token${default}${dim}${white})${default}"
- # echo -e "${dim}${blue}[INFO]${white} No IPINFO token found, add one (for free) to raise request limit to 50k/month (${underline}http://github.com/nitefood/asn/ipinfo-token${default}${dim}${white})${default}"
- # fi
-
CoreutilsFixup
IFS="$saveIFS"
@@ -3933,11 +4001,12 @@ else
IS_ASN_CONNHANDLER=false
fi
-# External API tokens for ipqualityscore.com (IP reputation & threat analisys lookup)
-# and ipinfo.io (IP geolocation lookup)
+# External API tokens for ipqualityscore.com (IP reputation & threat analisys 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
@@ -4514,6 +4583,7 @@ if [ -z "$input" ]; then
# JSON output
GetIXPresence "$asn"
QueryRipestat "$asn"
+ GetCloudflareHijacksAndLeaks "$asn"
final_json_output+="{"
final_json_output+="\"asn\":\"${asn}\""
final_json_output+=",\"asname\":\"${target_asname//\"/\\\"}\""
@@ -4529,6 +4599,17 @@ if [ -z "$input" ]; then
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\":{"
@@ -4576,9 +4657,15 @@ if [ -z "$input" ]; then
GetIXPresence "$asn"
echo ""
BoxHeader "BGP informations 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 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 informations for AS${asn} (${target_asname})"
echo ""
diff --git a/cloudshell_bootstrap.sh b/cloudshell_bootstrap.sh
index 7cf47d7..2936645 100755
--- a/cloudshell_bootstrap.sh
+++ b/cloudshell_bootstrap.sh
@@ -16,17 +16,37 @@ dim=$'\e[2m'
default=$'\e[0m'
clear
+sudo mkdir -p /etc/asn
echo -e "${dim}$banner${default}\n"
echo -en "Enter your IPQualityScore API token (or press Enter to skip): "
read -sr IQS_TOKEN
+echo -en "\nEnter your ipinfo.io API token (or press Enter to skip): "
+read -sr IPINFO_TOKEN
+echo -en "\nEnter your Cloudflare API token (or press Enter to skip): "
+read -sr CLOUDFLARE_TOKEN
+
if [ -n "$IQS_TOKEN" ]; then
- echo -en "\n- Enabling IPQualityScore lookups..."
- sudo mkdir -p /etc/asn
+ echo -en "\n\n- Enabling IPQualityScore API..."
echo "$IQS_TOKEN" | sudo tee /etc/asn/iqs_token &>/dev/null
echo "${green}OK${default}"
else
- echo -e "\n- IPQualityScore lookups ${red}DISABLED${default}"
+ echo -e "\n\n- IPQualityScore API ${red}DISABLED${default}"
+fi
+if [ -n "$IPINFO_TOKEN" ]; then
+ echo -en "- Enabling ipinfo.io API..."
+ echo "$IPINFO_TOKEN" | sudo tee /etc/asn/ipinfo_token &>/dev/null
+ echo "${green}OK${default}"
+else
+ echo -e "- ipinfo.io API ${red}DISABLED${default}"
fi
+if [ -n "$CLOUDFLARE_TOKEN" ]; then
+ echo -en "- Enabling Cloudflare API..."
+ echo "$CLOUDFLARE_TOKEN" | sudo tee /etc/asn/cloudflare_token &>/dev/null
+ echo "${green}OK${default}"
+else
+ echo -e "- Cloudflare API ${red}DISABLED${default}"
+fi
+
echo -en "- Installing prerequisite packages..."
sudo apt update &>/dev/null
sudo apt -y install curl whois bind9-host mtr-tiny jq ipcalc grepcidr nmap ncat aha &>/dev/null