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