dotfiles/.local/bin/recon
2026-08-15 19:39:06 +01:00

189 lines
7 KiB
Bash
Executable file

#!/usr/bin/env bash
set -o pipefail
export LC_ALL=C
# COLOURS
# standard terminal output hues
red='\033[0;31m'
green='\033[0;32m'
yellow='\033[1;33m'
cyan='\033[0;36m'
nc='\033[0m'
# CONFIGURATION
# defaults to mullvad dns for privacy-respecting resolution
resolver="@194.242.2.2"
timeout_val=2
ua="mozilla/5.0 (x11; linux x86_64) applewebkit/537.36"
# USAGE GUIDE
# displays instructions if arguments are missing
usage() {
printf "${cyan}usage:${nc} reconctl [-t timeout] <domain>\n"
printf " -t set network timeout in seconds (default: 2)\n"
printf " -h show help menu\n\n"
exit 1
}
while getopts "t:h" opt; do
case ${opt} in
t) timeout_val=$OPTARG ;;
h) usage ;;
*) usage ;;
esac
done
shift $((OPTIND - 1))
domain="$1"
[[ -z "$domain" ]] && usage
# INPUT VALIDATION
# rejects malformed hostnames before they reach dig/curl/openssl/whois
if [[ ! "$domain" =~ ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$ ]]; then
printf "${red}error: '%s' is not a valid domain${nc}\n" "$domain"
exit 1
fi
# HELPER FUNCTIONS
# standardises section headers for clean output
section_header() {
printf "\n${cyan}%s${nc}\n" "$1"
}
# DEPENDENCY AUDIT
# validates required packages before execution
for cmd in dig curl jq openssl whois awk; do
if ! command -v "$cmd" >/dev/null 2>&1; then
printf "${red}error: missing dependency '%s'${nc}\n" "$cmd"
exit 1
fi
done
# DNS ANALYSIS
# extracts primary records and security configurations
section_header "DNS RECORDS"
ipv4=$(dig $resolver +short A "$domain")
ipv6=$(dig $resolver +short AAAA "$domain")
cname=$(dig $resolver +short CNAME "$domain")
mx=$(dig $resolver +short MX "$domain")
caa=$(dig $resolver +short CAA "$domain")
txt=$(dig $resolver +short TXT "$domain" | grep -Ei "spf|dmarc" || true)
dnskey=$(dig $resolver +short DNSKEY "$domain")
printf " ipv4:\n%s\n" "$([[ -n "$ipv4" ]] && echo "$ipv4" | sed 's/^/ /' || echo " n/a")"
printf " ipv6:\n%s\n" "$([[ -n "$ipv6" ]] && echo "$ipv6" | sed 's/^/ /' || echo " n/a")"
printf " cname:\n %s\n" "${cname:-none detected}"
printf " nameservers:\n%s\n" "$(dig $resolver +short NS "$domain" | sed 's/^/ /')"
printf " mail exchangers:\n%s\n" "$([[ -n "$mx" ]] && echo "$mx" | sed 's/^/ /' || echo " none detected")"
printf " caa records:\n%s\n" "$([[ -n "$caa" ]] && echo "$caa" | sed 's/^/ /' || echo " none detected")"
printf " security txt:\n%s\n" "$([[ -n "$txt" ]] && echo "$txt" | sed 's/^/ /' || echo " none detected")"
printf " dnssec: %s\n" "$([[ -n "$dnskey" ]] && echo "enabled" || echo "not detected")"
# REVERSE LOOKUP
# checks ptr records for resolved v4 and v6 addresses
section_header "REVERSE DNS"
if [[ -n "$ipv4" || -n "$ipv6" ]]; then
for ip in $ipv4 $ipv6; do
ptr=$(dig $resolver +short -x "$ip")
printf " %s -> %s\n" "$ip" "${ptr:-n/a}"
done
else
printf " n/a\n"
fi
# HTTP PERFORMANCE
# tests routing responses and redirection chains
section_header "HTTP ROUTING"
http_status=$(timeout "$timeout_val" curl -A "$ua" -sL -o /dev/null -w "%{http_code}" "http://$domain" || echo "timeout")
https_status=$(timeout "$timeout_val" curl -A "$ua" -sL -o /dev/null -w "%{http_code}" "https://$domain" 2>/dev/null || echo "timeout")
redirects=$(timeout "$timeout_val" curl -A "$ua" -sI -L "http://$domain" 2>/dev/null | grep -i "^location:" | sed 's/[Ll]ocation: //g' | tr -d '\r')
printf " http status: %s\n" "$http_status"
printf " https status: %s\n" "$https_status"
printf " redirects:\n%s\n" "$([[ -n "$redirects" ]] && echo "$redirects" | sed 's/^/ -> /' || echo " none")"
# ENCRYPTION AUDIT
# pulls subject, issuer, and expiration from the tls certificate
section_header "TLS CERTIFICATE"
tls=$(timeout "$timeout_val" openssl s_client -connect "$domain:443" -servername "$domain" -showcerts </dev/null 2>/dev/null)
expiry=$(echo "$tls" | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
subject_issuer=$(echo "$tls" | grep -E "subject=|issuer=" | sed 's/^[ \t]*//')
printf " subject/issuer:\n%s\n" "$([[ -n "$subject_issuer" ]] && echo "$subject_issuer" | sed 's/^/ /' || echo " unknown")"
printf " expiry date: %s\n" "${expiry:-unknown}"
# SECURITY HEADERS
# evaluates presence of critical web security directives
section_header "SECURITY POSTURE"
headers=$(timeout "$timeout_val" curl -A "$ua" -sI "https://$domain" 2>/dev/null | tr -d '\r')
for h in "strict-transport-security" "content-security-policy" "x-frame-options" "x-content-type-options"; do
val=$(echo "$headers" | grep -i "^$h:" | cut -d: -f2- | xargs)
if [[ -n "$val" ]]; then
printf " %-25s ${green}present${nc}\n" "$h"
else
printf " %-25s ${yellow}missing${nc}\n" "$h"
fi
done
# INFRASTRUCTURE PROBE
# checks common critical ports on the primary resolved ip, results sorted after concurrent scan
section_header "PORT SCAN"
ports=(21 22 25 53 80 443 3306 8080 8443)
target_ip=$(echo "$ipv4" | head -n1)
if [[ -n "$target_ip" ]]; then
scan_tmp=$(mktemp)
exec 3>&2
exec 2>/dev/null
for port in "${ports[@]}"; do
(timeout 1 bash -c "</dev/tcp/$target_ip/$port" &>/dev/null && echo "$port" >> "$scan_tmp") &
done
wait
exec 2>&3
if [[ -s "$scan_tmp" ]]; then
sort -n "$scan_tmp" | while read -r port; do
printf " port %-5s ${green}open${nc}\n" "$port"
done
else
printf " no open ports detected\n"
fi
rm -f "$scan_tmp"
else
printf " ${red}no ipv4 address for scanning.${nc}\n"
fi
# ASSET ENUMERATION
# queries certificate transparency logs for child infrastructure via crt.sh json endpoint
section_header "SUBDOMAINS"
subdomains=$(timeout 15 curl -s "https://crt.sh/?q=%25.$domain&output=json" | jq -r '.[].name_value' 2>/dev/null | tr ',' '\n' | sed 's/\*\.//' | sort -u | head -n 15)
[[ -n "$subdomains" ]] && echo "$subdomains" | sed 's/^/ /' || echo " enumeration failed or none found"
# REGISTRY AUDIT
# extracts baseline domain registration metadata
section_header "WHOIS DATA"
whois_data=$(timeout "$timeout_val" whois "$domain" 2>/dev/null | grep -Ei "^registrar:|^creation date:|^updated date:" | tr -s ' ' | head -n 5)
[[ -n "$whois_data" ]] && echo "$whois_data" | sed 's/^/ /' || echo " extraction limited"
# RISK SCORE
# calculates a basic hazard rating based on missing protections and open ports
section_header "RISK SUMMARY"
risk=0
[[ ! "$https_status" =~ ^(2|3) ]] && ((risk += 2))
echo "$headers" | grep -qi "^strict-transport-security:" || ((risk += 1))
echo "$headers" | grep -qi "^content-security-policy:" || ((risk += 1))
[[ -z "$dnskey" ]] && ((risk += 1))
timeout 1 bash -c "</dev/tcp/$target_ip/22" &>/dev/null && ((risk += 1))
timeout 1 bash -c "</dev/tcp/$target_ip/21" &>/dev/null && ((risk += 2))
if [ $risk -le 1 ]; then
posture_color=$green
posture_text="low risk"
else
posture_color=$red
posture_text="risk identified"
fi
printf " domain: %s\n" "$domain"
printf " ip: %s\n" "${target_ip:-n/a}"
printf " posture: ${posture_color}%s${nc} (score: %d)\n\n" "$posture_text" "$risk"